minidlna/minidlna.c
Gleb Smirnoff 5e320f2798 Add monitoring support via kqueue(2). Based on patch from FreeBSD ports, authored by wg@FreeBSD.org and se@FreeBSD.org. However, this version doesn't create any thread, it uses main event dispatcher.
Some effort was made to unify monitoring via kqueue and via inotify
APIs. Now both provide their implementation of add_watch() function.

I guess there are some logical bugs in vnode_process(). With this commit
it would be better provide code as is, and resolve bugs separately.
2018-01-16 17:01:43 -08:00

1382 lines
35 KiB
C

/* MiniDLNA project
*
* http://sourceforge.net/projects/minidlna/
*
* MiniDLNA media server
* Copyright (C) 2008-2012 Justin Maggard
*
* This file is part of MiniDLNA.
*
* MiniDLNA is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2 as
* published by the Free Software Foundation.
*
* MiniDLNA 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 MiniDLNA. If not, see <http://www.gnu.org/licenses/>.
*
* Portions of the code from the MiniUPnP project:
*
* Copyright (c) 2006-2007, Thomas Bernard
* 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.
* * The name of the author may not be used to endorse or promote products
* derived from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 THE COPYRIGHT OWNER 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 <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <ctype.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <sys/file.h>
#include <sys/time.h>
#include <sys/param.h>
#include <sys/stat.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>
#include <time.h>
#include <signal.h>
#include <errno.h>
#include <pthread.h>
#include <limits.h>
#include <libgen.h>
#include <pwd.h>
#include <grp.h>
#include "config.h"
#ifdef ENABLE_NLS
#include <locale.h>
#include <libintl.h>
#endif
#include "event.h"
#include "upnpglobalvars.h"
#include "sql.h"
#include "upnphttp.h"
#include "upnpdescgen.h"
#include "minidlnapath.h"
#include "getifaddr.h"
#include "upnpsoap.h"
#include "options.h"
#include "utils.h"
#include "minissdp.h"
#include "minidlnatypes.h"
#include "process.h"
#include "upnpevents.h"
#include "scanner.h"
#include "monitor.h"
#include "log.h"
#include "tivo_beacon.h"
#include "tivo_utils.h"
#include "avahi.h"
#if SQLITE_VERSION_NUMBER < 3005001
# warning "Your SQLite3 library appears to be too old! Please use 3.5.1 or newer."
# define sqlite3_threadsafe() 0
#endif
static LIST_HEAD(httplisthead, upnphttp) upnphttphead;
/* OpenAndConfHTTPSocket() :
* setup the socket used to handle incoming HTTP connections. */
static int
OpenAndConfHTTPSocket(unsigned short port)
{
int s;
int i = 1;
struct sockaddr_in listenname;
/* Initialize client type cache */
memset(&clients, 0, sizeof(struct client_cache_s));
s = socket(PF_INET, SOCK_STREAM, 0);
if (s < 0)
{
DPRINTF(E_ERROR, L_GENERAL, "socket(http): %s\n", strerror(errno));
return -1;
}
if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &i, sizeof(i)) < 0)
DPRINTF(E_WARN, L_GENERAL, "setsockopt(http, SO_REUSEADDR): %s\n", strerror(errno));
memset(&listenname, 0, sizeof(struct sockaddr_in));
listenname.sin_family = AF_INET;
listenname.sin_port = htons(port);
listenname.sin_addr.s_addr = htonl(INADDR_ANY);
if (bind(s, (struct sockaddr *)&listenname, sizeof(struct sockaddr_in)) < 0)
{
DPRINTF(E_ERROR, L_GENERAL, "bind(http): %s\n", strerror(errno));
close(s);
return -1;
}
if (listen(s, 16) < 0)
{
DPRINTF(E_ERROR, L_GENERAL, "listen(http): %s\n", strerror(errno));
close(s);
return -1;
}
return s;
}
/* ProcessListen() :
* accept incoming HTTP connection. */
static void
ProcessListen(struct event *ev)
{
int shttp;
socklen_t clientnamelen;
struct sockaddr_in clientname;
clientnamelen = sizeof(struct sockaddr_in);
shttp = accept(ev->fd, (struct sockaddr *)&clientname, &clientnamelen);
if (shttp<0)
{
DPRINTF(E_ERROR, L_GENERAL, "accept(http): %s\n", strerror(errno));
}
else
{
struct upnphttp * tmp = 0;
DPRINTF(E_DEBUG, L_GENERAL, "HTTP connection from %s:%d\n",
inet_ntoa(clientname.sin_addr),
ntohs(clientname.sin_port) );
/*if (fcntl(shttp, F_SETFL, O_NONBLOCK) < 0) {
DPRINTF(E_ERROR, L_GENERAL, "fcntl F_SETFL, O_NONBLOCK\n");
}*/
/* Create a new upnphttp object and add it to
* the active upnphttp object list */
tmp = New_upnphttp(shttp);
if (tmp)
{
tmp->clientaddr = clientname.sin_addr;
LIST_INSERT_HEAD(&upnphttphead, tmp, entries);
}
else
{
DPRINTF(E_ERROR, L_GENERAL, "New_upnphttp() failed\n");
close(shttp);
}
}
}
/* Handler for the SIGTERM signal (kill)
* SIGINT is also handled */
static void
sigterm(int sig)
{
signal(sig, SIG_IGN); /* Ignore this signal while we are quitting */
DPRINTF(E_WARN, L_GENERAL, "received signal %d, good-bye\n", sig);
quitting = 1;
}
static void
sigusr1(int sig)
{
signal(sig, sigusr1);
DPRINTF(E_WARN, L_GENERAL, "received signal %d, clear cache\n", sig);
memset(&clients, '\0', sizeof(clients));
}
static void
sighup(int sig)
{
signal(sig, sighup);
DPRINTF(E_WARN, L_GENERAL, "received signal %d, re-read\n", sig);
reload_ifaces(1);
}
/* record the startup time */
static void
set_startup_time(void)
{
startup_time = time(NULL);
}
static void
getfriendlyname(char *buf, int len)
{
char *p = NULL;
char hn[256];
int off;
if (gethostname(hn, sizeof(hn)) == 0)
{
strncpyt(buf, hn, len);
p = strchr(buf, '.');
if (p)
*p = '\0';
}
else
strcpy(buf, "Unknown");
off = strlen(buf);
off += snprintf(buf+off, len-off, ": ");
#ifdef READYNAS
FILE *info;
char ibuf[64], *key, *val;
snprintf(buf+off, len-off, "ReadyNAS");
info = fopen("/proc/sys/dev/boot/info", "r");
if (!info)
return;
while ((val = fgets(ibuf, 64, info)) != NULL)
{
key = strsep(&val, ": \t");
val = trim(val);
if (strcmp(key, "model") == 0)
{
snprintf(buf+off, len-off, "%s", val);
key = strchr(val, ' ');
if (key)
{
strncpyt(modelnumber, key+1, MODELNUMBER_MAX_LEN);
*key = '\0';
}
snprintf(modelname, MODELNAME_MAX_LEN,
"Windows Media Connect compatible (%s)", val);
}
else if (strcmp(key, "serial") == 0)
{
strncpyt(serialnumber, val, SERIALNUMBER_MAX_LEN);
if (serialnumber[0] == '\0')
{
char mac_str[13];
if (getsyshwaddr(mac_str, sizeof(mac_str)) == 0)
strcpy(serialnumber, mac_str);
else
strcpy(serialnumber, "0");
}
break;
}
}
fclose(info);
#else
char * logname;
logname = getenv("LOGNAME");
#ifndef STATIC // Disable for static linking
if (!logname)
{
struct passwd * pwent;
pwent = getpwuid(getuid());
if (pwent)
logname = pwent->pw_name;
}
#endif
snprintf(buf+off, len-off, "%s", logname?logname:"Unknown");
#endif
}
static time_t
_get_dbtime(void)
{
char path[PATH_MAX];
struct stat st;
snprintf(path, sizeof(path), "%s/files.db", db_path);
if (stat(path, &st) != 0)
return 0;
return st.st_mtime;
}
static int
open_db(sqlite3 **sq3)
{
char path[PATH_MAX];
int new_db = 0;
snprintf(path, sizeof(path), "%s/files.db", db_path);
if (access(path, F_OK) != 0)
{
new_db = 1;
make_dir(db_path, S_ISVTX|S_IRWXU|S_IRWXG|S_IRWXO);
}
if (sqlite3_open(path, &db) != SQLITE_OK)
DPRINTF(E_FATAL, L_GENERAL, "ERROR: Failed to open sqlite database! Exiting...\n");
if (sq3)
*sq3 = db;
sqlite3_busy_timeout(db, 5000);
sql_exec(db, "pragma page_size = 4096");
sql_exec(db, "pragma journal_mode = OFF");
sql_exec(db, "pragma synchronous = OFF;");
sql_exec(db, "pragma default_cache_size = 8192;");
return new_db;
}
static void
check_db(sqlite3 *db, int new_db, pid_t *scanner_pid)
{
struct media_dir_s *media_path = NULL;
char cmd[PATH_MAX*2];
char **result;
int i, rows = 0;
int ret;
if (!new_db)
{
/* Check if any new media dirs appeared */
media_path = media_dirs;
while (media_path)
{
ret = sql_get_int_field(db, "SELECT TIMESTAMP as TYPE from DETAILS where PATH = %Q",
media_path->path);
if (ret != media_path->types)
{
ret = 1;
goto rescan;
}
media_path = media_path->next;
}
/* Check if any media dirs disappeared */
sql_get_table(db, "SELECT VALUE from SETTINGS where KEY = 'media_dir'", &result, &rows, NULL);
for (i=1; i <= rows; i++)
{
media_path = media_dirs;
while (media_path)
{
if (strcmp(result[i], media_path->path) == 0)
break;
media_path = media_path->next;
}
if (!media_path)
{
ret = 2;
sqlite3_free_table(result);
goto rescan;
}
}
sqlite3_free_table(result);
}
ret = db_upgrade(db);
if (ret != 0)
{
rescan:
CLEARFLAG(RESCAN_MASK);
if (ret < 0)
DPRINTF(E_WARN, L_GENERAL, "Creating new database at %s/files.db\n", db_path);
else if (ret == 1)
DPRINTF(E_WARN, L_GENERAL, "New media_dir detected; rebuilding...\n");
else if (ret == 2)
DPRINTF(E_WARN, L_GENERAL, "Removed media_dir detected; rebuilding...\n");
else
DPRINTF(E_WARN, L_GENERAL, "Database version mismatch (%d => %d); need to recreate...\n",
ret, DB_VERSION);
sqlite3_close(db);
snprintf(cmd, sizeof(cmd), "rm -rf %s/files.db %s/art_cache", db_path, db_path);
if (system(cmd) != 0)
DPRINTF(E_FATAL, L_GENERAL, "Failed to clean old file cache! Exiting...\n");
open_db(&db);
if (CreateDatabase() != 0)
DPRINTF(E_FATAL, L_GENERAL, "ERROR: Failed to create sqlite database! Exiting...\n");
}
if (ret || GETFLAG(RESCAN_MASK))
{
#if USE_FORK
sqlite3_close(db);
*scanner_pid = fork();
open_db(&db);
if (*scanner_pid == 0) /* child (scanner) process */
{
start_scanner();
sqlite3_close(db);
log_close();
freeoptions();
free(children);
exit(EXIT_SUCCESS);
}
else if (*scanner_pid < 0)
{
start_scanner();
}
else
SETFLAG(SCANNING_MASK);
#else
start_scanner();
#endif
}
}
static int
writepidfile(const char *fname, int pid, uid_t uid)
{
FILE *pidfile;
struct stat st;
char path[PATH_MAX], *dir;
int ret = 0;
if(!fname || *fname == '\0')
return -1;
/* Create parent directory if it doesn't already exist */
strncpyt(path, fname, sizeof(path));
dir = dirname(path);
if (stat(dir, &st) == 0)
{
if (!S_ISDIR(st.st_mode))
{
DPRINTF(E_ERROR, L_GENERAL, "Pidfile path is not a directory: %s\n",
fname);
return -1;
}
}
else
{
if (make_dir(dir, S_IRWXU|S_IRGRP|S_IXGRP|S_IROTH|S_IXOTH) != 0)
{
DPRINTF(E_ERROR, L_GENERAL, "Unable to create pidfile directory: %s\n",
fname);
return -1;
}
if (uid > 0)
{
if (chown(dir, uid, -1) != 0)
DPRINTF(E_WARN, L_GENERAL, "Unable to change pidfile %s ownership: %s\n",
dir, strerror(errno));
}
}
pidfile = fopen(fname, "w");
if (!pidfile)
{
DPRINTF(E_ERROR, L_GENERAL, "Unable to open pidfile for writing %s: %s\n",
fname, strerror(errno));
return -1;
}
if (fprintf(pidfile, "%d\n", pid) <= 0)
{
DPRINTF(E_ERROR, L_GENERAL,
"Unable to write to pidfile %s: %s\n", fname, strerror(errno));
ret = -1;
}
if (uid > 0)
{
if (fchown(fileno(pidfile), uid, -1) != 0)
DPRINTF(E_WARN, L_GENERAL, "Unable to change pidfile %s ownership: %s\n",
fname, strerror(errno));
}
fclose(pidfile);
return ret;
}
static int strtobool(const char *str)
{
return ((strcasecmp(str, "yes") == 0) ||
(strcasecmp(str, "true") == 0) ||
(atoi(str) == 1));
}
static void init_nls(void)
{
#ifdef ENABLE_NLS
const char *messages, *ctype, *locale_dir;
ctype = setlocale(LC_CTYPE, "");
if (!ctype || !strcmp(ctype, "C"))
ctype = setlocale(LC_CTYPE, "en_US.utf8");
if (!ctype)
DPRINTF(E_WARN, L_GENERAL, "Unset locale\n");
else if (!strstr(ctype, "utf8") && !strstr(ctype, "UTF8") &&
!strstr(ctype, "utf-8") && !strstr(ctype, "UTF-8"))
DPRINTF(E_WARN, L_GENERAL, "Using unsupported non-utf8 locale '%s'\n", ctype);
messages = setlocale(LC_MESSAGES, "");
if (!messages)
messages = "unset";
locale_dir = bindtextdomain("minidlna", getenv("TEXTDOMAINDIR"));
DPRINTF(E_DEBUG, L_GENERAL, "Using locale dir '%s' and locale langauge %s/%s\n", locale_dir, messages, ctype);
textdomain("minidlna");
#endif
}
/* init phase :
* 1) read configuration file
* 2) read command line arguments
* 3) daemonize
* 4) check and write pid file
* 5) set startup time stamp
* 6) compute presentation URL
* 7) set signal handlers */
static int
init(int argc, char **argv)
{
int i;
int pid;
int debug_flag = 0;
int verbose_flag = 0;
int options_flag = 0;
struct sigaction sa;
const char * presurl = NULL;
const char * optionsfile = "/etc/minidlna.conf";
char mac_str[13];
char *string, *word;
char *path;
char buf[PATH_MAX];
char log_str[75] = "general,artwork,database,inotify,scanner,metadata,http,ssdp,tivo=warn";
char *log_level = NULL;
struct media_dir_s *media_dir;
int ifaces = 0;
media_types types;
uid_t uid = 0;
gid_t gid = 0;
int error;
/* first check if "-f" option is used */
for (i=2; i<argc; i++)
{
if (strcmp(argv[i-1], "-f") == 0)
{
optionsfile = argv[i];
options_flag = 1;
break;
}
}
/* set up uuid based on mac address */
if (getsyshwaddr(mac_str, sizeof(mac_str)) < 0)
{
DPRINTF(E_OFF, L_GENERAL, "No MAC address found. Falling back to generic UUID.\n");
strcpy(mac_str, "554e4b4e4f57");
}
strcpy(uuidvalue+5, "4d696e69-444c-164e-9d41-");
strncat(uuidvalue, mac_str, 12);
getfriendlyname(friendly_name, FRIENDLYNAME_MAX_LEN);
runtime_vars.port = 8200;
runtime_vars.notify_interval = 895; /* seconds between SSDP announces */
runtime_vars.max_connections = 50;
runtime_vars.root_container = NULL;
runtime_vars.ifaces[0] = NULL;
/* read options file first since
* command line arguments have final say */
if (readoptionsfile(optionsfile) < 0)
{
/* only error if file exists or using -f */
if(access(optionsfile, F_OK) == 0 || options_flag)
DPRINTF(E_FATAL, L_GENERAL, "Error reading configuration file %s\n", optionsfile);
}
for (i=0; i<num_options; i++)
{
switch (ary_options[i].id)
{
case UPNPIFNAME:
for (string = ary_options[i].value; (word = strtok(string, ",")); string = NULL)
{
if (ifaces >= MAX_LAN_ADDR)
{
DPRINTF(E_ERROR, L_GENERAL, "Too many interfaces (max: %d), ignoring %s\n",
MAX_LAN_ADDR, word);
break;
}
while (isspace(*word))
word++;
runtime_vars.ifaces[ifaces++] = word;
}
break;
case UPNPPORT:
runtime_vars.port = atoi(ary_options[i].value);
break;
case UPNPPRESENTATIONURL:
presurl = ary_options[i].value;
break;
case UPNPNOTIFY_INTERVAL:
runtime_vars.notify_interval = atoi(ary_options[i].value);
break;
case UPNPSERIAL:
strncpyt(serialnumber, ary_options[i].value, SERIALNUMBER_MAX_LEN);
break;
case UPNPMODEL_NAME:
strncpyt(modelname, ary_options[i].value, MODELNAME_MAX_LEN);
break;
case UPNPMODEL_NUMBER:
strncpyt(modelnumber, ary_options[i].value, MODELNUMBER_MAX_LEN);
break;
case UPNPFRIENDLYNAME:
strncpyt(friendly_name, ary_options[i].value, FRIENDLYNAME_MAX_LEN);
break;
case UPNPMEDIADIR:
types = ALL_MEDIA;
path = ary_options[i].value;
word = strchr(path, ',');
if (word && (access(path, F_OK) != 0))
{
types = 0;
while (*path)
{
if (*path == ',')
{
path++;
break;
}
else if (*path == 'A' || *path == 'a')
types |= TYPE_AUDIO;
else if (*path == 'V' || *path == 'v')
types |= TYPE_VIDEO;
else if (*path == 'P' || *path == 'p')
types |= TYPE_IMAGE;
else
DPRINTF(E_FATAL, L_GENERAL, "Media directory entry not understood [%s]\n",
ary_options[i].value);
path++;
}
}
path = realpath(path, buf);
if (!path || access(path, F_OK) != 0)
{
DPRINTF(E_ERROR, L_GENERAL, "Media directory \"%s\" not accessible [%s]\n",
ary_options[i].value, strerror(errno));
break;
}
media_dir = calloc(1, sizeof(struct media_dir_s));
media_dir->path = strdup(path);
media_dir->types = types;
if (media_dirs)
{
struct media_dir_s *all_dirs = media_dirs;
while( all_dirs->next )
all_dirs = all_dirs->next;
all_dirs->next = media_dir;
}
else
media_dirs = media_dir;
break;
case UPNPALBUMART_NAMES:
for (string = ary_options[i].value; (word = strtok(string, "/")); string = NULL)
{
struct album_art_name_s * this_name = calloc(1, sizeof(struct album_art_name_s));
int len = strlen(word);
if (word[len-1] == '*')
{
word[len-1] = '\0';
this_name->wildcard = 1;
}
this_name->name = strdup(word);
if (album_art_names)
{
struct album_art_name_s * all_names = album_art_names;
while( all_names->next )
all_names = all_names->next;
all_names->next = this_name;
}
else
album_art_names = this_name;
}
break;
case UPNPDBDIR:
path = realpath(ary_options[i].value, buf);
if (!path)
path = (ary_options[i].value);
make_dir(path, S_ISVTX|S_IRWXU|S_IRWXG|S_IRWXO);
if (access(path, F_OK) != 0)
DPRINTF(E_FATAL, L_GENERAL, "Database path not accessible! [%s]\n", path);
strncpyt(db_path, path, PATH_MAX);
break;
case UPNPLOGDIR:
path = realpath(ary_options[i].value, buf);
if (!path)
path = (ary_options[i].value);
make_dir(path, S_ISVTX|S_IRWXU|S_IRWXG|S_IRWXO);
if (access(path, F_OK) != 0)
DPRINTF(E_FATAL, L_GENERAL, "Log path not accessible! [%s]\n", path);
strncpyt(log_path, path, PATH_MAX);
break;
case UPNPLOGLEVEL:
log_level = ary_options[i].value;
break;
case UPNPINOTIFY:
if (!strtobool(ary_options[i].value))
CLEARFLAG(INOTIFY_MASK);
break;
case ENABLE_TIVO:
if (strtobool(ary_options[i].value))
SETFLAG(TIVO_MASK);
break;
case ENABLE_DLNA_STRICT:
if (strtobool(ary_options[i].value))
SETFLAG(DLNA_STRICT_MASK);
break;
case ROOT_CONTAINER:
switch (ary_options[i].value[0]) {
case '.':
runtime_vars.root_container = NULL;
break;
case 'B':
case 'b':
runtime_vars.root_container = BROWSEDIR_ID;
break;
case 'M':
case 'm':
runtime_vars.root_container = MUSIC_ID;
break;
case 'V':
case 'v':
runtime_vars.root_container = VIDEO_ID;
break;
case 'P':
case 'p':
runtime_vars.root_container = IMAGE_ID;
break;
default:
runtime_vars.root_container = ary_options[i].value;
DPRINTF(E_WARN, L_GENERAL, "Using arbitrary root container [%s]\n",
ary_options[i].value);
break;
}
break;
case UPNPMINISSDPDSOCKET:
minissdpdsocketpath = ary_options[i].value;
break;
case UPNPUUID:
strcpy(uuidvalue+5, ary_options[i].value);
break;
case USER_ACCOUNT:
uid = strtoul(ary_options[i].value, &string, 0);
if (*string)
{
/* Symbolic username given, not UID. */
struct passwd *entry = getpwnam(ary_options[i].value);
if (!entry)
DPRINTF(E_FATAL, L_GENERAL, "Bad user '%s'.\n",
ary_options[i].value);
uid = entry->pw_uid;
if (!gid)
gid = entry->pw_gid;
}
break;
case FORCE_SORT_CRITERIA:
force_sort_criteria = ary_options[i].value;
if (force_sort_criteria[0] == '!')
{
SETFLAG(FORCE_ALPHASORT_MASK);
force_sort_criteria++;
}
break;
case MAX_CONNECTIONS:
runtime_vars.max_connections = atoi(ary_options[i].value);
break;
case MERGE_MEDIA_DIRS:
if (strtobool(ary_options[i].value))
SETFLAG(MERGE_MEDIA_DIRS_MASK);
break;
case WIDE_LINKS:
if (strtobool(ary_options[i].value))
SETFLAG(WIDE_LINKS_MASK);
break;
case TIVO_DISCOVERY:
if (strcasecmp(ary_options[i].value, "beacon") == 0)
CLEARFLAG(TIVO_BONJOUR_MASK);
break;
case ENABLE_SUBTITLES:
if (!strtobool(ary_options[i].value))
CLEARFLAG(SUBTITLES_MASK);
break;
default:
DPRINTF(E_ERROR, L_GENERAL, "Unknown option in file %s\n",
optionsfile);
}
}
if (log_path[0] == '\0')
{
if (db_path[0] == '\0')
strncpyt(log_path, DEFAULT_LOG_PATH, PATH_MAX);
else
strncpyt(log_path, db_path, PATH_MAX);
}
if (db_path[0] == '\0')
strncpyt(db_path, DEFAULT_DB_PATH, PATH_MAX);
/* command line arguments processing */
for (i=1; i<argc; i++)
{
if (argv[i][0] != '-')
{
DPRINTF(E_FATAL, L_GENERAL, "Unknown option: %s\n", argv[i]);
}
else if (strcmp(argv[i], "--help") == 0)
{
runtime_vars.port = -1;
break;
}
else switch(argv[i][1])
{
case 't':
if (i+1 < argc)
runtime_vars.notify_interval = atoi(argv[++i]);
else
DPRINTF(E_FATAL, L_GENERAL, "Option -%c takes one argument.\n", argv[i][1]);
break;
case 's':
if (i+1 < argc)
strncpyt(serialnumber, argv[++i], SERIALNUMBER_MAX_LEN);
else
DPRINTF(E_FATAL, L_GENERAL, "Option -%c takes one argument.\n", argv[i][1]);
break;
case 'm':
if (i+1 < argc)
strncpyt(modelnumber, argv[++i], MODELNUMBER_MAX_LEN);
else
DPRINTF(E_FATAL, L_GENERAL, "Option -%c takes one argument.\n", argv[i][1]);
break;
case 'p':
if (i+1 < argc)
runtime_vars.port = atoi(argv[++i]);
else
DPRINTF(E_FATAL, L_GENERAL, "Option -%c takes one argument.\n", argv[i][1]);
break;
case 'P':
if (i+1 < argc)
{
if (argv[++i][0] != '/')
DPRINTF(E_FATAL, L_GENERAL, "Option -%c requires an absolute filename.\n", argv[i-1][1]);
else
pidfilename = argv[i];
}
else
DPRINTF(E_FATAL, L_GENERAL, "Option -%c takes one argument.\n", argv[i][1]);
break;
case 'd':
debug_flag = 1;
case 'v':
verbose_flag = 1;
break;
case 'L':
SETFLAG(NO_PLAYLIST_MASK);
break;
case 'w':
if (i+1 < argc)
presurl = argv[++i];
else
DPRINTF(E_FATAL, L_GENERAL, "Option -%c takes one argument.\n", argv[i][1]);
break;
case 'i':
if (i+1 < argc)
{
i++;
if (ifaces >= MAX_LAN_ADDR)
{
DPRINTF(E_ERROR, L_GENERAL, "Too many interfaces (max: %d), ignoring %s\n",
MAX_LAN_ADDR, argv[i]);
break;
}
runtime_vars.ifaces[ifaces++] = argv[i];
}
else
DPRINTF(E_FATAL, L_GENERAL, "Option -%c takes one argument.\n", argv[i][1]);
break;
case 'f':
i++; /* discarding, the config file is already read */
break;
case 'h':
runtime_vars.port = -1; // triggers help display
break;
case 'r':
SETFLAG(RESCAN_MASK);
break;
case 'R':
snprintf(buf, sizeof(buf), "rm -rf %s/files.db %s/art_cache", db_path, db_path);
if (system(buf) != 0)
DPRINTF(E_FATAL, L_GENERAL, "Failed to clean old file cache %s. EXITING\n", db_path);
break;
case 'u':
if (i+1 != argc)
{
i++;
uid = strtoul(argv[i], &string, 0);
if (*string)
{
/* Symbolic username given, not UID. */
struct passwd *entry = getpwnam(argv[i]);
if (!entry)
DPRINTF(E_FATAL, L_GENERAL, "Bad user '%s'.\n", argv[i]);
uid = entry->pw_uid;
if (!gid)
gid = entry->pw_gid;
}
}
else
DPRINTF(E_FATAL, L_GENERAL, "Option -%c takes one argument.\n", argv[i][1]);
break;
case 'g':
if (i+1 != argc)
{
i++;
gid = strtoul(argv[i], &string, 0);
if (*string)
{
/* Symbolic group given, not GID. */
struct group *grp = getgrnam(argv[i]);
if (!grp)
DPRINTF(E_FATAL, L_GENERAL, "Bad group '%s'.\n", argv[i]);
gid = grp->gr_gid;
}
}
else
DPRINTF(E_FATAL, L_GENERAL, "Option -%c takes one argument.\n", argv[i][1]);
break;
#ifdef __linux__
case 'S':
SETFLAG(SYSTEMD_MASK);
break;
#endif
case 'V':
printf("Version " MINIDLNA_VERSION "\n");
exit(0);
break;
default:
DPRINTF(E_ERROR, L_GENERAL, "Unknown option: %s\n", argv[i]);
runtime_vars.port = -1; // triggers help display
}
}
if (runtime_vars.port <= 0)
{
printf("Usage:\n\t"
"%s [-d] [-v] [-f config_file] [-p port]\n"
"\t\t[-i network_interface] [-u uid_to_run_as] [-g group_to_run_as]\n"
"\t\t[-t notify_interval] [-P pid_filename]\n"
"\t\t[-s serial] [-m model_number]\n"
#ifdef __linux__
"\t\t[-w url] [-r] [-R] [-L] [-S] [-V] [-h]\n"
#else
"\t\t[-w url] [-r] [-R] [-L] [-V] [-h]\n"
#endif
"\nNotes:\n\tNotify interval is in seconds. Default is 895 seconds.\n"
"\tDefault pid file is %s.\n"
"\tWith -d minidlna will run in debug mode (not daemonize).\n"
"\t-w sets the presentation url. Default is http address on port 80\n"
"\t-v enables verbose output\n"
"\t-h displays this text\n"
"\t-r forces a rescan\n"
"\t-R forces a rebuild\n"
"\t-L do not create playlists\n"
#ifdef __linux__
"\t-S changes behaviour for systemd\n"
#endif
"\t-V print the version number\n",
argv[0], pidfilename);
return 1;
}
if (verbose_flag)
{
strcpy(log_str+65, "debug");
log_level = log_str;
}
else if (!log_level)
log_level = log_str;
/* Set the default log file path to NULL (stdout) */
path = NULL;
if (debug_flag)
{
pid = getpid();
strcpy(log_str+65, "maxdebug");
log_level = log_str;
}
else if (GETFLAG(SYSTEMD_MASK))
{
pid = getpid();
}
else
{
pid = process_daemonize();
#ifdef READYNAS
unlink("/ramfs/.upnp-av_scan");
path = "/var/log/upnp-av.log";
#else
if (access(db_path, F_OK) != 0)
make_dir(db_path, S_ISVTX|S_IRWXU|S_IRWXG|S_IRWXO);
snprintf(buf, sizeof(buf), "%s/minidlna.log", log_path);
path = buf;
#endif
}
log_init(path, log_level);
if (process_check_if_running(pidfilename) < 0)
{
DPRINTF(E_ERROR, L_GENERAL, SERVER_NAME " is already running. EXITING.\n");
return 1;
}
set_startup_time();
/* presentation url */
if (presurl)
strncpyt(presentationurl, presurl, PRESENTATIONURL_MAX_LEN);
else
strcpy(presentationurl, "/");
/* set signal handlers */
memset(&sa, 0, sizeof(struct sigaction));
sa.sa_handler = sigterm;
if (sigaction(SIGTERM, &sa, NULL))
DPRINTF(E_FATAL, L_GENERAL, "Failed to set %s handler. EXITING.\n", "SIGTERM");
if (sigaction(SIGINT, &sa, NULL))
DPRINTF(E_FATAL, L_GENERAL, "Failed to set %s handler. EXITING.\n", "SIGINT");
if (signal(SIGPIPE, SIG_IGN) == SIG_ERR)
DPRINTF(E_FATAL, L_GENERAL, "Failed to set %s handler. EXITING.\n", "SIGPIPE");
if (signal(SIGHUP, &sighup) == SIG_ERR)
DPRINTF(E_FATAL, L_GENERAL, "Failed to set %s handler. EXITING.\n", "SIGHUP");
signal(SIGUSR1, &sigusr1);
sa.sa_handler = process_handle_child_termination;
if (sigaction(SIGCHLD, &sa, NULL))
DPRINTF(E_FATAL, L_GENERAL, "Failed to set %s handler. EXITING.\n", "SIGCHLD");
if (writepidfile(pidfilename, pid, uid) != 0)
pidfilename = NULL;
if (uid > 0)
{
struct stat st;
if (stat(db_path, &st) == 0 && st.st_uid != uid && chown(db_path, uid, -1) != 0)
DPRINTF(E_ERROR, L_GENERAL, "Unable to set db_path [%s] ownership to %d: %s\n",
db_path, uid, strerror(errno));
}
if (gid > 0 && setgid(gid) == -1)
DPRINTF(E_FATAL, L_GENERAL, "Failed to switch to gid '%d'. [%s] EXITING.\n",
gid, strerror(errno));
if (uid > 0 && setuid(uid) == -1)
DPRINTF(E_FATAL, L_GENERAL, "Failed to switch to uid '%d'. [%s] EXITING.\n",
uid, strerror(errno));
children = calloc(runtime_vars.max_connections, sizeof(struct child));
if (!children)
{
DPRINTF(E_ERROR, L_GENERAL, "Allocation failed\n");
return 1;
}
if ((error = event_module.init()) != 0)
DPRINTF(E_FATAL, L_GENERAL, "Failed to init event module. "
"[%s] EXITING.\n", strerror(error));
return 0;
}
/* === main === */
/* process HTTP or SSDP requests */
int
main(int argc, char **argv)
{
int ret, i;
int shttpl = -1;
int smonitor = -1;
struct upnphttp * e = 0;
struct upnphttp * next;
struct timeval tv, timeofday, lastnotifytime = {0, 0};
time_t lastupdatetime = 0, lastdbtime = 0;
u_long timeout; /* in milliseconds */
int last_changecnt = 0;
pid_t scanner_pid = 0;
pthread_t inotify_thread = 0;
struct event ssdpev, httpev, monev;
#ifdef TIVO_SUPPORT
uint8_t beacon_interval = 5;
int sbeacon = -1;
struct sockaddr_in tivo_bcast;
struct timeval lastbeacontime = {0, 0};
struct event beaconev;
#endif
for (i = 0; i < L_MAX; i++)
log_level[i] = E_WARN;
ret = init(argc, argv);
if (ret != 0)
return 1;
init_nls();
DPRINTF(E_WARN, L_GENERAL, "Starting " SERVER_NAME " version " MINIDLNA_VERSION ".\n");
if (sqlite3_libversion_number() < 3005001)
{
DPRINTF(E_WARN, L_GENERAL, "SQLite library is old. Please use version 3.5.1 or newer.\n");
}
LIST_INIT(&upnphttphead);
ret = open_db(NULL);
if (ret == 0)
{
updateID = sql_get_int_field(db, "SELECT VALUE from SETTINGS where KEY = 'UPDATE_ID'");
if (updateID == -1)
ret = -1;
}
check_db(db, ret, &scanner_pid);
lastdbtime = _get_dbtime();
#ifdef HAVE_INOTIFY
if( GETFLAG(INOTIFY_MASK) )
{
if (!sqlite3_threadsafe() || sqlite3_libversion_number() < 3005001)
DPRINTF(E_ERROR, L_GENERAL, "SQLite library is not threadsafe! "
"Inotify will be disabled.\n");
else if (pthread_create(&inotify_thread, NULL, start_inotify, NULL) != 0)
DPRINTF(E_FATAL, L_GENERAL, "ERROR: pthread_create() failed for start_inotify. EXITING\n");
}
#endif /* HAVE_INOTIFY */
#ifdef HAVE_KQUEUE
if (!GETFLAG(SCANNING_MASK))
kqueue_monitor_start();
#endif /* HAVE_KQUEUE */
smonitor = OpenAndConfMonitorSocket();
if (smonitor > 0)
{
monev = (struct event ){ .fd = smonitor, .rdwr = EVENT_READ, .process = ProcessMonitorEvent };
event_module.add(&monev);
}
sssdp = OpenAndConfSSDPReceiveSocket();
if (sssdp < 0)
{
DPRINTF(E_INFO, L_GENERAL, "Failed to open socket for receiving SSDP. Trying to use MiniSSDPd\n");
reload_ifaces(0); /* populate lan_addr[0].str */
if (SubmitServicesToMiniSSDPD(lan_addr[0].str, runtime_vars.port) < 0)
DPRINTF(E_FATAL, L_GENERAL, "Failed to connect to MiniSSDPd. EXITING");
}
else
{
ssdpev = (struct event ){ .fd = sssdp, .rdwr = EVENT_READ, .process = ProcessSSDPRequest };
event_module.add(&ssdpev);
}
/* open socket for HTTP connections. */
shttpl = OpenAndConfHTTPSocket(runtime_vars.port);
if (shttpl < 0)
DPRINTF(E_FATAL, L_GENERAL, "Failed to open socket for HTTP. EXITING\n");
DPRINTF(E_WARN, L_GENERAL, "HTTP listening on port %d\n", runtime_vars.port);
httpev = (struct event ){ .fd = shttpl, .rdwr = EVENT_READ, .process = ProcessListen };
event_module.add(&httpev);
#ifdef TIVO_SUPPORT
if (GETFLAG(TIVO_MASK))
{
DPRINTF(E_WARN, L_GENERAL, "TiVo support is enabled.\n");
/* Add TiVo-specific randomize function to sqlite */
ret = sqlite3_create_function(db, "tivorandom", 1, SQLITE_UTF8, NULL, &TiVoRandomSeedFunc, NULL, NULL);
if (ret != SQLITE_OK)
DPRINTF(E_ERROR, L_TIVO, "ERROR: Failed to add sqlite randomize function for TiVo!\n");
if (GETFLAG(TIVO_BONJOUR_MASK))
{
tivo_bonjour_register();
}
else
{
/* open socket for sending Tivo notifications */
sbeacon = OpenAndConfTivoBeaconSocket();
if(sbeacon < 0)
DPRINTF(E_FATAL, L_GENERAL, "Failed to open sockets for sending Tivo beacon notify "
"messages. EXITING\n");
beaconev = { .fd = sbeacon, .rdwr = EVENT_READ, .process = ProcessTiVoBeacon };
event_module.add(&beaconev);
tivo_bcast.sin_family = AF_INET;
tivo_bcast.sin_addr.s_addr = htonl(getBcastAddress());
tivo_bcast.sin_port = htons(2190);
}
}
#endif
reload_ifaces(0);
lastnotifytime.tv_sec = time(NULL) + runtime_vars.notify_interval;
/* main loop */
while (!quitting)
{
if (gettimeofday(&timeofday, 0) < 0)
DPRINTF(E_FATAL, L_GENERAL, "gettimeofday(): %s\n", strerror(errno));
/* Check if we need to send SSDP NOTIFY messages and do it if
* needed */
tv = lastnotifytime;
tv.tv_sec += runtime_vars.notify_interval;
if (timevalcmp(&timeofday, &tv, >=))
{
DPRINTF(E_DEBUG, L_SSDP, "Sending SSDP notifies\n");
for (i = 0; i < n_lan_addr; i++)
{
SendSSDPNotifies(lan_addr[i].snotify, lan_addr[i].str,
runtime_vars.port, runtime_vars.notify_interval);
}
lastnotifytime = timeofday;
timeout = runtime_vars.notify_interval * 1000;
}
else
{
timevalsub(&tv, &timeofday);
timeout = tv.tv_sec * 1000 + tv.tv_usec / 1000;
}
#ifdef TIVO_SUPPORT
if (sbeacon >= 0)
{
u_long beacontimeout;
tv = lastbeacontime;
tv.tv_sec += beacon_interval;
if (timevalcmp(&timeofday, &tv, >=))
{
sendBeaconMessage(sbeacon, &tivo_bcast, sizeof(struct sockaddr_in), 1);
lastbeacontime = timeofday;
beacontimeout = beacon_interval * 1000;
if (timeout > beacon_interval * 1000)
timeout = beacon_interval * 1000;
/* Beacons should be sent every 5 seconds or
* so for the first minute, then every minute
* or so thereafter. */
if (beacon_interval == 5 && (timeofday.tv_sec - startup_time) > 60)
beacon_interval = 60;
}
else
{
timevalsub(&tv, &timeofday);
beacontimeout = tv.tv_sec * 1000 +
tv.tv_usec / 1000;
}
if (timeout > beacontimeout)
timeout = beacontimeout;
}
#endif
if (GETFLAG(SCANNING_MASK) && kill(scanner_pid, 0) != 0) {
CLEARFLAG(SCANNING_MASK);
if (_get_dbtime() != lastdbtime)
updateID++;
#ifdef HAVE_KQUEUE
kqueue_monitor_start();
#endif /* HAVE_KQUEUE */
}
event_module.process(timeout);
if (quitting)
goto shutdown;
upnpevents_gc();
/* increment SystemUpdateID if the content database has changed,
* and if there is an active HTTP connection, at most once every 2 seconds */
if (!LIST_EMPTY(&upnphttphead) &&
(timeofday.tv_sec >= (lastupdatetime + 2)))
{
if (GETFLAG(SCANNING_MASK))
{
time_t dbtime = _get_dbtime();
if (dbtime != lastdbtime)
{
lastdbtime = dbtime;
last_changecnt = -1;
}
}
if (sqlite3_total_changes(db) != last_changecnt)
{
updateID++;
last_changecnt = sqlite3_total_changes(db);
upnp_event_var_change_notify(EContentDirectory);
lastupdatetime = timeofday.tv_sec;
}
}
/* delete finished HTTP connections */
for (e = upnphttphead.lh_first; e != NULL; e = next)
{
next = e->entries.le_next;
if(e->state >= 100)
{
LIST_REMOVE(e, entries);
Delete_upnphttp(e);
}
}
}
shutdown:
/* kill the scanner */
if (GETFLAG(SCANNING_MASK) && scanner_pid)
kill(scanner_pid, SIGKILL);
/* close out open sockets */
while (upnphttphead.lh_first != NULL)
{
e = upnphttphead.lh_first;
LIST_REMOVE(e, entries);
Delete_upnphttp(e);
}
if (sssdp >= 0)
close(sssdp);
if (shttpl >= 0)
close(shttpl);
#ifdef TIVO_SUPPORT
if (sbeacon >= 0)
close(sbeacon);
#endif
if (smonitor >= 0)
close(smonitor);
for (i = 0; i < n_lan_addr; i++)
{
SendSSDPGoodbyes(lan_addr[i].snotify);
close(lan_addr[i].snotify);
}
if (inotify_thread)
{
pthread_kill(inotify_thread, SIGCHLD);
pthread_join(inotify_thread, NULL);
}
/* kill other child processes */
process_reap_children();
free(children);
sql_exec(db, "UPDATE SETTINGS set VALUE = '%u' where KEY = 'UPDATE_ID'", updateID);
sqlite3_close(db);
upnpevents_removeSubscribers();
if (pidfilename && unlink(pidfilename) < 0)
DPRINTF(E_ERROR, L_GENERAL, "Failed to remove pidfile %s: %s\n", pidfilename, strerror(errno));
log_close();
freeoptions();
exit(EXIT_SUCCESS);
}