Switch to side-by-side view

--- a
+++ b/src/main.cxx
@@ -0,0 +1,518 @@
+/* Copyright (C) 2015 J.F.Dockes
+ *   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 of the License, 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.,
+ *   59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
+ */
+/////////////////////////////////////////////////////////////////////
+// Main program
+
+#include <errno.h>             
+#include <fcntl.h>             
+#include <pwd.h>               
+#include <signal.h>            
+#include <stdio.h>             
+#include <stdlib.h>            
+#include <sys/param.h>         
+#include <unistd.h>            
+#include <grp.h>
+
+#include <iostream>            
+#include <string>              
+#include <unordered_map>       
+#include <vector>              
+
+#include "libupnpp/log.hxx"    
+#include "libupnpp/upnpplib.hxx"
+#include "execmd.h"
+#include "conftree.hxx"
+#include "mpdcli.hxx"
+#include "upmpd.hxx"
+#include "httpfs.hxx"
+
+using namespace std;
+using namespace UPnPP;
+
+static char *thisprog;
+
+static int op_flags;
+#define OPT_MOINS 0x1
+#define OPT_h     0x2
+#define OPT_p     0x4
+#define OPT_d     0x8
+#define OPT_D     0x10
+#define OPT_c     0x20
+#define OPT_l     0x40
+#define OPT_f     0x80
+#define OPT_q     0x100
+#define OPT_i     0x200
+#define OPT_P     0x400
+#define OPT_O     0x800
+#define OPT_v     0x1000
+
+static const char usage[] = 
+    "-c configfile \t configuration file to use\n"
+    "-h host    \t specify host MPD is running on\n"
+    "-p port     \t specify MPD port\n"
+    "-d logfilename\t debug messages to\n"
+    "-l loglevel\t  log level (0-6)\n"
+    "-D    \t run as a daemon\n"
+    "-f friendlyname\t define device displayed name\n"
+    "-q 0|1\t if set, we own the mpd queue, else avoid clearing it whenever we feel like it\n"
+    "-i iface    \t specify network interface name to be used for UPnP\n"
+    "-P upport    \t specify port number to be used for UPnP\n"
+    "-O 0|1\t decide if we run and export the OpenHome services\n"
+    "-v      \tprint version info\n"
+    "\n"
+    ;
+
+static void
+versionInfo(FILE *fp)
+{
+    fprintf(fp, "Upmpdcli %s %s\n",
+           UPMPDCLI_PACKAGE_VERSION, LibUPnP::versionString().c_str());
+}
+
+static void
+Usage(FILE *fp = stderr)
+{
+    fprintf(fp, "%s: usage:\n%s", thisprog, usage);
+    versionInfo(fp);
+    exit(1);
+}
+
+
+static const string dfltFriendlyName("UpMpd");
+
+// This is global
+string g_protocolInfo;
+
+// Static for cleanup in sig handler.
+static UpnpDevice *dev;
+
+string g_datadir(DATADIR "/");
+
+// Global
+string g_configfilename(CONFIGDIR "/upmpdcli.conf");
+ConfSimple *g_config;
+
+static void onsig(int)
+{
+    LOGDEB("Got sig" << endl);
+    dev->shouldExit();
+}
+
+static const int catchedSigs[] = {SIGINT, SIGQUIT, SIGTERM};
+static void setupsigs()
+{
+    struct sigaction action;
+    action.sa_handler = onsig;
+    action.sa_flags = 0;
+    sigemptyset(&action.sa_mask);
+    for (unsigned int i = 0; i < sizeof(catchedSigs) / sizeof(int); i++)
+        if (signal(catchedSigs[i], SIG_IGN) != SIG_IGN) {
+            if (sigaction(catchedSigs[i], &action, 0) < 0) {
+                perror("Sigaction failed");
+            }
+        }
+}
+
+int main(int argc, char *argv[])
+{
+    // Path for the sc2mpd command, or empty
+    string sc2mpdpath;
+
+    // Sender mode: path for the command creating the mpd and mpd2sc
+    // processes, and port for the auxiliary mpd.
+    string senderpath;
+    int sendermpdport = 6700;
+
+    // Main MPD parameters
+    string mpdhost("localhost");
+    int mpdport = 6600;
+    string mpdpassword;
+
+    string logfilename;
+    int loglevel(Logger::LLINF);
+    string friendlyname(dfltFriendlyName);
+    bool ownqueue = true;
+    bool enableAV = true;
+    bool enableOH = true;
+    bool ohmetapersist = true;
+    string upmpdcliuser("upmpdcli");
+    string pidfilename("/var/run/upmpdcli.pid");
+    string iconpath(DATADIR "/icon.png");
+    string presentationhtml(DATADIR "/presentation.html");
+    string iface;
+    unsigned short upport = 0;
+    string upnpip;
+
+    const char *cp;
+    if ((cp = getenv("UPMPD_HOST")))
+        mpdhost = cp;
+    if ((cp = getenv("UPMPD_PORT")))
+        mpdport = atoi(cp);
+    if ((cp = getenv("UPMPD_FRIENDLYNAME")))
+        friendlyname = atoi(cp);
+    if ((cp = getenv("UPMPD_CONFIG")))
+        g_configfilename = cp;
+    if ((cp = getenv("UPMPD_UPNPIFACE")))
+        iface = cp;
+    if ((cp = getenv("UPMPD_UPNPPORT")))
+        upport = atoi(cp);
+
+    thisprog = argv[0];
+    argc--; argv++;
+    while (argc > 0 && **argv == '-') {
+        (*argv)++;
+        if (!(**argv))
+            Usage();
+        while (**argv)
+            switch (*(*argv)++) {
+            case 'c':   op_flags |= OPT_c; if (argc < 2)  Usage();
+                g_configfilename = *(++argv); argc--; goto b1;
+            case 'D':   op_flags |= OPT_D; break;
+            case 'd':   op_flags |= OPT_d; if (argc < 2)  Usage();
+                logfilename = *(++argv); argc--; goto b1;
+            case 'f':   op_flags |= OPT_f; if (argc < 2)  Usage();
+                friendlyname = *(++argv); argc--; goto b1;
+            case 'h':   op_flags |= OPT_h; if (argc < 2)  Usage();
+                mpdhost = *(++argv); argc--; goto b1;
+            case 'i':   op_flags |= OPT_i; if (argc < 2)  Usage();
+                iface = *(++argv); argc--; goto b1;
+            case 'l':   op_flags |= OPT_l; if (argc < 2)  Usage();
+                loglevel = atoi(*(++argv)); argc--; goto b1;
+            case 'O': {
+                op_flags |= OPT_O; 
+                if (argc < 2)  Usage();
+                const char *cp =  *(++argv);
+                if (*cp == '1' || *cp == 't' || *cp == 'T' || *cp == 'y' || 
+                    *cp == 'Y')
+                    enableOH = true;
+                argc--; goto b1;
+            }
+            case 'P':   op_flags |= OPT_P; if (argc < 2)  Usage();
+                upport = atoi(*(++argv)); argc--; goto b1;
+            case 'p':   op_flags |= OPT_p; if (argc < 2)  Usage();
+                mpdport = atoi(*(++argv)); argc--; goto b1;
+            case 'q':   op_flags |= OPT_q; if (argc < 2)  Usage();
+                ownqueue = atoi(*(++argv)) != 0; argc--; goto b1;
+            case 'v': versionInfo(stdout); exit(0); break;
+            default: Usage();   break;
+            }
+    b1: argc--; argv++;
+    }
+
+    if (argc != 0)
+        Usage();
+
+    UpMpd::Options opts;
+
+    string cachedir;
+    string onstart;
+    string onstop;
+    string onvolumechange;
+    if (!g_configfilename.empty()) {
+        g_config = new ConfSimple(g_configfilename.c_str(), 1, true);
+        if (!g_config || !g_config->ok()) {
+            cerr << "Could not open config: " << g_configfilename << endl;
+            return 1;
+        }
+
+        string value;
+        if (!(op_flags & OPT_d))
+            g_config->get("logfilename", logfilename);
+        if (!(op_flags & OPT_f))
+            g_config->get("friendlyname", friendlyname);
+        if (!(op_flags & OPT_l) && g_config->get("loglevel", value))
+            loglevel = atoi(value.c_str());
+        if (!(op_flags & OPT_h))
+            g_config->get("mpdhost", mpdhost);
+        if (!(op_flags & OPT_p) && g_config->get("mpdport", value)) {
+            mpdport = atoi(value.c_str());
+        }
+        g_config->get("mpdpassword", mpdpassword);
+        if (!(op_flags & OPT_q) && g_config->get("ownqueue", value)) {
+            ownqueue = atoi(value.c_str()) != 0;
+        }
+        if (g_config->get("openhome", value)) {
+            enableOH = atoi(value.c_str()) != 0;
+        }
+        if (g_config->get("upnpav", value)) {
+            enableAV = atoi(value.c_str()) != 0;
+        }
+        if (g_config->get("ohmetapersist", value)) {
+            ohmetapersist = atoi(value.c_str()) != 0;
+        }
+        g_config->get("iconpath", iconpath);
+        g_config->get("presentationhtml", presentationhtml);
+        g_config->get("cachedir", cachedir);
+        g_config->get("onstart", onstart);
+        g_config->get("onstop", onstop);
+        g_config->get("onvolumechange", onvolumechange);
+        if (!(op_flags & OPT_i)) {
+            g_config->get("upnpiface", iface);
+            if (iface.empty()) {
+                g_config->get("upnpip", upnpip);
+            }
+        }
+        if (!(op_flags & OPT_P) && g_config->get("upnpport", value)) {
+            upport = atoi(value.c_str());
+        }
+        if (g_config->get("schttpport", value))
+            opts.schttpport = atoi(value.c_str());
+        g_config->get("scplaymethod", opts.scplaymethod);
+        g_config->get("sc2mpd", sc2mpdpath);
+        if (g_config->get("ohmetasleep", value))
+            opts.ohmetasleep = atoi(value.c_str());
+
+        g_config->get("scsenderpath", senderpath);
+        if (g_config->get("scsendermpdport", value))
+            sendermpdport = atoi(value.c_str());
+    }
+    if (Logger::getTheLog(logfilename) == 0) {
+        cerr << "Can't initialize log" << endl;
+        return 1;
+    }
+    Logger::getTheLog("")->setLogLevel(Logger::LogLevel(loglevel));
+
+    Pidfile pidfile(pidfilename);
+
+    // If started by root, do the pidfile + change uid thing
+    uid_t runas(0);
+    gid_t runasg(0);
+    if (geteuid() == 0) {
+        struct passwd *pass = getpwnam(upmpdcliuser.c_str());
+        if (pass == 0) {
+            LOGFAT("upmpdcli won't run as root and user " << upmpdcliuser << 
+                   " does not exist " << endl);
+            return 1;
+        }
+        runas = pass->pw_uid;
+        runasg = pass->pw_gid;
+
+        pid_t pid;
+        if ((pid = pidfile.open()) != 0) {
+            LOGFAT("Can't open pidfile: " << pidfile.getreason() << 
+                   ". Return (other pid?): " << pid << endl);
+            return 1;
+        }
+        if (pidfile.write_pid() != 0) {
+            LOGFAT("Can't write pidfile: " << pidfile.getreason() << endl);
+            return 1;
+        }
+	if (cachedir.empty())
+            cachedir = "/var/cache/upmpdcli";
+    } else {
+	if (cachedir.empty())
+            cachedir = path_cat(path_tildexpand("~") , "/.cache/upmpdcli");
+    }
+
+    string& mcfn = opts.cachefn;
+    if (ohmetapersist) {
+        opts.cachefn = path_cat(cachedir, "/metacache");
+        if (!path_makepath(cachedir, 0755)) {
+            LOGERR("makepath("<< cachedir << ") : errno : " << errno << endl);
+        } else {
+            int fd;
+            if ((fd = open(mcfn.c_str(), O_CREAT|O_RDWR, 0644)) < 0) {
+                LOGERR("creat("<< mcfn << ") : errno : " << errno << endl);
+            } else {
+                close(fd);
+                if (geteuid() == 0 && chown(mcfn.c_str(), runas, -1) != 0) {
+                    LOGERR("chown("<< mcfn << ") : errno : " << errno << endl);
+                }
+                if (geteuid() == 0 && chown(cachedir.c_str(), runas, -1) != 0) {
+                    LOGERR("chown("<< cachedir << ") : errno : " << errno << endl);
+                }
+            }
+        }
+    }
+    
+    if ((op_flags & OPT_D)) {
+        if (daemon(1, 0)) {
+            LOGFAT("Daemon failed: errno " << errno << endl);
+            return 1;
+        }
+    }
+
+    if (geteuid() == 0) {
+        // Need to rewrite pid, it may have changed with the daemon call
+        pidfile.write_pid();
+        if (!logfilename.empty() && logfilename.compare("stderr")) {
+            if (chown(logfilename.c_str(), runas, -1) < 0) {
+                LOGERR("chown("<<logfilename<<") : errno : " << errno << endl);
+            }
+        }
+        if (initgroups(upmpdcliuser.c_str(), runasg) < 0) {
+            LOGERR("initgroup failed. Errno: " << errno << endl);
+        }
+        if (setuid(runas) < 0) {
+            LOGFAT("Can't set my uid to " << runas << " current: " << geteuid()
+                   << endl);
+            return 1;
+        }
+#if 0        
+        gid_t list[100];
+        int ng = getgroups(100, list);
+        cerr << "GROUPS: ";
+        for (int i = 0; i < ng; i++) {
+            cerr << int(list[i]) << " ";
+        }
+        cerr << endl;
+#endif
+    }
+
+//// Dropped root 
+
+    if (sc2mpdpath.empty()) {
+        // Do we have an sc2mpd command installed (for songcast)?
+        if (!ExecCmd::which("sc2mpd", sc2mpdpath))
+            sc2mpdpath.clear();
+    }
+    if (senderpath.empty()) {
+        // Do we have an scmakempdsender command installed (for
+        // starting the songcast sender and its auxiliary mpd)?
+        if (!ExecCmd::which("scmakempdsender", senderpath))
+            senderpath.clear();
+    }
+    
+    if (!sc2mpdpath.empty()) {
+        // Check if sc2mpd is actually there
+        if (access(sc2mpdpath.c_str(), X_OK|R_OK) != 0) {
+            LOGERR("Specified path for sc2mpd: " << sc2mpdpath << 
+                   " is not executable" << endl);
+            sc2mpdpath.clear();
+        }
+    }
+
+    if (!senderpath.empty()) {
+        // Check that both the starter script and the mpd2sc sender
+        // command are executable. We'll assume that mpd is ok
+        if (access(senderpath.c_str(), X_OK|R_OK) != 0) {
+            LOGERR("The specified path for the sender starter script: ["
+                   << senderpath <<
+                   "] is not executable, disabling the sender mode.\n");
+            senderpath.clear();
+        } else {
+            string path;
+            if (!ExecCmd::which("mpd2sc", path)) {
+                LOGERR("Sender starter was specified and found but the mpd2sc "
+                       "command is not found (or executable). Disabling "
+                       "the sender mode.\n");
+                senderpath.clear();
+            }
+        }
+    }
+
+
+    // Initialize MPD client object. Retry until it works or power fail.
+    MPDCli *mpdclip = 0;
+    int mpdretrysecs = 2;
+    for (;;) {
+        mpdclip = new MPDCli(mpdhost, mpdport, mpdpassword, onstart, onstop,
+                             onvolumechange);
+        if (mpdclip == 0) {
+            LOGFAT("Can't allocate MPD client object" << endl);
+            return 1;
+        }
+        if (!mpdclip->ok()) {
+            LOGERR("MPD connection failed" << endl);
+            delete mpdclip;
+            mpdclip = 0;
+            sleep(mpdretrysecs);
+            mpdretrysecs = MIN(2*mpdretrysecs, 120);
+        } else {
+            break;
+        }
+    }
+
+    // Initialize libupnpp, and check health
+    LibUPnP *mylib = 0;
+    string hwaddr;
+    int libretrysecs = 10;
+    for (;;) {
+        // Libupnp init fails if we're started at boot and the network
+        // is not ready yet. So retry this forever
+        mylib = LibUPnP::getLibUPnP(true, &hwaddr, iface, upnpip, upport);
+        if (mylib) {
+            break;
+        }
+        sleep(libretrysecs);
+        libretrysecs = MIN(2*libretrysecs, 120);
+    }
+
+    if (!mylib->ok()) {
+        LOGFAT("Lib init failed: " <<
+               mylib->errAsString("main", mylib->getInitError()) << endl);
+        return 1;
+    }
+
+    if ((cp = getenv("UPMPDCLI_UPNPLOGFILENAME"))) {
+        char *cp1 = getenv("UPMPDCLI_UPNPLOGLEVEL");
+        int loglevel = LibUPnP::LogLevelNone;
+        if (cp1) {
+            loglevel = atoi(cp1);
+        }
+        loglevel = loglevel < 0 ? 0: loglevel;
+        loglevel = loglevel > int(LibUPnP::LogLevelDebug) ? 
+            int(LibUPnP::LogLevelDebug) : loglevel;
+
+        if (loglevel != LibUPnP::LogLevelNone) {
+            mylib->setLogFileName(cp, LibUPnP::LogLevel(loglevel));
+        }
+    }
+
+    // Create unique ID
+    string UUID = LibUPnP::makeDevUUID(friendlyname, hwaddr);
+
+    // Initialize the data we serve through HTTP (device and service
+    // descriptions, icons, presentation page, etc.)
+    unordered_map<string, VDirContent> files;
+    if (!initHttpFs(files, g_datadir, UUID, friendlyname, enableAV, enableOH,
+                    !senderpath.empty(),
+                    iconpath, presentationhtml)) {
+        exit(1);
+    }
+
+    if (ownqueue)
+        opts.options |= UpMpd::upmpdOwnQueue;
+    if (enableOH)
+        opts.options |= UpMpd::upmpdDoOH;
+    if (ohmetapersist)
+        opts.options |= UpMpd::upmpdOhMetaPersist;
+    if (!sc2mpdpath.empty()) {
+        opts.sc2mpdpath = sc2mpdpath;
+        opts.options |= UpMpd::upmpdOhReceiver;
+    }
+    if (!senderpath.empty()) {
+        opts.options |= UpMpd::upmpdOhSenderReceiver;
+        opts.senderpath = senderpath;
+        opts.sendermpdport = sendermpdport;
+    }
+
+    if (!enableAV)
+        opts.options |= UpMpd::upmpdNoAV;
+    // Initialize the UPnP device object.
+    UpMpd device(string("uuid:") + UUID, friendlyname, 
+                 files, mpdclip, opts);
+    dev = &device;
+
+    // And forever generate state change events.
+    LOGDEB("Entering event loop" << endl);
+    setupsigs();
+    device.eventloop();
+    LOGDEB("Event loop returned" << endl);
+
+    return 0;
+}