/*  -*- mode: cc-mode; coding: utf-8; tab-width: 4; c-basic-offset: 4 -*- vim:fenc=utf-8:filetype=c:et:sw=4:ts=4:sts=4

    daemondo - main.c
    
    Copyright (c) 2005-2007 James Berry <jberry@macports.org>
    All rights reserved.

    Redistribution and use in source and binary forms, with or without
    modification, are permitted provided that the following conditions
    are met:
    1. Redistributions of source code must retain the above copyright
       notice, this list of conditions and the following disclaimer.
    2. 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.
    3. Neither the name of The MacPorts Project nor the names of its contributors
       may 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.
*/

/*
    Potentially useful System Configuration regex patterns:

        (backslash quoting below is only to protect the C comment)
        State:/Network/Interface/.*\/Link 
        State:/Network/Interface/.*\/IPv4
        State:/Network/Interface/.*\/IPv6
        
        State:/Network/Global/DNS
        State:/Network/Global/IPv4
        
    Potentially useful notifications from Darwin Notify Center:
    
        com.apple.system.config.network_change
*/

#if HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <getopt.h>
#include <fcntl.h>
#include <stdarg.h>
#include <strings.h>
#include <time.h>
#include <sys/types.h>
#include <sys/event.h>
#include <sys/time.h>
#include <sys/wait.h>
#include <mach/mach.h>

#include <CoreFoundation/CoreFoundation.h>
#include <SystemConfiguration/SystemConfiguration.h>

#include <IOKit/pwr_mgt/IOPMLib.h>
#include <IOKit/IOMessage.h>

// Constants
static const CFTimeInterval kChildDeathTimeout = 20.0;
static const CFTimeInterval kChildStartPidTimeout = 30.0;

typedef enum {
    kPidStyleUnknown = 0,
    kPidStyleNone,
    kPidStyleExec,
    kPidStyleFileAuto,
    kPidStyleFileClean
} PidStyle;

// Globals
static CFStringRef         kProgramName        = NULL;
static CFStringRef         kChildWatchMode     = NULL;

static int                 verbosity           = 0;        // Verbosity level
static const char*         label               = NULL;

static const char* const*  startArgs           = NULL;     // Argvs for start-cmd, stop-cmd, and restart-cmd
static const char* const*  stopArgs            = NULL;
static const char* const*  restartArgs         = NULL;

static PidStyle            pidStyle            = kPidStyleUnknown;
static const char*         pidFile             = NULL;

static int                 terminating         = 0;        // TRUE if we're terminating
static pid_t               runningPid          = 0;        // Current running pid (0 while stopped, -1 if we don't know pid)

static int                 kqfd                = 0;        // Kqueue file descriptor

static mach_port_t         sigChild_m_port     = 0;        // Mach port to send signals through
static mach_port_t         sigGeneric_m_port   = 0;        // Mach port to send signals through

static CFMutableArrayRef   scRestartPatterns   = NULL;     // Array of sc patterns to restart daemon on
static CFMutableArrayRef   distNotifyNames     = NULL;     // Array of distributed notification names to restart daemon on
static CFMutableArrayRef   darwinNotifyNames   = NULL;     // Array of darwin notification names to restart daemon on

static io_connect_t        pwrRootPort         = 0;
static int                 restartOnWakeup     = 0;        // TRUE to restart daemon on wake from sleep
static CFRunLoopTimerRef   restartTimer        = NULL;     // Timer for scheduled restart
static CFTimeInterval      restartHysteresis   = 5.0;      // Default hysteresis is 5 seconds
static int                 restartWait         = 3;        // Default wait during restart is 3 seconds


__printflike(1, 2)
static void
LogMessage(const char* fmt, ...)
{
    struct tm tm;
    time_t timestamp;
    char datestring[32];
    
    // Format the date-time stamp
    time(&timestamp);
    strftime(datestring, sizeof(datestring), "%F %T", localtime_r(&timestamp, &tm));
    
    // Output the log header
    if (label != NULL)
        printf("%s %s: ", datestring, label);
    else
        printf("%s ", datestring);
    
    // Output the message
    va_list ap;
    va_start(ap, fmt);
    vprintf(fmt, ap);
    va_end(ap);
}


static const char*
CatArray(const char* const* strarray, char* buf, size_t size)
{
    const char* sep = " ";
    int cnt = 0;
    if (size == 0)
        return NULL;
    *buf = '\0';
    for (cnt = 0; *strarray; ++strarray) {
        if (cnt++ > 0)
            strlcat(buf, sep, size);
        strlcat(buf, *strarray, size);
    }
    return buf;
}


static void
DoVersion(void)
{
    printf("daemondo, version 1.1\n\n");
}


