/* * kissdx - KiSS PC-Link Daemon eXtended (based on kissd) * * Copyright (C) 2005 Stelian Pop * Portions Copyright (C) 2006 Vidar Tysse * Portions Copyright (C) 2007 Olivier Kahn * * Heavily based on kiss4lin, * Copyright (C) 2004 Jacob Kolding * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2, or (at your option) * any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA. * */ #include #include #include // gethostname #include #include #include #include #include #include #include #include // Standard GNU Exit code #include #include #include #include #include #include #include #include "kissdx.h" #include "utils.h" #include "config.h" #include "connection.h" #include "cmdclient.h" #include "cmdserver.h" // Global variables option_set_t options; int is_socket_timed_out = 0; char commandserver_path[PATH_MAX] = ""; // tmp file for socket "Local Command" char launchedArgument[PATH_MAX] = ""; // Command line arguments char launchedTimeDate[32] = ""; // DateTime of starting kissdx (for uptime feature) time_t last_time_list_hidden_entries_on = (time_t)0; // Used to time out user selection to display hidden content int is_image_scaling_enabled = 1; // Used to toggle image scaling on / off by user request // Set of text constant defined by default language char inetAddr_MediaCenter[64] = "127.0.0.1"; // Default ip adress to send the admin command // Set of flag managing priority between CommandLine_Options (CLO) and kissdx.conf_Options (KCO) short adminserver_port_CLO = 0; // Default: adminserver_port is not detected from command line options short loglevel_CLO = 0; // Default: loglevel is not detected in commandline options // Local constants const char *MsgErr_Config_line = "==================================================================="; const char *MsgErr_Config_fatalerror = ">> Fatal error preventing kissdx to start (See previous message) <<"; const char *MsgErr_Config_helpscreen = ">> Refer to the following full command line options help screen <<"; const char *MsgErr_Config_daemonmode = ">> Refer to the system log for more information (eg. /var/log/syslog) <<"; // ======================================================== // Prefix log information with [pid] time-hour // ex: [14084] 2007-01-30 21:31:46 Using config file ./kissdx.conf char * extended_logfile_format(const char *format) { static char extended_format[1024]; if (config.loglevel == LOGDEBUG) { char timestr[32]; time_t current_time = time(NULL); strftime(timestr, sizeof timestr, "%F %T", localtime(¤t_time)); snprintf(extended_format, sizeof extended_format, "[%5d] %s %s", getpid(), timestr, format); } else { strncpy(extended_format, format, sizeof extended_format); } return extended_format; } // ======================================================== // Handle the reception of SIGTERM or SIGKILL or SIGINT (see signal() in do_daemon() ) static void server_shutdown(int signal) { loglevel(LOGSTD,"%s", "Shutting down kissdx..."); // Delete temporary files created for socket "Local Command" if (*commandserver_path) unlink(commandserver_path); if (*config.pidfilepath) unlink(config.pidfilepath); // Exit normally exit(0); } // ======================================================== // Handle the reception of SIGCHLD (Child status has changed (POSIX).) // Wait for the status of child (clean exit of child to avoid zombies) static void kill_children(int signal) { loglevel(LOGDEBUG,"Debug: Signal SIGCHLD received '%d'",signal); int pid, preserved_errno = errno; do { do { errno = 0; pid = waitpid(-1, NULL, WNOHANG); } while (pid <= 0 && errno == EINTR); if (pid > 0) logv("Child process [%5d] exited", pid); } while (pid > 0); errno = preserved_errno; } // ======================================================== static void create_socket(int *sock, int port, int type) { struct sockaddr_in sin; int yes = 1; /* get an internet domain socket */ if ((*sock = socket(AF_INET, type, 0)) < 0) { loglevel(LOGERROR,"Error: create_socket socket type (%d) %s",type,strerror(errno)); exit(1); } // Refer Linux programming unleashed : // We want multiple processes on the same computer to have access to the same port, // so we must use setsockopt to make the socket shareable: // allow multiple processes to use this same port: // u_char loop = 1; // we want broadcast to also loop back to this computer // loop = 1; // setsockopt(out_socket_descriptor, SOL_SOCKET, SO_REUSEADDR,&loop, sizeof(loop)); /* lose the pesky "address already in use" error message */ if (setsockopt(*sock, SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(int)) < 0) { loglevel(LOGERROR,"Error: create_socket setsockopt port:type (%d:%d) %s",port,type,strerror(errno)); exit(1); } /* complete the socket structure */ // A broadcast request is not received if restricted listen memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; if (type == SOCK_DGRAM) { // We must listen for UDP discovery broadcasts on all interfaces. // NOTE: We really should use INADDR_BROADCAST here, but that fails on Cygwin/Winsock. sin.sin_addr.s_addr = INADDR_ANY; } else { // Listen for player / admin connections on the configured address only sin.sin_addr = config.listenaddress; } sin.sin_port = htons(port); /* bind the socket to the port number */ /* Error message is based on config.listenaddress that is 99% of classic error */ if (bind(*sock, (struct sockaddr *) &sin, sizeof(sin)) < 0) { loglevel(LOGERROR,"Help: Please check no other program already use port %s","8000/8888/8003"); loglevel(LOGERROR,"Help: Refer kissdx documentation and FAQ %s",""); loglevel(LOGERROR,"Error: create_socket binding address:port (%s:%d) %s", inet_ntoa(config.listenaddress),port, strerror(errno)); exit(1); } /* everything done for a udp socket / no need to start a listener */ if (type == SOCK_DGRAM) { return; } /* listen up to 10 incoming connection queue = simultanous TCP call */ /* Stress capabilities and heavy load support may need to increase this queue size */ /* Analysis in running system : netstat -s -p tcp |grep "overflow" */ /* candidate to be a parameter in .conf */ if (listen(*sock, 10) < 0) { loglevel(LOGERROR,"Error: create_socket listen up to 10 incomming request %s",strerror(errno)); exit(1); } } // ======================================================== static void udp_responder (int sd) { // UDP listener to respond on "ARE_YOU_KISS_PCLINK_SERVER?" request // Based on config.configautoload(yes/no) a new parsing of config is triggered struct sockaddr_in sin; socklen_t sinlen = sizeof(sin); char udp_buf[128]; const char *identityRequest ="ARE_YOU_KISS_PCLINK_SERVER?"; /* complete the socket structure */ memset(&sin, 0, sizeof(sin)); sin.sin_family = AF_INET; sin.sin_addr = config.listenaddress; sin.sin_port = htons(PORT_PCLINK); if (recvfrom(sd, udp_buf, sizeof(udp_buf), 0, (struct sockaddr*)&sin, &sinlen) < 0) { log("Err: recvfrom: %s", strerror(errno)); return; // exit gracefully } if (!strstr(udp_buf, identityRequest)) { return; // no other UDP request could be treated than ARE_YOU_KISS..... } loglevel(LOGPROTO,"UDP:8000 Received identity request '%s' from %s", identityRequest,inet_ntoa(sin.sin_addr)); strncpy(udp_buf, config.serversignature, sizeof(udp_buf)); if (config.configautoload) { // Re-read config file to get at any changes. Keep current configuration if error. config_settings_t new_config; int config_error = parse_config(&new_config, NULL); if (!config_error) { free_config(&config); memcpy(&config, &new_config, sizeof (config)); } else { loglevel(LOGERROR,"Loading new config failed from : %s",new_config.config_file); loglevel(LOGERROR,"Keep previous configuration from : %s",config.config_file); show_current_config_Verbose(&config); // Show the old (still current) configuration in verbose mode } loglevel(LOGSTD,"Service kissdx '%s-%s' is ready for streaming business ...",KISSDX_VERSION,KISSDX_VERSION_DATE); // response with hostname signature string to KiSS player + ConfigReloadStatus information string_substitute(udp_buf, "{ConfigReloadStatus}", config_error? "(conf!)" : "(OK)", sizeof udp_buf); } else { // response with hostname signature string to KiSS player string_substitute(udp_buf, "{ConfigReloadStatus}", " ", sizeof udp_buf); } loglevel(LOGDEBUG,"Debug: Server signature : %s",udp_buf); const char* buf_p = can_convert_charset(cept_config, cept_client)? convert_charset(udp_buf, cept_config, cept_client) : udp_buf; if (sendto(sd, buf_p, strlen(buf_p), 0, (struct sockaddr*)&sin, sizeof(sin)) < 0) { loglevel(LOGERROR,"Error: sending '%s' : %s", buf_p,strerror(errno)); } } // ======================================================== static void do_daemon(void) { // message for verbose mode to isolate 'heavy load task'(fork) and 'light load task'(no fork) const char *msg_noforkneeded ="Admin command parsing detects light action with no fork needed"; int sd_server, sd_client; // 'sd' stand for Socket Descriptor int sd_kml = -1; // default behavior is not to create a KML forwarder feature int sd_pclink; // Receive library browsing and media GET int sd_udp_responder; // Receive identity request int sd_loccmd; // receiving local command (e.g. hidden file ) int sd_admcmd = -1; // receiving Admin Command on TCP:8003 short udp_pclink_req = 0; // flag : which socket type of request have we received ? short tcp_pclink_req = 0; short tcp_kml_req = 0; short tcp_admcmd_req = 0; short tcp_loccmd_req = 0; enum admin_request_t admin_command_request_type = adm_unknown ; // Each admin command is referenced in servercmd.h short enable_fork = 0; // flag : does the admin command received is 'light' time consuming task (no fork) (0=false) struct sockaddr_in pin; socklen_t addrlen; fd_set set; struct timeval *listener_select_timeout = NULL; // No timeout : To block indefinitely until one of the file descriptors is ready pid_t child_pid; // child pid pid_t daemon_pid; // daemon pid // Set handler for signal: // Server Interuption => action : Remove tmp socket // Child process Interuption = Clean wait status signal(SIGINT, server_shutdown); // Signal sent by a Ctrl C in a terminal signal(SIGTERM, server_shutdown); // Signal sent by a kill command signal(SIGCHLD, kill_children); // Signal sent by a child change of status if (options.daemonize) openlog("kissdx", LOG_PID, LOG_DAEMON); loglevel(LOGSTD,"Service kissdx '%s-%s' opening listener ...", KISSDX_VERSION,KISSDX_VERSION_DATE ); if (*config.pidfilepath) { FILE *pidfile; if ((pidfile = fopen(config.pidfilepath, "w")) != NULL) { fprintf(pidfile, "%ld\n", (long) getpid()); fclose(pidfile); } else { loglevel(LOGERROR,"Error: creating pidfile '%s': %s", config.pidfilepath, strerror(errno)); } } // PCLINK listener on TCP port : TCP.PCLINK and UDP.PCLINK from kiSS player loglevel(LOGINFO,"Info: Trying to start binding on port %d",PORT_PCLINK); create_socket(&sd_pclink, PORT_PCLINK, SOCK_STREAM); // Stream = TCP 8000 default loglevel(LOGINFO,"Info: kissdx listener PCLINK TCP port %d started",PORT_PCLINK); create_socket(&sd_udp_responder, PORT_PCLINK, SOCK_DGRAM); // Dgram = UDP 8000 default loglevel(LOGINFO,"Info: kissdx listener PCLINK UDP port %d started",PORT_PCLINK); // KML listener is configure in kissdx.conf and started only if needed if (strlen(config.kmlfwdurl) != 0) { loglevel(LOGINFO,"Info: Trying to start binding on port %d",PORT_KML); create_socket(&sd_kml, PORT_KML, SOCK_STREAM); loglevel(LOGINFO,"Info: kissdx listener KML.Fwd TCP port %d started",PORT_KML); } // Command Admin listener is created for Admin purpose (Stop, Save Config....) if (config.adminserver_port > 0) { loglevel(LOGINFO,"Info: Trying to start binding on port %d",config.adminserver_port); create_socket(&sd_admcmd, config.adminserver_port, SOCK_STREAM); // TCP: 8003 default loglevel(LOGINFO,"kissdx listener ADM.CMD TCP port %d started",config.adminserver_port); } // Local Command listener targets the child purpose of options change (hidden options ...) // Master daemon and forked child commmunicate via LocalSocket (based on tmp file socket) setup_local_commandserver_socket(&sd_loccmd); loglevel(LOGSTD,"Service kissdx '%s-%s' is ready for streaming business ...",KISSDX_VERSION,KISSDX_VERSION_DATE); // -- Loop for daemon ---- again: // Automatically reset config.listhiddenentries after a timeout period. if (config.enablehiddenfilesminutes && last_time_list_hidden_entries_on != (time_t)0) { if (difftime(time(NULL), last_time_list_hidden_entries_on) / 60.0 >= (double)config.enablehiddenfilesminutes) { config.listhiddenentries = 0; last_time_list_hidden_entries_on = (time_t)0; } } // Starting listening for incoming request udp_pclink_req = tcp_pclink_req = tcp_kml_req = tcp_admcmd_req = tcp_loccmd_req = 0; //request flag reset // Request received may be a light timeconsuming task (Configuration change, GetVersion, ...) // But for security, default expectation of incomming request is heavy load, generating a fork (preserve Daemon) enable_fork = 0; FD_ZERO(&set); // Initialise the file descriptor destinated to select() (identify entry request tcp/udp) FD_SET(sd_pclink, &set); // Add file descriptor of listener PCLINK FD_SET(sd_udp_responder, &set); // Add file descriptor of listener UDP "Identity request" FD_SET(sd_loccmd, &set); // Add file descriptor of listener LocalCommand if (config.adminserver_port > 0) { FD_SET(sd_admcmd, &set); // Add file descriptor of listener AdministrationCommand } if (strlen(config.kmlfwdurl) != 0) { FD_SET(sd_kml, &set); // Add file descriptor of listener KML Forwarder } memset(&pin, 0, sizeof(pin)); memset(&addrlen, 0, sizeof(addrlen)); // Design of TCP request treatment : // kissdx listen to multiple request type (pclink, udp, admcmd, localcmd, kmlforwarder ....) // Select() does not lost request but response with the total number of ready file descriptors in all of the sets // As the current implementation treats only once request (set) per dodaemon.loop, a priority of treatment is hard coded // - a) Identify each request ready to be treated (and set the corresponding flag (eg. udp_pclink_req=1)) // - b) Get the socket descriptor in the following priority : PCLINK (browsing/streaming) > > KMLFwd > > ADMCMD // - c) Some request are so quick to serve that a new daemon.loop is a waste of time, they are treated online in daemon itself // UDP broadcast (identity request) and TCP.LocalSocket (set of Hidden options to ON/OFF) (not the image generation) // This order afford no freeze in video streaming, and treat other task in a second priority if (select(max(max(max(max(sd_admcmd,sd_pclink), sd_kml), sd_udp_responder), sd_loccmd) + 1, &set, NULL,NULL,listener_select_timeout) < 0) { if (errno == EINTR) // The operation was interrupted by a signal goto again; loglevel(LOGERROR,"ERROR: Socket scanning select: %s", strerror(errno)); exit(1); } // TASK treated directly in daemon without impact on capability to respond to KiSS tcp request // ==== Task qualified "light time consuming task" : UDP and LocalCmd // UDP broadcast reception from KiSS player (=> answers with server identification) // UDP response is treated in master daemon (against TCP PCLINK treated in a dedicated Child) if (FD_ISSET(sd_udp_responder, &set)) { udp_pclink_req = 1; // Flag "request comes from UDP PCLINK" udp_responder(sd_udp_responder);// Send back a server signature "kissdx vXX.YY ..." } // Local_socket reception from kissdx.Child options change request (HIDDEN options set/reset) // treated in master daemon if (FD_ISSET(sd_loccmd, &set)) { tcp_loccmd_req = 1; // Flag "request comes from Child local command" enable_fork = 0; // Default behaviors of local command is !EnableFork do { sd_client = accept(sd_loccmd, (struct sockaddr *)&pin, &addrlen); } while (sd_client < 0 && errno == EINTR); if (sd_client < 0) { loglevel(LOGERROR,"ERROR: Local command accept: %s", strerror(errno)); exit(1); } handle_local_command(sd_client); } // TASK treated in child (fork) due to impact on capability to respond to KiSS tcp request // ==== Task qualified "heavy load task" : // Priority of treatment : PCLINK (browsing/streaming) > > KMLFwd > > ADMCMD // Preparation of fork child configuration (need to accept a socket for sd_server and create sd_client) // Set the socket descriptor of server to the listening socket ready to be read (due to request reception) sd_server = -1; // no server detected (initialisation) if (FD_ISSET(sd_pclink, &set)) { tcp_pclink_req = 1; // Flag "request comes from TCP PCLINK" enable_fork = 1; // Default behaviors of PCLINK command is EnableFork sd_server = sd_pclink; } else if ( (strlen(config.kmlfwdurl) != 0) && (FD_ISSET(sd_kml, &set))) { tcp_kml_req = 1; // Flag "request comes from TCP KML" enable_fork = 1; sd_server = sd_kml; } else if ( (config.adminserver_port > 0) && FD_ISSET(sd_admcmd, &set)) { tcp_admcmd_req = 1; // Flag "request comes from TCP Admin Command" enable_fork = 0; // Default behaviors of admin command is not EnableFork (simple get/set action) sd_server = sd_admcmd; } // Flag feature (eg. tcp_admcmd_req) enable to focus on the most prioritary listener // even if FD_ISSET() may be true for multiple listeners (set) // Accept TCP socket and decide if needed to fork to answer to network request // Maybe a listener of type : PCLINK or KMLFwd or ADMCMD if (sd_server != -1) { do { sd_client = accept(sd_server, (struct sockaddr *)&pin, &addrlen); } while (sd_client < 0 && errno == EINTR); if (sd_client < 0) { loglevel(LOGERROR,"ERROR: TCP request accept: %s", strerror(errno)); exit(1); } // TCP.CommandAdmin server request // ==> Detects which admin command use case is requested ? // ==> Does this command afford to be treated by daemon itself or need a forked child? (enable_fork=0) if (tcp_admcmd_req) { // Read socket, store it in options.admincommand, parse it and isolate which admin usecase is requested // Part of admincommand are treated inside fct.parse_admcmd_request() as it's so light action requested // Ex: Set ListHiddenEntry On is done directly in parse fct, and return an 'adm_set' status (done) // Ex: Get ListHiddenEntry is done directly in parse fct, and return an 'adm_get' status (done) admin_command_request_type = parse_admcmd_request(sd_client, options.admincommand, options_admincommand_len); switch(admin_command_request_type) { case adm_unknown: //unknown command detected enable_fork = 0; // UNKNOWN is no heavy load task, no need to fork() loglevel(LOGERROR,"ERROR: Admin command unknown : %s",options.admincommand); break; case adm_stop: enable_fork = 0; // STOP is no heavy load task, no need to fork() loglevel(LOGDEBUG,"DEBUG: %s",msg_noforkneeded); handle_admincommand_stop(getpid(),sd_client); break; case adm_forcestop: enable_fork = 0; // FORCESTOP is no heavy load task, no need to fork() loglevel(LOGDEBUG,"DEBUG: %s",msg_noforkneeded); handle_admincommand_forcestop(sd_client); break; case adm_reloadconfig: enable_fork = 0; // RELOADCONFIG is no heavy load task, no need to fork() loglevel(LOGDEBUG,"DEBUG: %s",msg_noforkneeded); handle_admincommand_reloadconfig(sd_client); break; case adm_gettxtconfig: enable_fork = 0; // GET_TXT_CONFIG is no heavy load task, no need to fork() loglevel(LOGDEBUG,"DEBUG: %s",msg_noforkneeded); handle_admincommand_gettxtconfig(sd_client); break; case adm_getdict: enable_fork = 0; // GET_DICT is no heavy load task, no need to fork() loglevel(LOGDEBUG,"DEBUG: %s",msg_noforkneeded); handle_admincommand_getdict(sd_client); break; case adm_experimental: // future feature (experimental) // break; default: break; } // switch case of admcmd // admin light task are done, then close socket if (!enable_fork) { loglevel(LOGDEBUG,"Debug: Socket closed by kissdx engine; socket %d",sd_client); close(sd_client); } } // if admin command from TCP receiver // =================================================================================================== // if heavy resources task requiring a fork if (enable_fork) { loglevel(LOGDEBUG,"Debug: Serving need to fork %s",""); daemon_pid = getpid(); child_pid = fork(); switch (child_pid) { case -1: // Fork error wile attempting to fork log("Err: fork: %s", strerror(errno)); exit(1); case 0: // Child execution close(sd_server); // during fork, a heritage of link on sd_server // close is needed to let parent daemon manage this socket sd_server // TCP.PCLINK or TCP.KML comes from KiSS player if ( tcp_pclink_req || tcp_kml_req ) { logv("%s", "KiSS started connection"); if (tcp_pclink_req) handle_pclink_request(sd_client); if (tcp_kml_req) handle_kml_request(sd_client); } // OGG Vorbis media "KiSS unclosed socket" handling (until solving by a new firmware) if (is_socket_timed_out) shutdown(sd_client, SHUT_RDWR); // Child should close the socket accepted before forking (the server should also, BTW) close(sd_client); // closed connection log message if ((tcp_pclink_req || tcp_kml_req || tcp_admcmd_req) && !(is_socket_timed_out) ) loglevel(LOGINFO,"INFO: %s","Player KiSS closed connection"); else if ((tcp_pclink_req || tcp_kml_req || tcp_admcmd_req) && (is_socket_timed_out) ) loglevel(LOGINFO,"INFO: %s","Engine kissdx(fork) close connection"); //logv("Child process termination exit [%d]",getpid()); _exit(0); default: // Parent execution logv("kissdx fork child for services [%d]",child_pid); close(sd_client); // Server action of closing socket accepted before forking break; } // end "if enable fork" } // if heavy timeconsuming task requiring a fork } // endif server socket ready (sd_server >0) goto again; loglevel(LOGDEBUG,">>>> AFTER %s","goto AGAIN ==>> END"); /* not reached */ } // ======================================================== static void usage(const char *program, int verbose) { fprintf(stderr, "\n" "KiSS PC-Link daemon " KISSDX_VERSION " - " KISSDX_VERSION_DATE "\n\n" "Usage: %s [media center options] | [admin options]\n" "\nNote: Option sets 'Media' and 'Admin' are mutually exclusive.\n" "kissdx may be invoked as a server: Media center\n" "kissdx may be invoded as a client: Administration command\n" "\nMedia center options:\n" "---------------------\n" " -c, --config FILE Path to the configuration file\n" " -d, --daemon Fork into background and log using syslog\n" " -h, --help Print this on-line help\n" " -vn,--verbose=n Set logging level (1 to 5). No level means 5.\n" " The logging level can also be specified as one of:\n" " Error, Warning, Protocol, Information or Debug\n" "\nAdministration options: -s COMMAND [-m HOST] [-p PORT]\n" "-----------------------\n" " -s, --send COMMAND Invoke admin controller with COMMAND\n" " -m, --mediacenter Target name/IP to send command (default: localhost)\n" " -p, --port TCP port to send admin command (default: 8003)\n" "\nAdministration commands: -s COMMAND\n" "------------------------\n" " Refer verbose help for full command set explanation.\n" " -s STOP Stop kissdx mediacenter gracefully (-s s)\n" " -s FORCESTOP Stop kissdx mediacenter immediately (-s frs)\n" " -s RELOAD_CONFIG Reload configuration file (-s rlc)\n" " -s GET_TXT_CONFIG Show currently loaded configuration (-s gtc)\n" "\nSamples:\n" "-------\n" " kissdx -vh Verbose help including network TCP port usage\n" " kissdx -s STOP -v Graceful stop (local run): Current streaming continues\n" " kissdx -s STOP -p 8003 -m media.home.fr\n" " Remote stopping of a media center\n" , program); if (verbose) { fprintf(stderr, // Verbose help screen compliant with standard terminal width (90) "/------\\ /----------------------------\\ \n" "|kissdx| | KiSS player |\n" "|daemon|<= Q.UDP:8000 <==== ARE_YOU_KISS_PCLINK_SERVER? |Menu:Searching PCLINK host |\n" "| |=> A.UDP:8000 ======= HOSTNAME ==================>|(signature of mediacenter) |\n" "| | | |\n" "|daemon|<= Q.PCLINK:8000 <= GET,SIZE,LIST,ACTION1,ACTION2 |Action from remote control |\n" "|(fork)|=> A.PCLINK:8000 == MEDIA or BROWSING LIRARY ====>|(stream video,music,picture)|\n" "| | | |\n" "|daemon|<= Q.KML:8888 <= GET index.kml |Action from remote control |\n" "|(fork)|=> A.KML:8888 ==== Any KML content page ======>|(browsing like web does) |\n" "| | \\----------------------------/\n" "| |\n" "| | /----------------------------\\ \n" "|daemon|<= Q.ADMCMD:8003 <= STOP,FORCESTOP,RELOADCONFIG...|kissdx or any ADMCMD client |\n" "|(fork)|=> A.ADMCMD:8003 === Content related to ADMCMD ==>|(administration information)|\n" "\\------/ \\----------------------------/\n" "\nSome admin command to ease diagnostics: -s COMMAND\n" "---------------------------------------\n" " -s lau List audio \n" " -s lpi List picture\n" " -s lvi List video \n" " -s lvi /video/kids/ Commands could browse the mediacenter\n" "\nSome admin command for developpers: -s Setter & Getter\n" "-----------------------------------\n" " -s {verror;vwarn;vinfo;vproto;vdebug;v1..v5} Set VerboseLogLevel \n" " -s {slisthidden.yes;slisthidden.no;} Set ListHiddenEntry Yes/No \n" " -s {scal.yes;scal.no;} Set ConfigAutoLoad Yes/No \n" " -s {sdsn.yes;sdsn.no;} Set DisplaySequenceNumber Yes/No \n" "\n" " -s {gksv;gksvd} Get KissdxServerVersion; KissdxServerVersionDate\n" " -s {glarg;gldt} Get launch command line arguments; launch date time\n" " -s {glcus} Get launch command line custom options (-x -y -z)\n" " -s {gdic} Retrieve dictionary of supported admin commands\n" " -s {gverbose;glisthidden} Get VerboseLogLevel; ListHiddenEntry \n" " -s {gcal;gdsn} Get ConfigAutoLoad; DisplaySequenceNumber \n" "\n" ); } // end verbose mode of help usage screen } // ======================================================== int main(int argc, char *argv[]) { int c,i; /* getopt_long stores the option index here. */ int option_index = 0; short commandline_error = 0; // flag of command line argument error detection short admincommandmode = 0; // flag of admin command mode detection short mediastreamingmode = 0; // flag of media streaming server mode detection const int verbose_usage = 1; static struct option long_options[] = { { "config", required_argument, NULL, 'c' }, // 1= one parameter is requested (the configuration file) { "daemon", no_argument, NULL, 'd' }, { "help", no_argument, NULL, 'h' }, { "verbose", optional_argument, NULL, 'v' }, // [error(1);warn(2);proto(3);info(4);debug(5)] // Administration feature options { "send", required_argument, NULL, 's' }, // parameter is the command to send to media center { "mediacenter",required_argument, NULL, 'm' }, // Full hostname of mediacenter to administrate { "port", required_argument, NULL, 'p' }, // TCP Port to connect mediacenter and send admin command { "custoption1",required_argument, NULL, 'x' }, // Custom option free to be used by end user { "custoption2",required_argument, NULL, 'y' }, // Custom option free to be used by end user { "custoption3",required_argument, NULL, 'z' }, // Custom option free to be used by end user { NULL, 0, NULL, 0 } // standard ending records for options feature in GNU C }; // Store kissdx starting time (global value) time_t current_time = time(NULL); strftime(launchedTimeDate, sizeof launchedTimeDate, "%F %T", localtime(¤t_time)); loglevel(LOGSTD,"Starting timestamp :[%s]",launchedTimeDate); setlocale (LC_ALL, ""); memset(&options, 0, sizeof (options)); // Default value of options options.mediacenter = (char *)inetAddr_MediaCenter ; // localhost mediacenter is targeted options.adminserver_port = PORT_ADMCMD; // default port is 8003 options.help = 0; // no help request (in command line argument) options.loglevel = 1; // Error only log level config.loglevel = LOGERROR; // enable kissdx.h/loglevel() to display error during config parse *options.adminparam = '\0'; // reset any adm cmd parameter // Store options set at launch time for (i = 1; i < argc; ++i) { sprintf(launchedArgument,"%s%s ", launchedArgument, argv[i]); } if (strlen(launchedArgument)==0) { strcpy(launchedArgument, "-");} loglevel(LOGSTD,"Command line launch argument :[%s]",launchedArgument); while (1) { c = getopt_long(argc, argv, "-c:dhv::s:m:p:x:y:z:", long_options, &option_index); if (c == -1) { break; } //if (!optarg) loglevel (LOGERROR,"DEBUG Command line argument :'%c'='0'",c); // else loglevel (LOGERROR,"DEBUG Command line argument :'%c'='%s'",c,optarg); switch (c) { case 1: // argument without option name (used in Admin command parameter) loglevel (LOGDEBUG,"DEBUG Command line argument :'%s'",optarg); strncpy(options.adminparam, optarg, sizeof(options.adminparam)); break; case 'c': // Configuration file mediastreamingmode = 1; options.config_file = optarg; break; case 'd': // Daemon mediastreamingmode = 1; options.daemonize = 1; break; // Administration command options case 's': // Send admin command to mediacenter admincommandmode = 1; if (strlen(optarg) > 0) { log("Admin command requested : %s",optarg); strncpy(options.admincommand, optarg, sizeof(options.admincommand)); } else { // wrong admin command strncpy(options.admincommand, "ERROR", sizeof(options.admincommand)); } break; case 'm': // Mediacenter full hostname or IP adress for Admin command admincommandmode = 1; options.mediacenter = optarg; break; case 'p': // Specify port of media center to be sent an admin command options.adminserver_port = atoi(optarg); adminserver_port_CLO = 1; // flag : Commmand line options 'adminserver_port' priority on kissdx_conf_options break; case 'h': // Help mode (cumulative with verbose mode) options.help = 1; break; case 'v': // verbose mode expected to log level // if verbose log level is outside [1..5], default value occurs as 1:Error only if (!optarg) {options.loglevel = LOGDEBUG; loglevel_CLO = 1;} // --verbose else if (atoi(optarg) == 0) { // value is ascii int arglen = strlen(optarg); if (!strcasecmp(optarg, "0")) {options.loglevel = LOGERROR; loglevel_CLO = 1;} // -v0=-v1 else if (!strncasecmp(optarg, "error",arglen)) {options.loglevel = LOGERROR; loglevel_CLO = 1;} else if (!strncasecmp(optarg, "warning",arglen)) {options.loglevel = LOGWARN; loglevel_CLO = 1;} else if (!strncasecmp(optarg, "protocol",arglen)) {options.loglevel = LOGPROTO; loglevel_CLO = 1;} else if (!strncasecmp(optarg, "information",arglen)) {options.loglevel = LOGINFO; loglevel_CLO = 1;} else if (!strncasecmp(optarg, "debug",arglen)) {options.loglevel = LOGDEBUG; loglevel_CLO = 1;} else { // unknown ascii value or zero value commandline_error=1; loglevel(LOGERROR,"Error: Unknow value for option loglevel:'%s'. Refer help screen.",optarg); } } else if ( atoi(optarg)<=LOGSTD || atoi(optarg)>LOGDEBUG) { loglevel(LOGERROR,"Error: verbose command line expected [1..5|error..debug]; read '%s'", optarg); commandline_error=1; } else if (atoi(optarg)>LOGSTD && atoi(optarg)<=LOGDEBUG) { options.loglevel = atoi(optarg); loglevel(LOGDEBUG,"Debug: verbose loglevel read in CLO :%d", atoi(optarg)); loglevel_CLO = 1; // flag : Commmand line options 'loglevel' priority on kissdx_conf_options } break; case 'x': // Custom options argurment 01 strncpy(options.custoption1, optarg, sizeof(options.custoption1)); break; case 'y': // Custom options argurment 02 strncpy(options.custoption2, optarg, sizeof(options.custoption2)); break; case 'z': // Custom options argurment 03 strncpy(options.custoption3, optarg, sizeof(options.custoption3)); break; case '?': // Argument missing or not consistent with declaration "ac:dhkvs:m:p:" => EXIT commandline_error=1; break; //default: } } // Section for Options Consistency Check of Command line parameter // Does not accept a "Admin command mode " + "daemon config mode" ..... // answer is returned with an usage(argv[0]);exit(EX_USAGE); // many test should validate the acceptance of command requested // ex: -vc kissdx.conf -s STOP -s FORCESTOP -s STATUS -a ==> need usage() to be displayed again // First: if command line options error => help and quit if (commandline_error) { loglevel(LOGERROR,"%s", MsgErr_Config_line); loglevel(LOGERROR,"%s", MsgErr_Config_fatalerror); loglevel(LOGERROR,"%s", MsgErr_Config_helpscreen); loglevel(LOGERROR,"%s", MsgErr_Config_line); usage(argv[0],verbose_usage); exit(EX_USAGE); } // error in command line options if (options.help) { usage (argv[0],options.loglevel==LOGDEBUG ? 1 : 0); exit(EX_USAGE); } // help request // Server mode and Client mode is incompatible => Err // Admin command SEND + [Any All-Config-Daemon-Kml] option => help and quit if ( (admincommandmode) && (mediastreamingmode) ) { usage(argv[0],1); exit(EX_USAGE); } // Reject any AdminCommand not in the dictionary // Admin command SEND + COMMAND not in ['STOP','FORCESTOP','RELOAD_CONFIG' ....] => help and quit if (strlen(options.admincommand) > 0) { if ( (strcasecmp(options.admincommand, ADMCMD_STOP)) && (strcasecmp(options.admincommand, "s")) // stop for developer && (strcasecmp(options.admincommand, ADMCMD_FORCESTOP)) && (strcasecmp(options.admincommand, "frs")) // forcestop && (strcasecmp(options.admincommand, ADMCMD_RELOADCONFIG)) && (strcasecmp(options.admincommand, "rlc")) // reloadconfig && (strcasecmp(options.admincommand, ADMCMD_GETTXTCONFIG)) && (strcasecmp(options.admincommand, "gtc")) // Get txt config shortcut for dev && (strcasecmp(options.admincommand, "slisthidden.yes")) // SET List Hidden Entry YES (only for developers) && (strcasecmp(options.admincommand, "slisthidden.no")) // SET List Hidden Entry NO (only for developers) && (strcasecmp(options.admincommand, "scal.yes")) // SET Config Auto Load YES (only for developers) && (strcasecmp(options.admincommand, "scal.no")) // SET LConfig Auto Load NO (only for developers) && (strcasecmp(options.admincommand, "sdsn.yes")) // SET Display Seq Number YES (only for developers) && (strcasecmp(options.admincommand, "sdsn.no")) // SET Display Seq Number NO (only for developers) && (strcasecmp(options.admincommand, "v1")) // verbose level1 only for dev internal test && (strcasecmp(options.admincommand, "v2")) // verbose level2 && (strcasecmp(options.admincommand, "v3")) // verbose level3 && (strcasecmp(options.admincommand, "v4")) // verbose level4 && (strcasecmp(options.admincommand, "v5")) // verbose level5 && (strcasecmp(options.admincommand, "verror")) // verbose level1 only for dev internal test && (strcasecmp(options.admincommand, "vwarn")) // verbose level2 && (strcasecmp(options.admincommand, "vinfo")) // verbose level3 && (strcasecmp(options.admincommand, "vproto")) // verbose level4 && (strcasecmp(options.admincommand, "vdebug")) // verbose level5 && (strcasecmp(options.admincommand, "gksv")) // get Kissdx Server Version value && (strcasecmp(options.admincommand, "gksvd")) // get Kissdx Server Version Date value && (strcasecmp(options.admincommand, "gdic")) // get dictionary of admin command supported && (strcasecmp(options.admincommand, "glcus")) // get Launch custom option && (strcasecmp(options.admincommand, "glarg")) // get Launch Argument && (strcasecmp(options.admincommand, "gldt")) // get Launch date time && (strcasecmp(options.admincommand, "gverbose")) // get verbose log level cfg value && (strcasecmp(options.admincommand, "glisthidden")) // get list hidden entry cfg value && (strcasecmp(options.admincommand, "gcal")) // get configautoload cfg value && (strcasecmp(options.admincommand, "gdsn")) // get displaysequencenumber cfg value // todo : configautoload cfa / displayseqnumber // == PCLINK - Player Simulator && (strcasecmp(options.admincommand, "LIST_VIDEO")) && (strcasecmp(options.admincommand, "lvi")) // list video && (strcasecmp(options.admincommand, "lpi")) // list picture && (strcasecmp(options.admincommand, "lau")) // list audio ) { log("Admin command not supported : %s",options.admincommand); usage(argv[0],options.loglevel==LOGDEBUG ? 1 : 0); exit(EX_USAGE); } } // ================================================================================== // === kissdx used as an Admin Command client === // ================================================================================== if (strlen(options.admincommand) > 0) { // Set a minimal config object to let client mode benefit of loglevel() specified in CLO if (loglevel_CLO) { config.loglevel = options.loglevel; } else { config.loglevel = LOGERROR; // minimal loglevel if nothing is specifie in CLO } loglevel(LOGSTD,"kissdx Administration Client with command [%s][%d]", options.admincommand,strlen(options.admincommand)); //logv("%s %s","ADMCMD Command request : ", options.admincommand); //logv("%s %s","ADMCMD Mediacenter name : ", options.mediacenter); //logv("%s %d","ADMCMD Mediacenter tcp.port: ", options.adminserver_port); // ======================================================================================= // == ACTION section // ======================================================================================= if (!strcasecmp(options.admincommand, ADMCMD_STOP) ||!strcasecmp(options.admincommand,"s") ) { loglevel(LOGINFO,"%s %s","ADMCMD STOP Command request : ", options.admincommand); deliver_admcmd_action((char *)ADMCMD_ACTION,(char *)ADMCMD_STOP,(char *)"" ); exit(0);} if (!strcasecmp(options.admincommand, ADMCMD_FORCESTOP) ||!strcasecmp(options.admincommand,"frs") ) { logv("%s %s","ADMCMD FORCESTOP Command request : ", options.admincommand); deliver_admcmd_action((char *)ADMCMD_ACTION,(char *)ADMCMD_FORCESTOP,(char *)"" ); exit(0);} // [RELOAD_CONFIG] admin command if (!strcasecmp(options.admincommand, ADMCMD_RELOADCONFIG) ||!strcasecmp(options.admincommand,"rlc") ) { logv("%s %s","ADMCMD RELOAD Command request : ", options.admincommand); deliver_admcmd_action((char *)ADMCMD_ACTION,(char *)ADMCMD_RELOADCONFIG,(char *)"" ); exit(0);} // [GET_TXT_CONFIG] admin command if (!strcasecmp(options.admincommand, ADMCMD_GETTXTCONFIG) ||!strcasecmp(options.admincommand,"gtc")) { logv("%s %s","ADMCMD GET_TXT_CONFIG Command request : ", options.admincommand); deliver_admcmd_action((char *)ADMCMD_ACTION,(char *)ADMCMD_GETTXTCONFIG,(char *)"" ); exit(0);} // [GET_DICT] admin command if (!strcasecmp(options.admincommand, ADMCMD_GETDICT) ||!strcasecmp(options.admincommand,"gdic")) { logv("%s %s","ADMCMD GET_DICT Command request : ", options.admincommand); deliver_admcmd_action((char *)ADMCMD_ACTION,(char *)ADMCMD_GETDICT,(char *)"" ); exit(0);} // ======================================================================================= // == SET CONFIG OPTIONS section // ======================================================================================= // SET Hidden List Entry NO if (!strcasecmp(options.admincommand,"slisthidden.no")) { logv("%s %s","ADMCMD SET %s %s Command request : ", CFG_LIST_HIDDEN_ENTRY,"NO"); deliver_admcmd_action((char *)ADMCMD_SET,(char *)CFG_LIST_HIDDEN_ENTRY,(char *)"NO" ); exit(0);} // SET Hidden List Entry YES if (!strcasecmp(options.admincommand,"slisthidden.yes")) { logv("%s %s","ADMCMD SET %s %s Command request : ", CFG_LIST_HIDDEN_ENTRY,"YES"); deliver_admcmd_action((char *)ADMCMD_SET,(char *)CFG_LIST_HIDDEN_ENTRY,(char *)"YES" ); exit(0);} // SET Config AutoLoad NO ) if (!strcasecmp(options.admincommand,"scal.no")) { logv("%s %s","ADMCMD SET %s %s Command request : ", CFG_CONFIG_AUTOLOAD,"NO"); deliver_admcmd_action((char *)ADMCMD_SET,(char *)CFG_CONFIG_AUTOLOAD,(char *)"NO" ); exit(0);} // SET Config AutoLoad YES ) if (!strcasecmp(options.admincommand,"scal.yes")) { logv("%s %s","ADMCMD SET %s %s Command request : ", CFG_CONFIG_AUTOLOAD,"YES"); deliver_admcmd_action((char *)ADMCMD_SET,(char *)CFG_CONFIG_AUTOLOAD,(char *)"YES" ); exit(0);} // SET Display Seq Number NO if (!strcasecmp(options.admincommand,"sdsn.no")) { logv("%s %s","ADMCMD SET %s %s Command request : ", CFG_DISPLAY_SEQ_NUMBER,"NO"); deliver_admcmd_action((char *)ADMCMD_SET,(char *)CFG_DISPLAY_SEQ_NUMBER,(char *)"NO" ); exit(0);} // SET Display Seq Number YES if (!strcasecmp(options.admincommand,"sdsn.yes")) { logv("%s %s","ADMCMD SET %s %s Command request : ", CFG_DISPLAY_SEQ_NUMBER,"YES"); deliver_admcmd_action((char *)ADMCMD_SET,(char *)CFG_DISPLAY_SEQ_NUMBER,(char *)"YES" ); exit(0);} // SET Verbose Level : v1 .. v5 or Error .. Debug // send only the value (1..5), then char pointer : options.admincommand +1 if ( !strcasecmp(options.admincommand,"v1") || !strcasecmp(options.admincommand,"v2") || !strcasecmp(options.admincommand,"v3") || !strcasecmp(options.admincommand,"v4") || !strcasecmp(options.admincommand,"v5") || !strcasecmp(options.admincommand,"verror") || !strcasecmp(options.admincommand,"vwarn") || !strcasecmp(options.admincommand,"vinfo") || !strcasecmp(options.admincommand,"vproto") || !strcasecmp(options.admincommand,"vdebug") ) { logv("%s %s","ADMCMD SET %s %s Command request : ", CFG_VERBOSE_LOG_LEVEL,(char *)options.admincommand +1); deliver_admcmd_action((char *)ADMCMD_SET,(char *)CFG_VERBOSE_LOG_LEVEL,(char *)options.admincommand +1 ); exit(0);} // ======================================================================================= // == GET CONFIG OPTIONS section // ======================================================================================= // GET Hidden List Entry (ON/OFF) if (!strcasecmp(options.admincommand,"glisthidden")) { logv("%s %s","ADMCMD GET %s Command request : ", CFG_LIST_HIDDEN_ENTRY); deliver_admcmd_action((char *)ADMCMD_GET,(char *)CFG_LIST_HIDDEN_ENTRY,(char *)"" ); exit(0);} // GET Hidden List Entry (ON/OFF) if (!strcasecmp(options.admincommand,"gverbose")) { logv("%s %s","ADMCMD GET %s Command request : ", CFG_VERBOSE_LOG_LEVEL); deliver_admcmd_action((char *)ADMCMD_GET,(char *)CFG_VERBOSE_LOG_LEVEL,(char *)"" ); exit(0);} // GET Kissdx Server Version if (!strcasecmp(options.admincommand,"gksv")) { logv("%s %s","ADMCMD GET %s Command request : ", CFG_KISSDX_SRV_VERSION); deliver_admcmd_action((char *)ADMCMD_GET,(char *)CFG_KISSDX_SRV_VERSION,(char *)"" ); exit(0);} // GET Kissdx Server Version Date if (!strcasecmp(options.admincommand,"gksvd")) { logv("%s %s","ADMCMD GET %s Command request : ", CFG_KISSDX_SRV_VERSIONDATE); deliver_admcmd_action((char *)ADMCMD_GET,(char *)CFG_KISSDX_SRV_VERSIONDATE,(char *)"" ); exit(0);} // GET Launched Date time if (!strcasecmp(options.admincommand,"gldt")) { logv("%s %s","ADMCMD GET %s Command request : ", CFG_KISSDX_LNCH_DATETIME); deliver_admcmd_action((char *)ADMCMD_GET,(char *)CFG_KISSDX_LNCH_DATETIME,(char *)"" ); exit(0);} // GET Launched Arguments (all argument of initial command line) if (!strcasecmp(options.admincommand,"glarg")) { logv("%s %s","ADMCMD GET %s Command request : ", CFG_KISSDX_LNCH_ARGUMENT); deliver_admcmd_action((char *)ADMCMD_GET,(char *)CFG_KISSDX_LNCH_ARGUMENT,(char *)"" ); exit(0);} // GET Launched Options (restricted to command line Custom options o1..o3) // These option are not used internaly in kissdx, only store and send back on request if (!strcasecmp(options.admincommand,"glcus")) { logv("%s %s","ADMCMD GET %s Command request : ", CFG_KISSDX_LNCH_CUSTOPTION); deliver_admcmd_action((char *)ADMCMD_GET,(char *)CFG_KISSDX_LNCH_CUSTOPTION,(char *)"" ); exit(0);} // GET Config Auto Load if (!strcasecmp(options.admincommand,"gcal")) { logv("%s %s","ADMCMD GET %s Command request : ", CFG_CONFIG_AUTOLOAD); deliver_admcmd_action((char *)ADMCMD_GET,(char *)CFG_CONFIG_AUTOLOAD,(char *)"" ); exit(0);} // GET Display Sequence Number if (!strcasecmp(options.admincommand,"gdsn")) { logv("%s %s","ADMCMD GET %s Command request : ", CFG_DISPLAY_SEQ_NUMBER); deliver_admcmd_action((char *)ADMCMD_GET,(char *)CFG_DISPLAY_SEQ_NUMBER,(char *)"" ); exit(0);} // ======================================================================================= // == PCLINK CALL - PLAYER SIMULATION section // ======================================================================================= // [LIST_VIDEO] This feature is a first step to provide a KiSS player simulator : here is LIST_VIDEO command // Beta test with kissdx PcLink server are ok/ Direct connection to Kiss DP558.PcLink failed if (!strcasecmp(options.admincommand,"LIST_VIDEO") || !strcasecmp(options.admincommand,"lvi") ) { logv("%s %s","ADMCMD LIST_VIDEO Command request : ", options.admincommand); simulplayer((char *)"LIST VIDEO",options.adminparam,"RESPONSE"); exit(0);} if (!strcasecmp(options.admincommand,"lau") ) { logv("%s %s","ADMCMD LIST_AUDIO Command request : ", options.admincommand); simulplayer((char *)"LIST AUDIO",options.adminparam,"RESPONSE"); exit(0);} if (!strcasecmp(options.admincommand,"lpi") ) { logv("%s %s","ADMCMD LIST_PICTURE Command request : ", options.admincommand); simulplayer((char *)"LIST PICTURE",options.adminparam,"RESPONSE"); exit(0);} // Default other choice : Admincommand is in dictionary but not yet developped loglevel(LOGSTD,"Client mode: Admin command not yet supported : %s",options.admincommand); exit(0); } // === kissdx start protocol to be a streaming server ================================================= // First parse config before any operation if (parse_config(&config, NULL)) { // First parsing of configuration could not afford a fatal error log("kissdx is unable to start due to bad configuration%s","."); // In case of daemon mode, a command line error message should help diagnostic if (options.daemonize) { fprintf(stderr, "%s %s", MsgErr_Config_fatalerror, "\n"); fprintf(stderr, "%s %s", MsgErr_Config_daemonmode, "\n"); } exit(1); } if (options.daemonize) { int fd; switch(fork()) { case 0: break; case -1: perror("fork"); exit(1); default: _exit(0); } if (setsid() < 0) { perror("setsid"); } switch(fork()) { case 0: break; case -1: perror("fork 2"); exit(1); default: _exit(0); } chdir("/"); // Avoid tying up access to whatever dir we were run from close(0); close(1); close(2); if ((fd = open("/dev/null", O_RDONLY)) != 0) { dup2(fd, 0); close(fd); } if ((fd = open("/dev/null", O_WRONLY)) != 1) { dup2(fd, 1); close(fd); } if ((fd = open("/dev/null", O_WRONLY)) != 2) { dup2(fd, 2); close(fd); } do_daemon(); } else do_daemon(); exit(0); }