/*
 * Copyright (c) 2014, 2017 Nicira, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at:
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <config.h>
#include "daemon.h"
#include "daemon-private.h"
#include <stdio.h>
#include <io.h>
#include <stdlib.h>
#include <unistd.h>
#include "dirs.h"
#include "fatal-signal.h"
#include "ovs-thread.h"
#include "openvswitch/poll-loop.h"
#include "openvswitch/vlog.h"

VLOG_DEFINE_THIS_MODULE(daemon_windows);

/* Constants for flock function */
#define	LOCK_SHARED	0x0                     /* Shared lock. */
#define	LOCK_UNLOCK	0x80000000              /* Unlock. Custom value. */

static bool service_create;          /* Was --service specified? */
static bool service_started;         /* Have we dispatched service to start? */

/* --service-monitor: Should the service be restarted if it dies
 * unexpectedly? */
static bool monitor;

bool detach;                 /* Was --detach specified? */
static bool detached;        /* Running as the child process. */
static HANDLE write_handle;  /* End of pipe to write to parent. */

char *pidfile;                 /* --pidfile: Name of pidfile (null if none). */
static FILE *filep_pidfile;    /* File pointer to access the pidfile. */

/* Handle to the Services Manager and the created service. */
static SC_HANDLE manager, service;

/* Handle to the status information structure for the current service. */
static SERVICE_STATUS_HANDLE hstatus;

/* Hold the service's current status. */
static SERVICE_STATUS service_status;

/* Handle to an event object used to wakeup from poll_block(). */
static HANDLE wevent;

/* Hold the arguments sent to the main function. */
static int sargc;
static char ***sargvp;

static void check_service(void);
static void handle_scm_callback(void);
static void init_service_status(void);
static void set_config_failure_actions(void);

static bool detach_process(int argc, char *argv[]);

extern int main(int argc, char *argv[]);

void
daemon_usage(void)
{
    printf(
        "\nService options:\n"
        "  --service               run in background as a service.\n"
        "  --service-monitor       restart the service in case of an "
                                   "unexpected failure. \n");
}

/* Sets up a following call to service_start() to detach from the foreground
 * session, running this process in the background.  */
void
set_detach(void)
{
    detach = true;
}

/* Registers the call-back and configures the actions in case of a failure
 * with the Windows services manager. */
void
service_start(int *argcp, char **argvp[])
{
    int argc = *argcp;
    char **argv = *argvp;
    int i;
    SERVICE_TABLE_ENTRY service_table[] = {
        {(LPTSTR)program_name, (LPSERVICE_MAIN_FUNCTION)main},
        {NULL, NULL}
    };

    /* If one of the command line option is "--detach", we create
     * a new process in case of parent, wait for child to start and exit.
     * In case of the child, we just return. We should not be creating a
     * service in either case. */
    if (detach_process(argc, argv)) {
        return;
    }

    /* 'service_started' is 'false' when service_start() is called the first
     * time.  It is 'true', when it is called the second time by the Windows
     * services manager. */
    if (service_started) {
        init_service_status();

        wevent = CreateEvent(NULL, TRUE, FALSE, NULL);
        if (!wevent) {
            char *msg_buf = ovs_lasterror_to_string();
            VLOG_FATAL("Failed to create a event (%s).", msg_buf);
        }

        poll_wevent_wait(wevent);

        /* Register the control handler. This function is called by the service
         * manager to stop the service. */
        hstatus = RegisterServiceCtrlHandler(program_name,
                                         (LPHANDLER_FUNCTION)control_handler);
        if (!hstatus) {
            char *msg_buf = ovs_lasterror_to_string();
            VLOG_FATAL("Failed to register the service control handler (%s).",
                        msg_buf);
        }

        if (monitor) {
            set_config_failure_actions();
        }

        /* When the service control manager does the call back, it does not
         * send the same arguments as sent to the main function during the
         * service start. So, use the arguments passed over during the first
         * time. */
        *argcp = sargc;
        *argvp = *sargvp;

        /* Enable default error mode so we can take advantage of WER
         * (Windows Error Reporting) crash dumps.
         * Being a service it does not allow for WER window pop-up.
         * XXX implement our on crash dump collection mechanism. */
        SetErrorMode(0);

        return;
    }

    assert_single_threaded();

    /* A reference to arguments passed to the main function the first time.
     * We need it after the call-back from service control manager. */
    sargc = argc;
    sargvp = argvp;

    /* We are only interested in the '--service' and '--service-monitor'
     * options before the call-back from the service control manager. */
    for (i = 0; i < argc; i ++) {
        if (!strcmp(argv[i], "--service")) {
            service_create = true;
        } else if (!strcmp(argv[i], "--service-monitor")) {
            monitor = true;
        }
    }

    /* If '--service' is not a command line option, run in foreground. */
    if (!service_create) {
        return;
    }

    /* If we have been configured to run as a service, then that service
     * should already have been created either manually or through a start up
     * script. */
    check_service();

    service_started = true;

    /* StartServiceCtrlDispatcher blocks and returns after the service is
     * stopped. */
    if (!StartServiceCtrlDispatcher(service_table)) {
        char *msg_buf = ovs_lasterror_to_string();
        VLOG_FATAL("Failed at StartServiceCtrlDispatcher (%s)", msg_buf);
    }
    exit(0);
}