static void
DoHelp(void)
{
    DoVersion();
    
    const char* helpText =
        "usage: daemondo [-hv] [--version]\n"
        "                     --start-cmd prog args... ;\n"
        "                     [--stop-cmd prog arg... ;]\n"
        "                     [--restart-cmd prog arg... ;]\n"
        "                     [--restart-wakeup]\n"
        "                     [--restart-netchange]\n"
        "\n"
        "daemondo is a wrapper program that runs daemons. It starts the specified\n"
        "daemon on launch, stops it when given SIGTERM, and restarts it on SIGHUP.\n"
        "It can also watch for transitions in system state, such as a change in\n"
        "network availability or system power state, and restart the daemon on such\n"
        "an event.\n"
        "\n"
        "daemondo works well as an adapter between darwin 8's launchd, and daemons\n"
        "that are normally started via traditional rc.d style scripts or parameters.\n"
        "\n"
        "Parameters:\n"
        "\n"
        "  -h, --help                      Provide this help.\n"
        "  -v                              Increase verbosity.\n"
        "      --verbosity=n               Set verbosity to n.\n"
        "  -V, --version                   Display program version information.\n"
        "  -l, --label=desc                Label used to describe the daemon.\n"
        "\n"
        "  -s, --start-cmd args... ;       Required: command that will start the daemon.\n"
        "  -k, --stop-cmd args... ;        Command that will stop the daemon.\n"
        "  -r, --restart-cmd args... ;     Command that will restart the daemon.\n"
        "\n"
        "      --pid=none|exec|fileauto|fileclean\n"
        "                                  Whether to use/how to treat pid file.\n"
        "      --pidfile=<pidfile>         A pidfile from which to scavenge the target pid.\n"
        "\n"
        "      --restart-wakeup            Restart daemon on wake from sleep.\n"
        "      --restart-netchange         Restart daemon on a network change.\n"
        "      --restart-config regex... ; SC patterns on which to restart the daemon.\n"
        "      --restart-dist-notify names... ;\n"
        "                                  Distributed Notification Center notifications\n"
        "                                  on which to restart the daemon.\n"
        "      --restart-darwin-notify names... ;\n"
        "                                  Darwin Notification Center notifications\n"
        "                                  on which to restart the daemon.\n"
        "      --restart-config regex... ; SC patterns on which to restart the daemon.\n"
        "\n"
        "daemondo responds to SIGHUP by restarting the daemon, and to SIGTERM by\n"
        "stopping it. daemondo exits on receipt of SIGTERM, or when it detects\n"
        "that the daemon process has died.\n"
        "\n"
        "The arguments start-cmd, stop-cmd, restart-cmd, restart-config,\n"
        "restart-dist-notify, and restart-darwin-notify, if present,\n"
        "must each be followed by arguments terminated by a ';'. You may need to\n"
        "escape or quote the ';' to protect it from special handling by your shell.\n"
        "\n"
        "daemondo runs in one of two modes: (1) If no stop-cmd is given, daemondo\n"
        "executes start-cmd asynchronously, and tracks the process id; that process id\n"
        "is used to signal the daemon for later stop and/or restart. (2) If stop-cmd\n"
        "is given, then both start-cmd and stop-cmd are issued synchronously, and are\n"
        "assumed to do all the work of controlling the daemon. In such cases there is\n"
        "no process id to track. In either mode, restart-cmd, if present, is used to\n"
        "restart the daemon. If in mode 1, restart-cmd must not disrupt the process id.\n"
        "If restart-cmd is not provided, the daemon is restarted via a stop/start\n"
        "sequence.\n"
        "\n"
        "The argument restart-config specifies a set of regex patterns corresponding\n"
        "to system configuration keys, on notification of change for which the daemon\n"
        "will be restarted\n"
        "\n"
        "The arguments restart-dist-notify and restart-darwin-notify specify a set of\n"
        "notification names from the distributed and darwin notification centers,\n"
        "respectively, on receipt of which the daemon will be restarted.\n"
        "\n"
        "The argument restart-wakeup will cause the daemon to be restarted when the\n"
        "computer wakes from sleep.\n"
        "\n"
        "The argument restart-netchange will cause the daemon to be restarted when\n"
        "the network configuration changes. This is a shortcut for the more\n"
        "verbose --restart-darwin-notify com.apple.system.config.network_change.\n"
        "\n"
        "In mode 1 only, daemondo will exit when it detects that the daemon being\n"
        "monitored has exited.\n"
        "\n"
        ;
        
    printf("%s", helpText);
}


static void
CreatePidFile(void)
{
    // Write a pid file if we're expected to
    if (pidFile != NULL)
    {
        FILE* f = NULL;
        switch (pidStyle)
        {
        default:
        case kPidStyleNone:         // No pid is available
        case kPidStyleFileAuto:     // The process should create its own pid file
        case kPidStyleFileClean:    // The process should create its own pid file
            break;
        case kPidStyleExec:         // We know the pid, and will write it to the pid file
            f = fopen(pidFile, "w");
            if (f != NULL)
            {
                fprintf(f, "%d", runningPid);
                fclose(f);
            }
            break;
        }
    }
}

static void
DestroyPidFile(void)
{
    // Cleanup the pid file
    if (pidFile != NULL)
    {
        switch (pidStyle)
        {
        default:
        case kPidStyleNone:         // No pid is available
        case kPidStyleFileAuto:     // The process should remove its own pid file
            break;
        case kPidStyleExec:         // We wrote the file, and we'll remove it
        case kPidStyleFileClean:    // The process wrote the file, but we'll remove it
            if (verbosity >= 5)
                LogMessage("Attempting to delete pidfile %s\n", pidFile);
            if (unlink(pidFile) && verbosity >= 3)
                LogMessage("Failed attempt to delete pidfile %s (%d)\n", pidFile, errno);
            break;
        }
    } else {
        if (verbosity >= 5)
            LogMessage("No pidfile to delete: none specified\n");
    }
}


