diff --git a/admin/Makefile.am b/admin/Makefile.am index 320854ba..eb2ee624 100644 --- a/admin/Makefile.am +++ b/admin/Makefile.am @@ -31,4 +31,5 @@ nobase_dist_admin_DATA = \ includes/web-page.xsl \ ui/confirmdeleteuser.xsl \ ui/confirmkillclient.xsl \ - ui/confirmkillsource.xsl + ui/confirmkillsource.xsl \ + ui/confirmkilldumpfile.xsl diff --git a/admin/includes/mountnav.xsl b/admin/includes/mountnav.xsl index 44814cb9..1f002138 100644 --- a/admin/includes/mountnav.xsl +++ b/admin/includes/mountnav.xsl @@ -8,6 +8,14 @@
  • Move listeners
  • Metadata
  • Set fallback
  • + + +
  • Stop dumpfile
  • +
    + +
  • No dumpfile running
  • +
    +
  • Kill source
  • diff --git a/admin/ui/confirmkilldumpfile.xsl b/admin/ui/confirmkilldumpfile.xsl new file mode 100644 index 00000000..8654411b --- /dev/null +++ b/admin/ui/confirmkilldumpfile.xsl @@ -0,0 +1,24 @@ + + + + Confirm Stopping dumpfile + + + + + + Please confirm stopping the dumpfile for . + /admin/streamlist.xsl + /admin/dumpfilecontrol.xsl + + + + + + + + + + + + diff --git a/src/admin.c b/src/admin.c index 515b3115..98187ca9 100644 --- a/src/admin.c +++ b/src/admin.c @@ -143,6 +143,8 @@ #define KILLSOURCE_RAW_REQUEST "killsource" #define KILLSOURCE_HTML_REQUEST "killsource.xsl" #define KILLSOURCE_JSON_REQUEST "killsource.json" +#define DUMPFILECONTROL_RAW_REQUEST "dumpfilecontrol" +#define DUMPFILECONTROL_HTML_REQUEST "dumpfilecontrol.xsl" #define ADMIN_XSL_RESPONSE "response.xsl" #define MANAGEAUTH_RAW_REQUEST "manageauth" #define MANAGEAUTH_HTML_REQUEST "manageauth.xsl" @@ -190,6 +192,7 @@ static void command_list_listen_sockets (client_t *client, source_t *source, adm static void command_move_clients (client_t *client, source_t *source, admin_format_t response); static void command_kill_client (client_t *client, source_t *source, admin_format_t response); static void command_kill_source (client_t *client, source_t *source, admin_format_t response); +static void command_dumpfile_control (client_t *client, source_t *source, admin_format_t response); static void command_manageauth (client_t *client, source_t *source, admin_format_t response); static void command_updatemetadata (client_t *client, source_t *source, admin_format_t response); static void command_buildm3u (client_t *client, source_t *source, admin_format_t response); @@ -237,6 +240,8 @@ static const admin_command_handler_t handlers[] = { { KILLSOURCE_RAW_REQUEST, ADMINTYPE_MOUNT, ADMIN_FORMAT_RAW, ADMINSAFE_UNSAFE, command_kill_source, NULL}, { KILLSOURCE_HTML_REQUEST, ADMINTYPE_MOUNT, ADMIN_FORMAT_HTML, ADMINSAFE_UNSAFE, command_kill_source, NULL}, { KILLSOURCE_JSON_REQUEST, ADMINTYPE_MOUNT, ADMIN_FORMAT_JSON, ADMINSAFE_UNSAFE, command_kill_source, NULL}, + { DUMPFILECONTROL_RAW_REQUEST, ADMINTYPE_MOUNT, ADMIN_FORMAT_RAW, ADMINSAFE_UNSAFE, command_dumpfile_control, NULL}, + { DUMPFILECONTROL_HTML_REQUEST, ADMINTYPE_MOUNT, ADMIN_FORMAT_HTML, ADMINSAFE_UNSAFE, command_dumpfile_control, NULL}, { MANAGEAUTH_RAW_REQUEST, ADMINTYPE_GENERAL, ADMIN_FORMAT_RAW, ADMINSAFE_HYBRID, command_manageauth, NULL}, { MANAGEAUTH_HTML_REQUEST, ADMINTYPE_GENERAL, ADMIN_FORMAT_HTML, ADMINSAFE_HYBRID, command_manageauth, NULL}, { MANAGEAUTH_JSON_REQUEST, ADMINTYPE_GENERAL, ADMIN_FORMAT_JSON, ADMINSAFE_HYBRID, command_manageauth, NULL}, @@ -543,6 +548,9 @@ xmlDocPtr admin_build_sourcelist(const char *mount, client_t *client, admin_form xmlNewTextChild(srcnode, NULL, XMLSTR("content-type"), XMLSTR(source->format->contenttype)); } + + snprintf(buf, sizeof(buf), "%"PRIu64, source->dumpfile_written); + xmlNewTextChild(srcnode, NULL, XMLSTR("dumpfile_written"), XMLSTR(buf)); } node = avl_get_next(node); } @@ -1116,6 +1124,19 @@ static void command_kill_source(client_t *client, admin_send_response_simple(client, source, response, "Source Removed", 1); } +static void command_dumpfile_control (client_t *client, source_t *source, admin_format_t response) +{ + const char *action; + COMMAND_REQUIRE(client, "action", action); + + if (strcmp(action, "kill") == 0) { + source_kill_dumpfile(source); + admin_send_response_simple(client, source, response, "Dumpfile killed.", 1); + } else { + admin_send_response_simple(client, source, response, "No such action", 0); + } +} + static void command_kill_client(client_t *client, source_t *source, admin_format_t response) diff --git a/src/cfgfile.c b/src/cfgfile.c index bd286448..b6f036b7 100644 --- a/src/cfgfile.c +++ b/src/cfgfile.c @@ -1737,6 +1737,14 @@ static void _parse_mount(xmlDocPtr doc, } else if (xmlStrcmp(node->name, XMLSTR("dump-file")) == 0) { mount->dumpfile = (char *)xmlNodeListGetString(doc, node->xmlChildrenNode, 1); + } else if (xmlStrcmp(node->name, XMLSTR("dump-file-size-limit")) == 0) { + unsigned int val = mount->dumpfile_size_limit; + __read_unsigned_int(configuration, doc, node, &val, 0, UINT_MAX); + mount->dumpfile_size_limit = val; + } else if (xmlStrcmp(node->name, XMLSTR("dump-file-time-limit")) == 0) { + unsigned int val = mount->dumpfile_time_limit; + __read_unsigned_int(configuration, doc, node, &val, 0, UINT_MAX); + mount->dumpfile_time_limit = val; } else if (xmlStrcmp(node->name, XMLSTR("intro")) == 0) { mount->intro_filename = (char *)xmlNodeListGetString(doc, node->xmlChildrenNode, 1); @@ -2989,6 +2997,10 @@ static void merge_mounts(mount_proxy * dst, mount_proxy * src) if (!dst->dumpfile) dst->dumpfile = (char*)xmlStrdup((xmlChar*)src->dumpfile); + if (!dst->dumpfile_size_limit) + dst->dumpfile_size_limit = src->dumpfile_size_limit; + if (!dst->dumpfile_time_limit) + dst->dumpfile_time_limit = src->dumpfile_time_limit; if (!dst->intro_filename) dst->intro_filename = (char*)xmlStrdup((xmlChar*)src->intro_filename); if (!dst->fallback_when_full) diff --git a/src/cfgfile.h b/src/cfgfile.h index 988d5a41..7539843e 100644 --- a/src/cfgfile.h +++ b/src/cfgfile.h @@ -88,6 +88,8 @@ typedef struct _mount_proxy { * NULL to not dump. */ char *dumpfile; + uint64_t dumpfile_size_limit; + unsigned int dumpfile_time_limit; /* Send contents of file to client before the stream */ char *intro_filename; /* Switch new listener to fallback source when max listeners reached */ diff --git a/src/format_ebml.c b/src/format_ebml.c index 1e2353fa..b7954b87 100644 --- a/src/format_ebml.c +++ b/src/format_ebml.c @@ -368,33 +368,16 @@ static void ebml_free_client_data (client_t *client) client->format_data = NULL; } -static void ebml_write_buf_to_file_fail (source_t *source) -{ - ICECAST_LOG_WARN("Write to dump file failed, disabling"); - fclose (source->dumpfile); - source->dumpfile = NULL; -} - static void ebml_write_buf_to_file (source_t *source, refbuf_t *refbuf) { - ebml_source_state_t *ebml_source_state = source->format->_state; - if ( ! ebml_source_state->file_headers_written) - { - if (fwrite (ebml_source_state->header->data, 1, - ebml_source_state->header->len, - source->dumpfile) != ebml_source_state->header->len) - ebml_write_buf_to_file_fail(source); - else + if (!ebml_source_state->file_headers_written) { + if (source_write_dumpfile(source, ebml_source_state->header->data, ebml_source_state->header->len)) ebml_source_state->file_headers_written = true; } - if (fwrite (refbuf->data, 1, refbuf->len, source->dumpfile) != refbuf->len) - { - ebml_write_buf_to_file_fail(source); - } - + source_write_dumpfile(source, refbuf->data, refbuf->len); } /* internal ebml parsing */ diff --git a/src/format_mp3.c b/src/format_mp3.c index bbaaee7a..11ba7950 100644 --- a/src/format_mp3.c +++ b/src/format_mp3.c @@ -760,13 +760,6 @@ static void free_mp3_client_data (client_t *client) static void write_mp3_to_file (source_t *source, refbuf_t *refbuf) { - if (refbuf->len == 0) - return; - if (fwrite (refbuf->data, 1, refbuf->len, source->dumpfile) < (size_t)refbuf->len) - { - ICECAST_LOG_WARN("Write to dump file failed, disabling"); - fclose (source->dumpfile); - source->dumpfile = NULL; - } + source_write_dumpfile(source, refbuf->data, refbuf->len); } diff --git a/src/format_ogg.c b/src/format_ogg.c index b0d49d52..afc80d7d 100644 --- a/src/format_ogg.c +++ b/src/format_ogg.c @@ -565,21 +565,6 @@ static int write_buf_to_client(client_t *client) } -static int write_ogg_data (source_t *source, refbuf_t *refbuf) -{ - int ret = 1; - - if (fwrite (refbuf->data, 1, refbuf->len, source->dumpfile) != refbuf->len) - { - ICECAST_LOG_WARN("Write to dump file failed, disabling"); - fclose (source->dumpfile); - source->dumpfile = NULL; - ret = 0; - } - return ret; -} - - static void write_ogg_to_file (source_t *source, refbuf_t *refbuf) { ogg_state_t *ogg_info = source->format->_state; @@ -589,13 +574,11 @@ static void write_ogg_to_file (source_t *source, refbuf_t *refbuf) refbuf_t *header = refbuf->associated; while (header) { - if (write_ogg_data (source, header) == 0) + if (!source_write_dumpfile(source, header->data, header->len)) return; header = header->next; } ogg_info->file_headers = refbuf->associated; } - write_ogg_data (source, refbuf); + source_write_dumpfile(source, refbuf->data, refbuf->len); } - - diff --git a/src/format_text.c b/src/format_text.c index b9261b35..5232da00 100644 --- a/src/format_text.c +++ b/src/format_text.c @@ -54,6 +54,10 @@ static size_t skipchar(char *text, char skip, size_t len) return ret; } +static void text_to_file (source_t *source, refbuf_t *refbuf) +{ + source_write_dumpfile(source, refbuf->data, refbuf->len); +} static refbuf_t *text_get_buffer(source_t *source) { @@ -97,7 +101,7 @@ int format_text_get_plugin(source_t *source) plugin->write_buf_to_client = format_generic_write_to_client; plugin->create_client_data = NULL; plugin->free_plugin = text_free_plugin; - plugin->write_buf_to_file = NULL; + plugin->write_buf_to_file = text_to_file; plugin->set_tag = NULL; plugin->apply_settings = NULL; diff --git a/src/source.c b/src/source.c index 327ba167..2926723b 100644 --- a/src/source.c +++ b/src/source.c @@ -506,6 +506,10 @@ static refbuf_t *get_next_buffer (source_t *source) "%"PRIu64, source->format->read_bytes); stats_event_args (source->mount, "total_bytes_sent", "%"PRIu64, source->format->sent_bytes); + if (source->dumpfile) { + stats_event_args(source->mount, "dumpfile_written", + "%"PRIu64, source->dumpfile_written); + } source->client_stats_update = current + 5; } if (fds < 0) @@ -607,24 +611,31 @@ static void send_to_listener (source_t *source, client_t *client, int deletion_e /* Open the file for stream dumping. * This function should do all processing of the filename. */ -static FILE * source_open_dumpfile(const char * filename) { +static void source_open_dumpfile(source_t *source) { + const char *filename = source->dumpfilename; #ifndef _WIN32 /* some of the below functions seems not to be standard winapi functions */ + time_t curtime = time(NULL); char buffer[PATH_MAX]; - time_t curtime; struct tm *loctime; - /* Get the current time. */ - curtime = time (NULL); - /* Convert it to local time representation. */ - loctime = localtime (&curtime); + loctime = localtime(&curtime); - strftime (buffer, sizeof(buffer), filename, loctime); + strftime(buffer, sizeof(buffer), filename, loctime); filename = buffer; #endif - return fopen (filename, "ab"); + source->dumpfile = fopen(filename, "ab"); + + if (source->dumpfile) { + source->dumpfile_start = curtime; + stats_event(source->mount, "dumpfile_written", "0"); + stats_event_time_iso8601(source->mount, "dumpfile_start"); + } else { + ICECAST_LOG_WARN("Cannot open dump file \"%s\" for appending: %s, disabling.", + source->dumpfilename, strerror(errno)); + } } /* Perform any initialisation just before the stream data is processed, the header @@ -647,14 +658,7 @@ static void source_init (source_t *source) stats_event (source->mount, "listenurl", listenurl); if (source->dumpfilename != NULL) - { - source->dumpfile = source_open_dumpfile (source->dumpfilename); - if (source->dumpfile == NULL) - { - ICECAST_LOG_WARN("Cannot open dump file \"%s\" for appending: %s, disabling.", - source->dumpfilename, strerror(errno)); - } - } + source_open_dumpfile(source); /* grab a read lock, to make sure we get a chance to cleanup */ thread_rwlock_rlock (source->shutdown_rwlock); @@ -1179,13 +1183,27 @@ static void source_apply_mount (ice_config_t *config, source_t *source, mount_pr source->fallback_mount = NULL; } + /* Dumpfile settings */ if (mountinfo && mountinfo->dumpfile) { util_replace_string(&(source->dumpfilename), mountinfo->dumpfile); } else { free(source->dumpfilename); source->dumpfilename = NULL; + if (source->dumpfile) { + ICECAST_LOG_INFO("Stopping dumpfile as it is now de-configured for source %p at mountpoint %#H.", source, source->mount); + source_kill_dumpfile(source); + } } + if (mountinfo) { + source->dumpfile_size_limit = mountinfo->dumpfile_size_limit; + source->dumpfile_time_limit = mountinfo->dumpfile_time_limit; + } else { + source->dumpfile_size_limit = 0; + source->dumpfile_time_limit = 0; + } + + if (source->intro_file) { fclose (source->intro_file); @@ -1461,3 +1479,46 @@ void source_recheck_mounts (int update_all) avl_tree_unlock (global.source_tree); config_release_config(); } + +/* Writes a buffer of raw data to a dumpfile. returns true if the write was successful (and complete). */ +bool source_write_dumpfile(source_t *source, const void *buffer, size_t len) +{ + if (!source->dumpfile) + return false; + + if (!len) + return true; + + if (fwrite(buffer, 1, len, source->dumpfile) != len) { + ICECAST_LOG_WARN("Write to dump file failed, disabling"); + source_kill_dumpfile(source); + return false; + } + + source->dumpfile_written += len; + + if (source->dumpfile_size_limit && source->dumpfile_written > source->dumpfile_size_limit) { + ICECAST_LOG_INFO("Dumpfile for source %p at mountpoint %#H reached size limit. Dumpfile will be disabled.", source, source->mount); + source_kill_dumpfile(source); + } else if (source->dumpfile_time_limit) { + time_t now = time(NULL); + if (now > (source->dumpfile_start + source->dumpfile_time_limit)) { + ICECAST_LOG_INFO("Dumpfile for source %p at mountpoint %#H reached time limit. Dumpfile will be disabled.", source, source->mount); + source_kill_dumpfile(source); + } + } + + return true; +} + +void source_kill_dumpfile(source_t *source) +{ + if (!source->dumpfile) + return; + + fclose(source->dumpfile); + source->dumpfile = NULL; + source->dumpfile_written = 0; + stats_event(source->mount, "dumpfile_written", NULL); + stats_event(source->mount, "dumpfile_start", NULL); +} diff --git a/src/source.h b/src/source.h index 784dcaf6..a9b6c042 100644 --- a/src/source.h +++ b/src/source.h @@ -54,8 +54,15 @@ struct source_tag { FILE *intro_file; + /* Dumpfile related data */ + /* Config */ char *dumpfilename; /* Name of a file to dump incoming stream to */ + uint64_t dumpfile_size_limit; + unsigned int dumpfile_time_limit; + /* Runtime */ FILE *dumpfile; + time_t dumpfile_start; + uint64_t dumpfile_written; unsigned long peak_listeners; unsigned long listeners; @@ -104,6 +111,10 @@ int source_remove_client(void *key); void source_main(source_t *source); void source_recheck_mounts (int update_all); +/* Writes a buffer of raw data to a dumpfile. returns true if the write was successful (and complete). */ +bool source_write_dumpfile(source_t *source, const void *buffer, size_t len); +void source_kill_dumpfile(source_t *source); + extern mutex_t move_clients_mutex; #endif diff --git a/web/assets/css/style.css b/web/assets/css/style.css index 0240029d..eec468b0 100644 --- a/web/assets/css/style.css +++ b/web/assets/css/style.css @@ -156,6 +156,10 @@ ul.boxnav > li.critical > a, a.critical, input[type='submit'].critical { background-color: #ff704d !important; } +ul.boxnav > li.disabled > a, a.disabled, input[type='submit'].disabled { + background-color: #a4a4a4 !important; +} + th.actions { width: 10%; }