/* This function is registered with the Windows services manager through
 * a call to RegisterServiceCtrlHandler() and will be called by the Windows
 * services manager asynchronously to stop the service. */
void
control_handler(DWORD request)
{
    switch (request) {
    case SERVICE_CONTROL_STOP:
    case SERVICE_CONTROL_SHUTDOWN:
        service_status.dwCurrentState = SERVICE_STOPPED;
        service_status.dwWin32ExitCode = NO_ERROR;
        SetEvent(wevent);
        SetServiceStatus(hstatus, &service_status);
        break;

    default:
        break;
    }
}

/* Return 'true' if the Windows services manager has called the
 * control_handler() and asked the program to terminate. */
bool
should_service_stop(void)
{
    if (service_started) {
        if (service_status.dwCurrentState != SERVICE_RUNNING) {
            return true;
        } else {
            poll_wevent_wait(wevent);
        }
    }
    return false;
}

/* Set the service as stopped. The control manager will terminate the
 * service soon after this call. Hence, this should ideally be the last
 * call before termination. */
void
service_stop()
{
    if (!service_started) {
        return;
    }
    fatal_signal_atexit_handler();

    ResetEvent(wevent);
    CloseHandle(wevent);

    service_status.dwCurrentState = SERVICE_STOPPED;
    service_status.dwWin32ExitCode = NO_ERROR;
    SetServiceStatus(hstatus, &service_status);
}

/* Call this function to signal that the daemon is ready. init_service()
 * or control_handler() has already initalized/set the
 * service_status.dwCurrentState .*/
static void
service_complete(void)
{
    if (hstatus) {
        SetServiceStatus(hstatus, &service_status);
    }
}

/* Check whether 'program_name' has been created as a service. */
static void
check_service()
{
    /* Establish a connection to the local service control manager. */
    manager = OpenSCManager(NULL, NULL, SC_MANAGER_ENUMERATE_SERVICE);
    if (!manager) {
        char *msg_buf = ovs_lasterror_to_string();
        VLOG_FATAL("Failed to open the service control manager (%s).",
                   msg_buf);
    }

    service = OpenService(manager, program_name, SERVICE_ALL_ACCESS);
    if (!service) {
        char *msg_buf = ovs_lasterror_to_string();
        VLOG_FATAL("Failed to open service (%s).", msg_buf);
    }
}

/* Service status of a service can be checked asynchronously through
 * tools like 'sc' or through Windows services manager and is set
 * through a call to SetServiceStatus(). */
static void
init_service_status()
{
    /* The service runs in its own process. */
    service_status.dwServiceType = SERVICE_WIN32_OWN_PROCESS;

    /* The control codes the service accepts. */
    service_status.dwControlsAccepted = SERVICE_ACCEPT_STOP |
                                            SERVICE_ACCEPT_SHUTDOWN;

    /* Initialize the current state as SERVICE_RUNNING. */
    service_status.dwCurrentState = SERVICE_RUNNING;

    /* The exit code to indicate if there was an error. */
    service_status.dwWin32ExitCode = NO_ERROR;

    /* The checkpoint value the service increments periodically. Set as 0
     * as we do not plan to periodically increment the value. */
    service_status.dwCheckPoint = 0;

    /* The estimated time required for the stop operation in ms. */
    service_status.dwWaitHint = 1000;
}