static pid_t
CheckForValidPidFile(void)
{
    // Try to read the pid from the pid file
    pid_t pid = -1;
    FILE* f = fopen(pidFile, "r");
    if (f != NULL)
    {
        if (1 != fscanf(f, "%d", &pid))
            pid = -1;
        if (pid == 0)
            pid = -1;
        fclose(f);
    }
    
    // Check whether the pid represents a valid process
    if (pid != -1 && 0 != kill(pid, 0))
        pid = -1;
    
    return pid;
}


static pid_t
DeletePreexistingPidFile(void)
{
    // Try to read the pid from the pid file
    pid_t pid = -1;
    FILE* f = fopen(pidFile, "r");
    if (f != NULL)
    {
        if (1 != fscanf(f, "%d", &pid))
            pid = -1;
        if (pid == 0)
            pid = -1;
        fclose(f);
    }
    
    // Check whether the pid represents a valid process
    int valid = (pid != -1 && 0 != kill(pid, 0));
    
    // Log information about the discovered pid file
    if (verbosity >= 3 && pid != -1) {
        LogMessage("Discovered preexisting pidfile %s containing pid %d which is a %s process\n", pidFile, pid,
            (valid) ? "valid" : "invalid");
    }

    // Try to delete the pidfile if it's present
    if (pid != -1) {
        if (unlink(pidFile)) {
            if (verbosity >= 3)
                LogMessage("Error %d while trying to cleanup preexisting pidfile %s\n", errno, pidFile);
        } else {
            if (verbosity >= 3)
                LogMessage("Deleted preexisting pidfile %s\n", pidFile);
        }
    }

    return pid;
}


static pid_t
WaitForValidPidFile(void)
{
    CFAbsoluteTime patience = CFAbsoluteTimeGetCurrent() + kChildStartPidTimeout;
    
    // Poll for a child process and pidfile to be generated, until we lose patience.
    pid_t pid = -1;
    while ((pid = CheckForValidPidFile()) == -1 && (patience - CFAbsoluteTimeGetCurrent() > 0))
        sleep(1);
        
    if (verbosity >= 3)
        LogMessage("Discovered pid %d from pidfile %s\n", pid, pidFile);

    return pid;
}



static void
MonitorChild(pid_t childPid)
{
    runningPid = childPid;
    
    if (runningPid != 0 && runningPid != -1) {
        if (verbosity >=3 )
            LogMessage("Start monitoring of pid %d via kevent\n", runningPid);
        
        // Monitor the process deaths for that pid
        struct kevent ke;
        EV_SET(&ke, childPid, EVFILT_PROC, EV_ADD | EV_ONESHOT, NOTE_EXIT, 0, NULL);
        if (-1 == kevent(kqfd, &ke, 1, NULL, 0, NULL))
            LogMessage("Could not monitor kevent for pid %d (%d)\n", runningPid, errno);
    }
}


static void
UnmonitorChild(void)
{
    runningPid = 0;
}


static int
MonitoringChild(void)
{
    return runningPid != 0;
}


static void
ProcessChildDeath(pid_t childPid)
{
    // Take special note if process runningPid dies
    if (runningPid != 0 && runningPid != -1 && childPid == runningPid)
    {
        if (verbosity >= 1)
            LogMessage("Target process %d has died\n", childPid);
            
        UnmonitorChild();
        DestroyPidFile();
        
        CFRunLoopStop(CFRunLoopGetCurrent());
    }
}


static void
WaitChildDeath(pid_t childPid)
{
    // Wait for the death of a particular child
    int wait_result = 0;
    int wait_stat = 0;
    
    // Set up a timer for how long we'll wait for child death before we
    // kill the child outright with SIGKILL (infanticide)
    CFAbsoluteTime patience = CFAbsoluteTimeGetCurrent() + kChildDeathTimeout;
    
    // Wait for the death of child, calling into our run loop if it's not dead yet.
    // Note that the wait may actually be processed by our runloop callback, in which
    // case the wait here will simply return -1.
    while ((wait_result = wait4(childPid, &wait_stat, WNOHANG, NULL)) == 0)
    {
        CFTimeInterval patienceRemaining = patience - CFAbsoluteTimeGetCurrent();
        if (patienceRemaining > 0)
            CFRunLoopRunInMode(kChildWatchMode, patienceRemaining, true);
        else
        {
            // We've run out of patience; kill the child with SIGKILL
            if (verbosity >= 3)
                LogMessage("Child %d didn't die; Killing with SIGKILL.\n", childPid);
            
            if (0 != kill(childPid, SIGKILL))
            {
                if (verbosity >= 3)
                    LogMessage("Attempt to kill process %d failed.\n", childPid);
            }
        }
    }
    
    // The child should be dead and gone by now.
    ProcessChildDeath(childPid);
}


static void
CheckChildren(void)
{
    // Process any pending child deaths
    int wait_stat = 0;
    pid_t pid = 0;
    while ((pid = wait4(0, &wait_stat, WNOHANG, NULL)) != 0 && pid != -1)
        ProcessChildDeath(pid);
}


