| /* |
| * Copyright (c) 2014 Netflix, Inc. |
| * All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are met: |
| * |
| * Redistributions of source code must retain the above copyright notice, this |
| * list of conditions and the following disclaimer. |
| * Redistributions in binary form must reproduce the above copyright notice, |
| * this list of conditions and the following disclaimer in the documentation |
| * and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY NETFLIX, INC. AND CONTRIBUTORS "AS IS" AND ANY |
| * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
| * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE |
| * DISCLAIMED. IN NO EVENT SHALL NETFLIX OR CONTRIBUTORS BE LIABLE FOR ANY |
| * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES |
| * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; |
| * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND |
| * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
| * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF |
| * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #include <pthread.h> |
| #include <signal.h> |
| #include <stdio.h> |
| #include <ctype.h> |
| #include <stdlib.h> |
| #include <string.h> |
| #include <sys/types.h> |
| #include <sys/wait.h> |
| #include <unistd.h> |
| #include <dirent.h> |
| #include <regex.h> |
| |
| #include "dial_server.h" |
| #include "dial_options.h" |
| #include <signal.h> |
| |
| #define BUFSIZE 256 |
| |
| static char *spDefaultFriendlyName = "Google Fiber TV Box"; |
| static char *spDefaultModelName = "GFHD100"; |
| static char *spDefaultUuid = "0"; |
| static char spFriendlyName[BUFSIZE]; |
| static char spModelName[BUFSIZE]; |
| static char spUuid[BUFSIZE]; |
| static char spUiType[BUFSIZE]; |
| static int gDialPort; |
| |
| static char *spAppNetflix = "netflix"; // name of the netflix executable |
| static char *defaultLaunchParam = "source_type=12"; |
| |
| static char *spAppYouTube = "browser_shell"; |
| static char *spAppYouTubeMatch = "www.youtube.com/tv"; |
| |
| static char *spAppFiberTV = "miniclient"; |
| static char *spAppFiberTVMatch = NULL; |
| |
| // Adding 20 bytes for prepended source_type for Netflix |
| static char sQueryParam[DIAL_MAX_PAYLOAD+20]; |
| |
| static const char *spIpAddress; |
| |
| static int doesMatch( char* pzExp, char* pzStr) |
| { |
| regex_t exp; |
| int ret; |
| int match = 0; |
| if ((ret = regcomp( &exp, pzExp, REG_EXTENDED ))) { |
| char errbuf[1024] = {0,}; |
| regerror(ret, &exp, errbuf, sizeof(errbuf)); |
| fprintf( stderr, "regexp error: %s", errbuf ); |
| } else { |
| regmatch_t matches[1]; |
| if( regexec( &exp, pzStr, 1, matches, 0 ) == 0 ) { |
| match = 1; |
| } |
| } |
| regfree(&exp); |
| return match; |
| } |
| |
| // For Fiber we do not let DIAL handle SIGTERM, as we consider that a killing |
| // signal (pkillwait uses it first). |
| /* |
| void signalHandler(int signal) |
| { |
| switch(signal) |
| { |
| case SIGTERM: |
| // just ignore this, we don't want to die |
| break; |
| } |
| } |
| */ |
| |
| /* The URL encoding source code was obtained here: |
| * http://www.geekhideout.com/urlcode.shtml |
| */ |
| |
| /* Converts a hex character to its integer value */ |
| char from_hex(char ch) { |
| return isdigit(ch) ? ch - '0' : tolower(ch) - 'a' + 10; |
| } |
| |
| /* Converts an integer value to its hex character*/ |
| char to_hex(char code) { |
| static char hex[] = "0123456789abcdef"; |
| return hex[code & 15]; |
| } |
| |
| /* Returns a url-encoded version of str */ |
| /* IMPORTANT: be sure to free() the returned string after use */ |
| char *url_encode(const char *str) { |
| const char *pstr; |
| char *buf, *pbuf; |
| pstr = str; |
| buf = malloc(strlen(str) * 3 + 1); |
| pbuf = buf; |
| if( buf ) |
| { |
| while (*pstr) { |
| if (isalnum(*pstr) || *pstr == '-' || *pstr == '_' || *pstr == '.' || *pstr == '~') |
| *pbuf++ = *pstr; |
| else if (*pstr == ' ') |
| *pbuf++ = '+'; |
| else |
| *pbuf++ = '%', *pbuf++ = to_hex(*pstr >> 4), *pbuf++ = to_hex(*pstr & 15); |
| pstr++; |
| } |
| *pbuf = '\0'; |
| } |
| return buf; |
| } |
| |
| /* |
| * End of URL ENCODE source |
| */ |
| |
| /* |
| * This function will walk /proc and look for the application in |
| * /proc/<PID>/comm. and /proc/<PID>/cmdline to find it's command (executable |
| * name) and command line (if needed). |
| * Implementors can override this function with an equivalent. |
| */ |
| static int isAppRunning( char *pzName, char *pzCommandPattern ) { |
| DIR* proc_fd = opendir("/proc"); |
| if( proc_fd != NULL ) { |
| struct dirent* procEntry; |
| while((procEntry=readdir(proc_fd)) != NULL) { |
| if( doesMatch( "^[0-9][0-9]*$", procEntry->d_name ) ) { |
| char exePath[64] = {0,}; |
| char link[256] = {0,}; |
| char cmdlinePath[64] = {0,}; |
| char buffer[1024] = {0,}; |
| int len; |
| sprintf( exePath, "/proc/%s/exe", procEntry->d_name); |
| sprintf( cmdlinePath, "/proc/%s/cmdline", procEntry->d_name); |
| |
| if( (len = readlink( exePath, link, sizeof(link)-1)) != -1 ) { |
| char executable[256] = {0,}; |
| strcat( executable, pzName ); |
| strcat( executable, "$" ); |
| // TODO: Make this search for EOL to prevent false positivies |
| if( !doesMatch( executable, link ) ) { |
| continue; |
| } |
| // else //fall through, we found it |
| } |
| else continue; |
| |
| if (pzCommandPattern != NULL) { |
| FILE *cmdline = fopen(cmdlinePath, "r"); |
| if (!cmdline) { |
| continue; |
| } |
| if (fgets(buffer, 1024, cmdline) == NULL) { |
| fclose(cmdline); |
| continue; |
| } |
| fclose(cmdline); |
| |
| if (!doesMatch( pzCommandPattern, buffer )) { |
| continue; |
| } |
| } |
| |
| closedir(proc_fd); |
| return atoi(procEntry->d_name); |
| } |
| } |
| |
| closedir(proc_fd); |
| } else { |
| fprintf(stderr, "/proc failed to open\n"); |
| } |
| return 0; |
| } |
| |
| /* Running an application is done through the runminiclient script */ |
| static pid_t runApplication( const char * const args[], DIAL_run_t *run_id ) { |
| const char * const script_args[] = {"/etc/init.d/S99miniclient", "restart", 0}; |
| |
| /* Write application information to /tmp/runapp */ |
| FILE *runapp = fopen("/tmp/runapp.tmp","w"); |
| if (!runapp) { |
| fprintf(stderr, "Couldn't open /tmp/runapp.tmp file\n"); |
| return kDIALStatusStopped; |
| } |
| for(int i = 0; args[i]; ++i) { |
| int outputCharacters = fprintf(runapp, "%s ", args[i]); |
| if (outputCharacters<0) { |
| fprintf(stderr, "Error writing to /tmp/runapp.tmp file\n"); |
| fclose(runapp); |
| return kDIALStatusStopped; |
| } |
| } |
| fsync(fileno(runapp)); |
| fclose(runapp); |
| runapp=NULL; |
| int appclientUid = 201; |
| int videoGid = 200; |
| if (chown("/tmp/runapp.tmp", appclientUid, videoGid)) { |
| fprintf(stderr, "Error chowning /tmp/runapp.tmp file. App launch may fail."); |
| } |
| if (rename("/tmp/runapp.tmp", "/tmp/runapp")) { |
| fprintf(stderr, "Error renaming /tmp/runapp.tmp file\n"); |
| return kDIALStatusStopped; |
| } |
| |
| |
| pid_t pid = fork(); |
| if (pid != -1) { |
| if (!pid) { // child |
| // Close all descriptors except stdin,stdout,stderr |
| int fd, maxfd; |
| maxfd = sysconf(_SC_OPEN_MAX); |
| for (fd=3;fd<maxfd;fd++) { |
| close(fd); |
| } |
| execv(*script_args, (char * const *) script_args); |
| // It won't reach this unless there was an error |
| fprintf(stderr, "Error executing %s\n", *script_args); |
| exit(1); |
| } else { |
| *run_id = (void *)(long)pid; // parent PID |
| // Wait until the script S99miniclient is done before continuing |
| waitpid(pid, NULL, 0); |
| // TODO(jfthibert) Should we try to wait a few seconds until the actual |
| // program is started? |
| } |
| return kDIALStatusRunning; |
| } else { |
| return kDIALStatusStopped; |
| } |
| } |
| |
| |
| /* Compare the applications last launch parameters with the new parameters. |
| * If they match, return false |
| * If they don't match, return true |
| */ |
| static int shouldRelaunch( |
| DIALServer *pServer, |
| const char *pAppName, |
| const char *args ) |
| { |
| return ( strncmp( DIAL_get_payload(pServer, pAppName), args, DIAL_MAX_PAYLOAD ) != 0 ); |
| } |
| |
| static DIALStatus youtube_start(DIALServer *ds, const char *appname, |
| const char *args, size_t arglen, |
| DIAL_run_t *run_id, void *callback_data) { |
| fprintf(stderr, "** LAUNCH YouTube ** with args %s\n\n", args); |
| |
| char url[512] = {0,}; |
| int urlLength = snprintf( url, sizeof(url), "https://www.youtube.com/tv?%s", args); |
| if (urlLength>=sizeof(url)) { |
| fprintf(stderr, "Warning, YouTube URL was truncated (%d>=%d)\n", urlLength, sizeof(url)); |
| } |
| |
| const char * const youtube_args[] = { "youtube", |
| url, NULL |
| }; |
| runApplication( youtube_args, run_id ); |
| |
| return kDIALStatusRunning; |
| } |
| |
| static DIALStatus youtube_status(DIALServer *ds, const char *appname, |
| DIAL_run_t run_id, int *pCanStop, void *callback_data) { |
| // YouTube can stop |
| *pCanStop = 1; |
| return isAppRunning( spAppYouTube, spAppYouTubeMatch ) ? kDIALStatusRunning : kDIALStatusStopped; |
| } |
| |
| static void youtube_stop(DIALServer *ds, const char *appname, DIAL_run_t run_id, |
| void *callback_data) { |
| fprintf(stderr, "** KILL YouTube **\n"); |
| pid_t pid; |
| if ((pid = isAppRunning( spAppYouTube, spAppYouTubeMatch ))) { |
| kill(pid, SIGTERM); |
| } |
| } |
| |
| static DIALStatus netflix_start(DIALServer *ds, const char *appname, |
| const char *args, size_t arglen, |
| DIAL_run_t *run_id, void *callback_data) { |
| int shouldRelaunchApp = 0; |
| int payloadLen = 0; |
| int appPid = 0; |
| |
| // only launch Netflix if it isn't running |
| appPid = isAppRunning( spAppNetflix, NULL ); |
| shouldRelaunchApp = shouldRelaunch( ds, appname, args ); |
| |
| // construct the payload to determine if it has changed from the previous launch |
| payloadLen = strlen(args); |
| memset( sQueryParam, 0, DIAL_MAX_PAYLOAD ); |
| strcat( sQueryParam, defaultLaunchParam ); |
| if( payloadLen ) |
| { |
| char * pUrlEncodedParams; |
| pUrlEncodedParams = url_encode( args ); |
| if( pUrlEncodedParams ) |
| { |
| strcat( sQueryParam, "&dial="); |
| strcat( sQueryParam, pUrlEncodedParams ); |
| free( pUrlEncodedParams ); |
| } |
| } |
| |
| fprintf(stderr, "appPid = %s, shouldRelaunch = %s queryParams = %s\n", |
| appPid?"TRUE":"FALSE", |
| shouldRelaunchApp?"TRUE":"FALSE", |
| sQueryParam ); |
| |
| // if its not running, launch it. The Netflix application should |
| // never be relaunched |
| if( !appPid ) |
| { |
| const char * const netflix_args[] = {"netflix", "-Q", sQueryParam, 0}; |
| return runApplication( netflix_args, run_id ); |
| } |
| else return kDIALStatusRunning; |
| } |
| |
| static DIALStatus netflix_status(DIALServer *ds, const char *appname, |
| DIAL_run_t run_id, int* pCanStop, void *callback_data) { |
| // Netflix application can't stop |
| *pCanStop = 0; |
| |
| return isAppRunning( spAppNetflix, NULL ) ? kDIALStatusRunning : kDIALStatusStopped; |
| } |
| |
| static void netflix_stop(DIALServer *ds, const char *appname, DIAL_run_t run_id, |
| void *callback_data) { |
| int pid; |
| pid = isAppRunning( spAppNetflix, NULL ); |
| if( pid ) |
| { |
| fprintf(stderr, "Killing pid %d\n", pid); |
| kill((pid_t)pid, SIGTERM); |
| } |
| } |
| |
| static DIALStatus fibertv_start(DIALServer *ds, const char *appname, |
| const char *args, size_t arglen, |
| DIAL_run_t *run_id, void *callback_data) { |
| fprintf(stderr, "** LAUNCH FiberTV **\n"); |
| |
| const char * const miniclient_args[] = { "miniclient", NULL }; |
| runApplication( miniclient_args, run_id ); |
| |
| return kDIALStatusRunning; |
| } |
| |
| static DIALStatus fibertv_status(DIALServer *ds, const char *appname, |
| DIAL_run_t run_id, int *pCanStop, void *callback_data) { |
| // FiberTV can stop |
| *pCanStop = 1; |
| return isAppRunning( spAppFiberTV, spAppFiberTVMatch ) ? kDIALStatusRunning : kDIALStatusStopped; |
| } |
| |
| static void fibertv_stop(DIALServer *ds, const char *appname, DIAL_run_t run_id, |
| void *callback_data) { |
| fprintf(stderr, "** KILL FiberTV **\n"); |
| pid_t pid; |
| if ((pid = isAppRunning( spAppFiberTV, spAppFiberTVMatch ))) { |
| kill(pid, SIGTERM); |
| } |
| } |
| |
| static DIALStatus basil_start(DIALServer *ds, const char *appname, |
| const char *args, size_t arglen, |
| DIAL_run_t *run_id, void *callback_data) { |
| fprintf(stderr, "** LAUNCH GoogleFiberTV **\n"); |
| return kDIALStatusRunning; // Basil is always running |
| } |
| |
| static DIALStatus basil_status(DIALServer *ds, const char *appname, |
| DIAL_run_t run_id, int *pCanStop, void *callback_data) { |
| *pCanStop = 0; |
| return kDIALStatusRunning; // Basil is always running |
| } |
| |
| static struct CastServiceData basil_service_data(DIALServer *ds, const char *appname, |
| DIAL_run_t run_id, void *callback_data) { |
| struct CastServiceData serviceData; |
| serviceData.connection_svc_host = spIpAddress; |
| serviceData.connection_svc_port = 5153; |
| serviceData.connection_svc_path = "/connections"; |
| serviceData.protocol = "marjoram"; |
| return serviceData; |
| } |
| |
| static void basil_stop(DIALServer *ds, const char *appname, DIAL_run_t run_id, |
| void *callback_data) { |
| fprintf(stderr, "** KILL GoogleFiberTV (ignored) **\n"); |
| // Basil cannot be killed, but log the attempt |
| } |
| |
| void run_ssdp(int port, const char *pFriendlyName, const char * pModelName, const char *pUuid, const char **ppIpAddress); |
| |
| static void printUsage() |
| { |
| int i, numberOfOptions = sizeof(gDialOptions) / sizeof(dial_options_t); |
| fprintf(stderr, "usage: dialserver <options>\n"); |
| fprintf(stderr, "options:\n"); |
| for( i = 0; i < numberOfOptions; i++ ) |
| { |
| fprintf(stderr, " %s|%s [value]: %s\n", |
| gDialOptions[i].pOption, |
| gDialOptions[i].pLongOption, |
| gDialOptions[i].pOptionDescription ); |
| } |
| } |
| |
| static void setValue( char * pSource, char dest[] ) |
| { |
| // Destination is always one of our static buffers with size BUFSIZE |
| memset( dest, 0, BUFSIZE ); |
| strncpy( dest, pSource, BUFSIZE-1 ); |
| } |
| |
| void runDial(void) |
| { |
| DIALServer *ds; |
| ds = DIAL_create(); |
| struct DIALAppCallbacks cb_nf = {netflix_start, netflix_stop, netflix_status, NULL, NULL}; |
| struct DIALAppCallbacks cb_yt = {youtube_start, youtube_stop, youtube_status, NULL, NULL}; |
| struct DIALAppCallbacks cb_ft = {fibertv_start, fibertv_stop, fibertv_status, NULL, NULL}; |
| struct DIALAppCallbacks cb_gf = {basil_start, basil_stop, basil_status, basil_service_data, NULL}; |
| |
| DIAL_register_app(ds, "Netflix", &cb_nf, NULL, 1, ".netflix.com"); |
| DIAL_register_app(ds, "YouTube", &cb_yt, NULL, 1, ".youtube.com"); |
| if (strcmp(spUiType, "oregano") == 0) { |
| DIAL_register_app(ds, "GoogleFiberTV", &cb_gf, NULL, 0, NULL); |
| } else { |
| DIAL_register_app(ds, "FiberTV", &cb_ft, NULL, 0, NULL); |
| } |
| if (DIAL_start(ds)) { |
| fprintf(stderr, "DIAL_start failed, exiting.\n"); |
| exit(1); |
| } |
| |
| gDialPort = DIAL_get_port(ds); |
| fprintf(stderr, "launcher listening on gDialPort %d\n", gDialPort); |
| run_ssdp(gDialPort, spFriendlyName, spModelName, spUuid, &spIpAddress); |
| |
| DIAL_stop(ds); |
| free(ds); |
| } |
| |
| static void processOption( int index, char * pOption ) |
| { |
| switch(index) |
| { |
| case 0: // Friendly name |
| setValue( pOption, spFriendlyName ); |
| break; |
| case 1: // Model Name |
| setValue( pOption, spModelName ); |
| break; |
| case 2: // UUID |
| setValue( pOption, spUuid ); |
| break; |
| case 3: // UI type |
| setValue( pOption, spUiType ); |
| break; |
| default: |
| // Should not get here |
| fprintf( stderr, "Option %d not valid\n", index); |
| exit(1); |
| } |
| } |
| |
| int main(int argc, char* argv[]) |
| { |
| // We want DIAL to die on a SIGTERM - pkillwait sends SIGTERM first. |
| /* |
| struct sigaction action; |
| action.sa_handler = signalHandler; |
| sigemptyset(&action.sa_mask); |
| action.sa_flags = 0; |
| sigaction(SIGTERM, &action, NULL); |
| */ |
| |
| srand(time(NULL)); |
| int i; |
| |
| // Set stdout to unbuffered mode to avoid delayed logs |
| setvbuf(stdout, NULL, _IONBF, 0); |
| |
| i = isAppRunning(spAppNetflix, NULL ); |
| fprintf(stderr, "Netflix is %s\n", i ? "Running":"Not Running"); |
| i = isAppRunning( spAppYouTube, spAppYouTubeMatch ); |
| fprintf(stderr, "YouTube is %s\n", i ? "Running":"Not Running"); |
| i = isAppRunning( spAppFiberTV, spAppFiberTVMatch ); |
| fprintf(stderr, "FiberTV is %s\n", i ? "Running":"Not Running"); |
| |
| // set all defaults |
| setValue(spDefaultFriendlyName, spFriendlyName ); |
| setValue(spDefaultModelName, spModelName ); |
| setValue(spDefaultUuid, spUuid ); |
| |
| // Process command line options |
| // Loop through pairs of command line options. |
| for( i = 1; i < argc; i+=2 ) |
| { |
| int numberOfOptions = sizeof(gDialOptions) / sizeof(dial_options_t); |
| while( --numberOfOptions >= 0 ) |
| { |
| int shortLen, longLen; |
| shortLen = strlen(gDialOptions[numberOfOptions].pOption); |
| longLen = strlen(gDialOptions[numberOfOptions].pLongOption); |
| if( ( ( strncmp( argv[i], gDialOptions[numberOfOptions].pOption, shortLen ) == 0 ) || |
| ( strncmp( argv[i], gDialOptions[numberOfOptions].pLongOption, longLen ) == 0 ) ) && |
| ( (i+1) < argc ) ) |
| { |
| processOption( numberOfOptions, argv[i+1] ); |
| break; |
| } |
| } |
| // if we don't find an option in our list, bail out. |
| if( numberOfOptions < 0 ) |
| { |
| printUsage(); |
| exit(1); |
| } |
| } |
| runDial(); |
| |
| return 0; |
| } |
| |