/* In case of an unexpected termination, configure the action to be
 * taken. */
static void
set_config_failure_actions()
{
    /* In case of a failure, restart the process the first two times
     * After 'dwResetPeriod', the failure count is reset. */
    SC_ACTION fail_action[3] = {
        {SC_ACTION_RESTART, 0},
        {SC_ACTION_RESTART, 0},
        {SC_ACTION_NONE, 0}
    };
    SERVICE_FAILURE_ACTIONS service_fail_action;

    /* Reset failure count after (in seconds). */
    service_fail_action.dwResetPeriod = 10;

    /* Reboot message. */
    service_fail_action.lpRebootMsg = NULL;

    /* The command line of the process. */
    service_fail_action.lpCommand = NULL;

    /* Number of elements in 'fail_actions'. */
    service_fail_action.cActions = sizeof(fail_action)/sizeof(fail_action[0]);

    /* A pointer to an array of SC_ACTION structures. */
    service_fail_action.lpsaActions = fail_action;

    if (!ChangeServiceConfig2(service, SERVICE_CONFIG_FAILURE_ACTIONS,
                              &service_fail_action)) {
        char *msg_buf = ovs_lasterror_to_string();
        VLOG_FATAL("Failed to configure service fail actions (%s).", msg_buf);
    }
}

/* When a daemon is passed the --detach option, we create a new
 * process and pass an additional non-documented option called --pipe-handle.
 * Through this option, the parent passes one end of a pipe handle. */
void
set_pipe_handle(const char *pipe_handle)
{
    write_handle = (HANDLE) atoi(pipe_handle);
}

/* If one of the command line option is "--detach", creates
 * a new process in case of parent, waits for child to start and exits.
 * In case of the child, returns. */
static bool
detach_process(int argc, char *argv[])
{
    SECURITY_ATTRIBUTES sa;
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    HANDLE read_pipe, write_pipe;
    char *buffer;
    int error, i;
    char ch;

    /* We are only interested in the '--detach' and '--pipe-handle'. */
    for (i = 0; i < argc; i ++) {
        if (!detach && !strcmp(argv[i], "--detach")) {
            detach = true;
        } else if (!strncmp(argv[i], "--pipe-handle", 13)) {
            /* If running as a child, return. */
            detached = true;
            return true;
        }
    }

    /* Nothing to do if the option --detach is not set. */
    if (!detach) {
        return false;
    }

    /* Set the security attribute such that a process created will
     * inherit the pipe handles. */
    sa.nLength = sizeof(sa);
    sa.lpSecurityDescriptor = NULL;
    sa.bInheritHandle = TRUE;

    /* Create an anonymous pipe to communicate with the child. */
    error = CreatePipe(&read_pipe, &write_pipe, &sa, 0);
    if (!error) {
        VLOG_FATAL("CreatePipe failed (%s)", ovs_lasterror_to_string());
    }

    GetStartupInfo(&si);

    /* To the child, we pass an extra argument '--pipe-handle=write_pipe' */
    buffer = xasprintf("%s %s=%ld", GetCommandLine(), "--pipe-handle",
                       write_pipe);

    /* Create a detached child */
    error = CreateProcess(NULL, buffer, NULL, NULL, TRUE, DETACHED_PROCESS,
                          NULL, NULL, &si, &pi);
    if (!error) {
        VLOG_FATAL("CreateProcess failed (%s)", ovs_lasterror_to_string());
    }

    /* Close one end of the pipe in the parent. */
    CloseHandle(write_pipe);

    /* Block and wait for child to say it is ready. */
    error = ReadFile(read_pipe, &ch, 1, NULL, NULL);
    if (!error) {
        VLOG_FATAL("Failed to read from child (%s)",
                   ovs_lasterror_to_string());
    }
    /* The child has successfully started and is ready. */
    exit(0);
}