static pid_t
Exec(const char* const argv[], int sync)
{
    if (!argv || !argv[0] || !*argv[0])
        return -1;
        
    pid_t pid = fork();
    switch (pid)
    {
    case 0:
        // In the child process
        {           
            // Child process has no stdin, but shares stdout and stderr with us
            // Is that the right behavior?
            int nullfd = 0;
            if ((nullfd = open("/dev/null", O_RDONLY)) == -1)
                _exit(1);
            dup2(nullfd, STDIN_FILENO);

            // Launch the child
            execvp(argv[0], (char* const*)argv);
            
            // We get here only if the exec fails.
            LogMessage("Unable to launch process %s.\n", argv[0]);
            _exit(1);
        }
        /*NOTREACHED*/
    
    case -1:
        // error starting child process
        LogMessage("Unable to fork child process %s.\n", argv[0]);
        break;
    
    default:
        // In the original process
        if (sync)
        {
            // If synchronous, wait for the child process to complete
            WaitChildDeath(pid);
            pid = 0;
        }
        break;
    }
    
    return pid;
}


static int
Start(void)
{   
    char buf[1024];
    
    if (!startArgs || !startArgs[0])
    {
        LogMessage("There is nothing to start. No start-cmd was specified\n");
        return 2;
    }
    
    if (verbosity >= 1)
        LogMessage("Starting process\n");
    if (pidFile != NULL)
        DeletePreexistingPidFile();
    if (verbosity >= 2)
        LogMessage("Running start-cmd %s\n", CatArray(startArgs, buf, sizeof(buf)));
        
    // Exec the start-cmd
    pid_t pid = Exec(startArgs, pidStyle == kPidStyleNone);
    
    // Process error during Exec
    if (pid == -1)
    {
        if (verbosity >= 2)
            LogMessage("Error running start-cmd %s\n", CatArray(startArgs, buf, sizeof(buf)));
        if (verbosity >= 1)
            LogMessage("error while starting\n");
        return 2;
    }
    
    // Try to discover the pid of the running process
    switch (pidStyle)
    {
    case kPidStyleNone:         // The command should have completed: we have no pid (should be zero)
        pid = -1;
        break;
        
    case kPidStyleExec:         // The pid comes from the Exec
        break;
        
    case kPidStyleFileAuto:     // Poll pid from the pidfile
    case kPidStyleFileClean:
        pid = WaitForValidPidFile();
        if (pid == -1)
        {
            if (verbosity >= 2)
                LogMessage("Error; expected pidfile not found following Exec of start-cmd %s\n", CatArray(startArgs, buf, sizeof(buf)));
            if (verbosity >= 1)
                LogMessage("error while starting\n");
            return 2;
        }
        break;
        
    default:
        break;
    }
    
    // If we have a pid, then begin tracking it
    MonitorChild(pid);
    if (pid != 0 && pid != -1)
    {
        if (verbosity >= 1)
            LogMessage("Target process id is %d\n", pid);

        // Create a pid file if we need to      
        CreatePidFile();
    }
    
    return 0;
}


static int
Stop(void)
{
    char buf[1024];

    pid_t pid;
    if (!stopArgs || !stopArgs[0])
    {
        // We don't have a stop command, so we try to kill the process
        // we're tracking with runningPid
        if ((pid = runningPid) != 0 && pid != -1)
        {
            if (verbosity >= 1)
                LogMessage("Stopping process %d\n", pid);
            
            // Send the process a SIGTERM to ask it to quit
            kill(pid, SIGTERM);
            
            // Wait for process to quit, killing it after a timeout
            WaitChildDeath(pid);
        }
        else
        {
            if (verbosity >= 1)
                LogMessage("process was already stopped\n");
        }
    }
    else
    {
        // We have a stop-cmd to use. We execute it synchronously,
        // and trust it to do the job.
        if (verbosity >= 1)
            LogMessage("Stopping process\n");
        if (verbosity >= 2)
            LogMessage("Running stop-cmd %s\n", CatArray(stopArgs, buf, sizeof(buf)));
        pid = Exec(stopArgs, TRUE);
        if (pid == -1)
        {
            if (verbosity >= 2)
                LogMessage("Error while running stop-cmd %s\n", CatArray(stopArgs, buf, sizeof(buf)));
            if (verbosity >= 1)
                LogMessage("error stopping process\n");
            return 2;
        }

        // We've executed stop-cmd, so we assume any runningPid process is gone
        UnmonitorChild();
        DestroyPidFile();
    }
    
    return 0;
}


static int
Restart(void)
{
    char buf[1024];

    if (!restartArgs || !restartArgs[0])
    {
        // We weren't given a restart command, so just use stop/start
        if (verbosity >= 1)
            LogMessage("Restarting process\n");
            
        // Stop the process
        Stop();
        
        // Delay for a restartWait seconds to allow other process support to stabilize
        // (This gives a chance for other processes that might be monitoring the process,
        // for instance, to detect its death and cleanup).
        sleep(restartWait);
        
        // Start it again
        Start();
    }
    else
    {
        // Bug: we should recapture the target process id from the pidfile in this case

        // Execute the restart-cmd and trust it to do the job
        if (verbosity >= 1)
            LogMessage("Restarting process\n");
        if (verbosity >= 2)
            LogMessage("Running restart-cmd %s\n", CatArray(restartArgs, buf, sizeof(buf)));
            
        pid_t pid = Exec(restartArgs, TRUE);
        if (pid == -1)
        {
            if (verbosity >= 2)
                LogMessage("Error running restart-cmd %s\n", CatArray(restartArgs, buf, sizeof(buf)));
            if (verbosity >= 1)
                LogMessage("error restarting process\n");
            return 2;
        }
    }
    
    return 0;
}


