blob: 33e83dcf693b1a6f1bccbf11453c8eca997546fa [file] [log] [blame]
/*
* 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;
}