static void
flock(FILE* fd, int operation)
{
    HANDLE hFile;
    OVERLAPPED ov = {0};

    hFile = (HANDLE)_get_osfhandle(fileno(fd));
    if (hFile == INVALID_HANDLE_VALUE) {
        VLOG_FATAL("Failed to get PID file handle (%s).",
                   ovs_strerror(errno));
    }

    if (operation & LOCK_UNLOCK) {
        if (UnlockFileEx(hFile, 0, 1, 0, &ov) == 0) {
            VLOG_FATAL("Failed to unlock PID file (%s).",
                       ovs_lasterror_to_string());
        }
    } else {
       /* Use LOCKFILE_FAIL_IMMEDIATELY flag to avoid hang of another daemon that tries to
           acquire exclusive lock over the same PID file */
        if (LockFileEx(hFile, operation | LOCKFILE_FAIL_IMMEDIATELY,
                       0, 1, 0, &ov) == FALSE) {
            VLOG_FATAL("Failed to lock PID file (%s).",
                       ovs_lasterror_to_string());
        }
    }
}

static void
unlink_pidfile(void)
{
    if (filep_pidfile) {
        /* Remove the shared lock on file */
        flock(filep_pidfile, LOCK_UNLOCK);
        fclose(filep_pidfile);
    }
    if (pidfile) {
        unlink(pidfile);
    }
}

/* If a pidfile has been configured, creates it and stores the running
 * process's pid in it.  Ensures that the pidfile will be deleted when the
 * process exits. */
static void
make_pidfile(void)
{
    int error;

    error = GetFileAttributes(pidfile);
    if (error != INVALID_FILE_ATTRIBUTES) {
        /* pidfile exists. Try to unlink() it. */
        error = unlink(pidfile);
        if (error) {
            VLOG_FATAL("Failed to delete existing pidfile %s (%s)", pidfile,
                       ovs_strerror(errno));
        }
    }

    filep_pidfile = fopen(pidfile, "w");
    if (filep_pidfile == NULL) {
        VLOG_FATAL("failed to open %s (%s)", pidfile, ovs_strerror(errno));
    }

    flock(filep_pidfile, LOCKFILE_EXCLUSIVE_LOCK);

    fatal_signal_add_hook(unlink_pidfile, NULL, NULL, true);

    fprintf(filep_pidfile, "%ld\n", (long int) getpid());
    if (fflush(filep_pidfile) == EOF) {
        VLOG_FATAL("Failed to write into the pidfile %s", pidfile);
    }

    flock(filep_pidfile, LOCK_SHARED);
    /* This will remove the exclusive lock. The shared lock will remain */
    flock(filep_pidfile, LOCK_UNLOCK);

    /* Don't close the pidfile till the process exits. */
}

void
daemonize_start(bool access_datapath OVS_UNUSED,
                bool access_hardware_ports OVS_UNUSED)
{
    if (pidfile) {
        make_pidfile();
    }
}

void
daemonize_complete(void)
{
    /* If running as a child because '--detach' option was specified,
     * communicate with the parent to inform that the child is ready. */
    if (detached) {
        int error;

        close_standard_fds();

        error = WriteFile(write_handle, "a", 1, NULL, NULL);
        if (!error) {
            VLOG_FATAL("Failed to communicate with the parent (%s)",
                       ovs_lasterror_to_string());
        }
    }

    service_complete();
}

void
daemon_become_new_user(bool access_datapath OVS_UNUSED,
                       bool access_hardware_ports OVS_UNUSED)
{
}

/* Returns the file name that would be used for a pidfile if 'name' were
 * provided to set_pidfile().  The caller must free the returned string. */
char *
make_pidfile_name(const char *name)
{
    if (name) {
        if (strchr(name, ':')) {
            return xstrdup(name);
        } else {
            return xasprintf("%s/%s", ovs_rundir(), name);
        }
    } else {
        return xasprintf("%s/%s.pid", ovs_rundir(), program_name);
    }
}

void
daemon_set_new_user(const char *user_spec OVS_UNUSED)
{
    VLOG_FATAL("--user options is not currently supported.");
}