static void
ScheduledRestartCallback(CFRunLoopTimerRef timer UNUSED, void *info UNUSED)
{
    if (verbosity >= 3)
        LogMessage("Scheduled restart time has arrived.\n");
        
    // Our scheduled restart fired, so restart now
    Restart();
}


static void
CancelScheduledRestart(void)
{
    // Kill off any existing timer
    if (restartTimer)
    {
        if (CFRunLoopTimerIsValid(restartTimer))
            CFRunLoopTimerInvalidate(restartTimer);
        CFRelease(restartTimer);
        restartTimer = NULL;
    }
}


static void
ScheduleRestartForTime(CFAbsoluteTime absoluteTime)
{
    // Cancel any currently scheduled restart
    CancelScheduledRestart();
    
    // Schedule a new restart
    restartTimer = CFRunLoopTimerCreate(NULL, absoluteTime, 0, 0, 0, ScheduledRestartCallback, NULL);
    if (restartTimer)
        CFRunLoopAddTimer(CFRunLoopGetCurrent(), restartTimer, kCFRunLoopDefaultMode);
}


static void
ScheduleDelayedRestart(void)
{
    // The hysteresis here allows us to take multiple restart requests within a small
    // period of time, and coalesce them together into only one. It also allows for
    // a certain amount of "slop time" for things to stabilize following whatever
    // event is triggering the restart.
    if (verbosity >= 3)
        LogMessage("Scheduling restart %f seconds in future.\n", restartHysteresis);
    ScheduleRestartForTime(CFAbsoluteTimeGetCurrent() + restartHysteresis);
}


static void
DynamicStoreChanged(
                    SCDynamicStoreRef   store UNUSED,
                    CFArrayRef          changedKeys,
                    void                *info UNUSED
                    )
{
    if (verbosity >= 3)
    {
        char bigBuf[1024];
        *bigBuf = '\0';
        
        CFIndex cnt = CFArrayGetCount(changedKeys);
        CFIndex i;
        for (i = 0; i < cnt; ++i)
        {
            char buf[256];
            CFStringRef value = CFArrayGetValueAtIndex(changedKeys, i);
            CFStringGetCString(value, buf, sizeof(buf), kCFStringEncodingUTF8);
            if (i > 0)
                strlcat(bigBuf, ", ", sizeof(bigBuf));
            strlcat(bigBuf, buf, sizeof(bigBuf));
        }

        LogMessage("Restarting daemon because of the following changes in the dynamic store: %s\n", bigBuf);
    }
    
    ScheduleDelayedRestart();
}


static void
PowerCallBack(void *x UNUSED, io_service_t y UNUSED, natural_t messageType, void *messageArgument)
{
    switch (messageType)
    {
    case kIOMessageSystemWillSleep:
    case kIOMessageCanSystemSleep:
        /*  Power Manager waits for your reply via one of these functions for up
        to 30 seconds. If you don't acknowledge the power change by calling
        IOAllowPowerChange(), you'll delay sleep by 30 seconds. */
        IOAllowPowerChange(pwrRootPort, (long)messageArgument);
        break;
    case kIOMessageSystemHasPoweredOn:
        if (restartOnWakeup)
        {
            if (verbosity >= 3)
                LogMessage("Restarting daemon because of system wake from sleep\n");
            ScheduleDelayedRestart();
        }
        break;
    }
}


static void
NotificationCenterCallback(
                                CFNotificationCenterRef center UNUSED,
                                void *observer UNUSED,
                                CFStringRef name,
                                const void *object UNUSED,
                                CFDictionaryRef userInfo UNUSED)
{
    if (verbosity >= 3)
    {
        char buf[256];
        CFStringGetCString(name, buf, sizeof(buf), kCFStringEncodingUTF8);
        LogMessage("Restarting daemon due to receipt of the notification %s\n", buf);
    }
        
    ScheduleDelayedRestart();
}


static void
SignalCallback(CFMachPortRef port UNUSED, void *msg, CFIndex size UNUSED, void *info UNUSED)
{
    mach_msg_header_t* hdr = (mach_msg_header_t*)msg;
    switch (hdr->msgh_id)
    {
    case SIGTERM:       
        // On receipt of SIGTERM we set our terminate flag and stop the process
        if (!terminating)
        {
            terminating = true;
            if (verbosity >= 1)
                LogMessage("SIGTERM received\n");
            Stop();
        }
        break;
    
    case SIGHUP:
        if (verbosity >= 1)
            LogMessage("SIGHUP received\n");
        if (!terminating)
            Restart();
        break;
        
    case SIGCHLD:
        CheckChildren();
        break;
        
    default:
        break;
    }
}


static void KQueueCallBack (CFSocketRef socketRef, CFSocketCallBackType type UNUSED,
             CFDataRef address UNUSED, const void *data UNUSED, void *context UNUSED)
{
    int fd = CFSocketGetNative(socketRef);
    
    struct kevent event;
    memset(&event, 0x00, sizeof(struct kevent));
    
    if (kevent(fd, NULL, 0, &event, 1, NULL) == -1) {
        LogMessage("Couldn't get kevent.  Error %d/%s\n", errno, strerror(errno));
    } else {
        if (event.fflags & NOTE_EXIT) {
        
            pid_t pid = event.ident;
            
            if (verbosity >= 3)
                LogMessage("Received kevent: pid %d has exited\n", pid);
                
            ProcessChildDeath(pid);
        } else
            LogMessage("Unexpected kevent received: %d\n", event.fflags);
    }

}


static void
AddNotificationToCenter(const void* value, void* context)
{
    CFNotificationCenterAddObserver((CFNotificationCenterRef)context,
        kProgramName,
        NotificationCenterCallback,
        value,      // name of notification
        NULL,       // object to observe
        CFNotificationSuspensionBehaviorDeliverImmediately);
}


static void handle_child_signal(int sig)
{
    // Because there's a limited environment in which we can operate while
    // handling a signal, we send a mach message to our run loop, and handle
    // things from there.
    mach_msg_header_t header;
    header.msgh_bits        = MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0);
    header.msgh_size        = sizeof(header);
    header.msgh_remote_port = sigChild_m_port;
    header.msgh_local_port  = MACH_PORT_NULL;
    header.msgh_reserved    = 0;
    header.msgh_id          = sig;
    
    mach_msg_return_t status = mach_msg_send(&header);
    if (status != 0) {
        LogMessage("mach_msg_send failed in handle_child_signal!\n");
    }
}


static void handle_generic_signal(int sig)
{
    // Because there's a limited environment in which we can operate while
    // handling a signal, we send a mach message to our run loop, and handle
    // things from there.
    mach_msg_header_t header;
    header.msgh_bits        = MACH_MSGH_BITS(MACH_MSG_TYPE_MAKE_SEND, 0);
    header.msgh_size        = sizeof(header);
    header.msgh_remote_port = sigGeneric_m_port;
    header.msgh_local_port  = MACH_PORT_NULL;
    header.msgh_reserved    = 0;
    header.msgh_id          = sig;
    
    mach_msg_return_t status = mach_msg_send(&header);
    if (status != 0) {
        LogMessage("mach_msg_send failed in handle_generic_signal!\n");
    }
}


static int
MainLoop(void)
{
    // *** TODO: This routine needs more error checking
    
    int status = 0;
    
    if (verbosity >= 3)
        LogMessage("Initializing; daemondo pid is %d\n", getpid());
    
    // === Setup Notifications of Changes to System Configuration ===
    // Create a new SCDynamicStore session and an associated runloop source, adding it default mode
    SCDynamicStoreRef   dsRef           = SCDynamicStoreCreate(NULL, kProgramName, DynamicStoreChanged, NULL);
    CFRunLoopSourceRef  dsSrc           = SCDynamicStoreCreateRunLoopSource(NULL, dsRef, 0);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), dsSrc, kCFRunLoopDefaultMode);
    
    // Tell the DynamicStore which keys to notify us on: this is the set of keys on which the
    // daemon will be restarted, at least for now--we may want to give more flexibility at some point.
    (void) SCDynamicStoreSetNotificationKeys(dsRef, NULL, scRestartPatterns);
    
    
    // === Setup Notifications from Notification Centers  ===
    CFArrayApplyFunction(distNotifyNames, CFRangeMake(0, CFArrayGetCount(distNotifyNames)),
        AddNotificationToCenter, CFNotificationCenterGetDistributedCenter());
    CFArrayApplyFunction(darwinNotifyNames, CFRangeMake(0, CFArrayGetCount(darwinNotifyNames)),
        AddNotificationToCenter, CFNotificationCenterGetDarwinNotifyCenter());


    // === Setup Notifications of Changes to System Power State ===
    // Register for system power notifications, adding a runloop source to handle then
    IONotificationPortRef   powerRef = NULL;
    io_object_t             pwrNotifier = 0;
    pwrRootPort = IORegisterForSystemPower(0, &powerRef, PowerCallBack, &pwrNotifier);
    if (pwrRootPort != 0)
        CFRunLoopAddSource(CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(powerRef), kCFRunLoopDefaultMode);
        
    
    // === Setup Notifications of Signals ===
    // Add a mach port source to our runloop for handling of the signals
    CFMachPortRef       sigChildPort    = CFMachPortCreate(NULL, SignalCallback, NULL, NULL);
    CFMachPortRef       sigGenericPort  = CFMachPortCreate(NULL, SignalCallback, NULL, NULL);
    
    CFRunLoopSourceRef  sigChildSrc     = CFMachPortCreateRunLoopSource(NULL, sigChildPort, 0);
    CFRunLoopSourceRef  sigGenericSrc   = CFMachPortCreateRunLoopSource(NULL, sigGenericPort, 0);
    
    
    // === Setup kevent notifications of process death
    kqfd = kqueue();
    CFSocketRef kqSocket                = CFSocketCreateWithNative(NULL,  kqfd,
                                            kCFSocketReadCallBack, KQueueCallBack, NULL);
    CFRunLoopSourceRef  kqueueSrc       = CFSocketCreateRunLoopSource(NULL, kqSocket, 0);   
    
    
    // Add only the child signal sources to the childwatch mode
    CFRunLoopAddSource(CFRunLoopGetCurrent(), sigChildSrc, kChildWatchMode);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), kqueueSrc, kChildWatchMode);
    
    // Add both child and generic signal sources to the default mode
    CFRunLoopAddSource(CFRunLoopGetCurrent(), sigChildSrc, kCFRunLoopDefaultMode);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), kqueueSrc, kCFRunLoopDefaultMode);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), sigGenericSrc, kCFRunLoopDefaultMode);
    
    // Install signal handlers
    sigChild_m_port     = CFMachPortGetPort(sigChildPort);
    sigGeneric_m_port   = CFMachPortGetPort(sigGenericPort);

    signal(SIGCHLD, handle_child_signal);
    signal(SIGTERM, handle_generic_signal);
    signal(SIGHUP, handle_generic_signal);
    
    
    // === Core Loop ===
    // Start the daemon
    status = Start();
    
    if (verbosity >= 3)
        LogMessage("Start event loop\n");
    
    // Run the run loop until we stop it, or until the process we're tracking stops
    while (status == 0 && !terminating && MonitoringChild())
        CFRunLoopRunInMode(kCFRunLoopDefaultMode, 99999999.0, true);
        
    if (verbosity >= 3)
        LogMessage("End event loop\n");
    
        
    // === Tear Down (we don't really need to do all of this) ===
    // The daemon should by now have either been stopped, or stopped of its own accord
        
    // Remove signal handlers
    signal(SIGTERM, SIG_DFL);
    signal(SIGHUP, SIG_DFL);
    signal(SIGCHLD, SIG_DFL);
    
    sigChild_m_port = 0;
    sigGeneric_m_port = 0;
    
    // Remove run loop sources
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), sigChildSrc, kChildWatchMode);
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), kqueueSrc, kChildWatchMode);
    
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), sigChildSrc, kCFRunLoopDefaultMode);
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), kqueueSrc, kCFRunLoopDefaultMode);
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), sigGenericSrc, kCFRunLoopDefaultMode);
    
    // Tear down signal handling infrastructure
    CFRelease(sigChildSrc);
    CFRelease(sigGenericSrc);
    
    CFRelease(sigChildPort);
    CFRelease(sigGenericPort);
    
    // Tear down kqueue infrastructure
    CFRelease(kqueueSrc);
    CFRelease(kqSocket);
    close(kqfd);

    // Tear down DynamicStore stuff
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), dsSrc, kCFRunLoopDefaultMode);
    
    CFRelease(dsSrc);
    CFRelease(dsRef);
    
    // Tear down notifications from Notification Center
    CFNotificationCenterRemoveEveryObserver(CFNotificationCenterGetDistributedCenter(), kProgramName);
    CFNotificationCenterRemoveEveryObserver(CFNotificationCenterGetDarwinNotifyCenter(), kProgramName);

    // Tear down power management stuff
    CFRunLoopRemoveSource(CFRunLoopGetCurrent(), IONotificationPortGetRunLoopSource(powerRef), kCFRunLoopDefaultMode);
    IODeregisterForSystemPower(&pwrNotifier);
    
    if (verbosity >= 3)
        LogMessage("Terminating\n");
    
    return status;  
}


static int
CollectCmdArgs(char* arg1, int argc, char* const argv[], const char * const ** args)
{
    // Count the number of additional arguments up until end of args or the marker argument ";"
    int moreArgs = 0;
    for (; moreArgs < argc && 0 != strcmp(";", argv[moreArgs]); ++moreArgs)
        ;
        
    // We were given one argument for free
    int nargs = moreArgs + 1;
        
    // Allocate an array for the arguments
    *args = calloc(sizeof(char**), nargs+1);
    if (!*args)
        return 0;
        
    // Copy the arguments into our new array
    (*(char***)args)[0] = arg1;
    
    int i;
    for (i = 0; i < moreArgs; ++i)
        (*(char***)args)[i+1] = argv[i];
        
    // NULL-terminate the argument array
    (*(char***)args)[nargs] = NULL;
    
    // Return number of args we consumed, accounting for potential trailing ";"
    return (moreArgs == argc) ? moreArgs : moreArgs + 1;
}


static void
AddSingleArrayArg(const char* arg, CFMutableArrayRef array)
{
    CFStringRef s = CFStringCreateWithCString(NULL, arg, kCFStringEncodingUTF8);
    CFArrayAppendValue(array, s);
    CFRelease(s);
}


static int
CollectArrayArgs(char* arg1, int argc, char* const argv[], CFMutableArrayRef array)
{
    // Let CollectCmdArgs do the grunt work
    const char* const* args = NULL;
    int argsUsed = CollectCmdArgs(arg1, argc, argv, &args);
    
    // Add arguments to the mutable array
    if (args != NULL)
    {
        const char* const* argp = args;
        for (; *argp != NULL; ++argp)
            AddSingleArrayArg(*argp, array);
        free((void*)args);
    }
    
    return argsUsed;
}


enum {
    kVerbosityOpt           = 256,
    kRestartConfigOpt,
    kRestartDistNotifyOpt,
    kRestartDarwinNotifyOpt,
    kRestartWakeupOpt,
    kRestartNetChangeOpt,
    kPidOpt,
    kPidFileOpt,
    kRestartHysteresisOpt,
    kRestartWaitOpt
};


int
main(int argc, char* argv[])
{
    int status = 0;
    
    // Initialization
    kProgramName        = CFSTR("daemondo");
    kChildWatchMode     = CFSTR("ChildWatch");      // A runloop mode
    
    scRestartPatterns   = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
    distNotifyNames     = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
    darwinNotifyNames   = CFArrayCreateMutable(NULL, 0, &kCFTypeArrayCallBacks);
    
    // Make stdout flush after every line
    setvbuf(stdout, (char *)NULL, _IOLBF, 0);
    
    // Process arguments
    static struct option longopts[] = {
            // Start/Stop/Restart the process
        { "start-cmd",      required_argument,      0,              's' },
        { "stop-cmd",       required_argument,      0,              'k' },
        { "restart-cmd",    required_argument,      0,              'r' },
        
            // Dynamic Store Keys to monitor
        { "restart-config", required_argument,      0,              kRestartConfigOpt },
        
            // Notifications to monitor
        { "restart-dist-notify",
                            required_argument,      0,              kRestartDistNotifyOpt },
        { "restart-darwin-notify",
                            required_argument,      0,              kRestartDarwinNotifyOpt },
        
            // Control over behavior on power state
        { "restart-wakeup", no_argument,            0,              kRestartWakeupOpt },

            // Short-cuts
        { "restart-netchange",
                            no_argument,            0,              kRestartNetChangeOpt },
                            
            // Pid-files
        { "pid",            required_argument,      0,              kPidOpt },
        { "pidfile",        required_argument,      0,              kPidFileOpt },
        
            // other
        { "help",           no_argument,            0,              'h' },
        { "v",              no_argument,            0,              'v' },
        { "verbosity",      optional_argument,      0,              kVerbosityOpt },
        { "version",        no_argument,            0,              'V' },
        { "label",          required_argument,      0,              'l' },
        { "restart-hysteresis",
                            required_argument,      0,              kRestartHysteresisOpt },
        { "restart-wait",
                            required_argument,      0,              kRestartWaitOpt },
        
        { 0,                0,                      0,              0 }
    };

    while (status == 0 && optind < argc)
    {
        int optindex = 0;
        int ret = getopt_long(argc, argv, ":s:k:r:l:hvV", longopts, &optindex);
        int opt = ret;
        switch (opt)
        {
        case ':':
            printf("Option error: missing argument for option %s\n", longopts[optindex].name);
            exit(1);
            /*NOTREACHED*/
            
        case 's':
            if (startArgs)
            {
                printf("Option error: start-cmd option may be given only once.\n");
                exit(1);
            }
            else
            {
                optind += CollectCmdArgs(optarg, argc - optind, argv + optind, &startArgs);
                optreset = 1;
            }
            break;
            
        case 'k':
            if (stopArgs)
            {
                printf("Option error: stop-cmd option may be given only once.\n");
                exit(1);
            }
            else
            {
                optind += CollectCmdArgs(optarg, argc - optind, argv + optind, &stopArgs);
                optreset = 1;
            }
            break;

        case 'r':
            if (restartArgs)
            {
                printf("Option error: restart-cmd option may be given only once.\n");
                exit(1);
            }
            else
            {
                optind += CollectCmdArgs(optarg, argc - optind, argv + optind, &restartArgs);
                optreset = 1;
            }
            break;
            
        case kRestartConfigOpt:
            optind += CollectArrayArgs(optarg, argc - optind, argv + optind, scRestartPatterns);
            optreset = 1;
            break;
            
        case kRestartDistNotifyOpt:
            optind += CollectArrayArgs(optarg, argc - optind, argv + optind, distNotifyNames);
            optreset = 1;
            break;
            
        case kRestartDarwinNotifyOpt:
            optind += CollectArrayArgs(optarg, argc - optind, argv + optind, darwinNotifyNames);
            optreset = 1;
            break;
            
        case kRestartWakeupOpt:
            restartOnWakeup = TRUE;
            break;
            
        case kRestartNetChangeOpt:
            AddSingleArrayArg("com.apple.system.config.network_change", darwinNotifyNames);
            break;
            
        case kRestartHysteresisOpt:
            restartHysteresis = strtof(optarg, NULL);
            if (restartHysteresis < 0)
                restartHysteresis = 0;
            break;
            
        case kRestartWaitOpt:
            restartWait = strtol(optarg, NULL, 10);
            if (restartWait < 0)
                restartWait = 0;
            break;
            
        case kPidOpt:
            if      (0 == strcasecmp(optarg, "none"))
                pidStyle = kPidStyleNone;
            else if (0 == strcasecmp(optarg, "exec"))
                pidStyle = kPidStyleExec;
            else if (0 == strcasecmp(optarg, "fileauto"))
                pidStyle = kPidStyleFileAuto;
            else if (0 == strcasecmp(optarg, "fileclean"))
                pidStyle = kPidStyleFileClean;
            else {
                status = 1;
                LogMessage("Unexpected pid style %s\n", optarg);
            }
            break;
        
        case kPidFileOpt:
            if (pidFile != NULL)
                free((char*)pidFile);
            pidFile = strdup(optarg);
            break;
        
        case 'h':
            DoHelp();
            exit(0);
            /*NOTREACHED*/
            
        case 'l':
            if (label != NULL)
                free((char*)label);
            label = strdup(optarg);
            break;
            
        case 'v':
            ++verbosity;
            break;
        
        case kVerbosityOpt:
            if (optarg)
                verbosity = strtol(optarg, NULL,  10);
            else
                ++verbosity;
            break;
    
        case 'V':
            DoVersion();
            break;
            
        default:
            LogMessage("unexpected parameter: %s\n", argv[optind]);
            status = 1;
            break;
        }
    }
    
    // Default the pid style if it wasn't given
    if (pidStyle == kPidStyleUnknown)
    {
        if (startArgs && !stopArgs && !restartArgs)
            pidStyle = kPidStyleExec;
        else
            pidStyle = kPidStyleNone;
    }
    
    // Go into our main loop
    if (status == 0 && startArgs)
        status = MainLoop();
    else
        printf("use option --help for help\n");
        
    return status;
}
