root/sbin/svc/remote.c
/*
 * Copyright (c) 2014 The DragonFly Project.  All rights reserved.
 *
 * This code is derived from software contributed to The DragonFly Project
 * by Matthew Dillon <dillon@backplane.com>
 *
 * 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 DragonFly 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 HOLDERS 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.
 */
/*
 * Handle remote listen/connect and parsing operations.
 */

#include "svc.h"

typedef struct SvcConnect {
        struct SvcConnect *next;
        command_t *cmd;
        char      *label;
        pthread_t td;
        int     active;
        int     fd;
        int     rc;
        FILE    *fpr;
        FILE    *fpw;
} connect_t;

static void *remote_connect_thread(void *arg);
static void *remote_listener_thread(void *arg);
static void *remote_accepted_thread(void *arg);
static void remote_issue(connect_t *conn, command_t *cmd);
static int decode_args(connect_t *conn, char ***avp,
                        const char *ptr, size_t len);

connect_t *CHead;
connect_t **CNextP = &CHead;


/*
 * Execute cmd on the running service by connecting to the service, passing-in
 * the command, and processing results.
 *
 * Called only by master process
 */
void
remote_execute(command_t *cmd, const char *label)
{
        connect_t *conn = calloc(1, sizeof(*conn));

        conn->fd = -1;
        conn->cmd = cmd;
        conn->label = strdup(label);
        conn->active = 1;
        conn->next = *CNextP;
        *CNextP = conn;

        pthread_create(&conn->td, NULL, remote_connect_thread, conn);
}

/*
 * Threaded connect/execute
 */
static
void *
remote_connect_thread(void *arg)
{
        connect_t *conn;
        command_t *cmd;
        struct sockaddr_un sou;
        size_t len;
        char *ptr;

        conn = arg;
        cmd = conn->cmd;

        bzero(&sou, sizeof(sou));
        if ((conn->fd = socket(AF_UNIX, SOCK_STREAM, 0)) >= 0) {
                sou.sun_family = AF_UNIX;
                snprintf(sou.sun_path, sizeof(sou.sun_path),
                         "%s/service.%s.sk", cmd->piddir, conn->label);
                len = strlen(sou.sun_path);
                len = offsetof(struct sockaddr_un, sun_path[len+1]);

                if (connect(conn->fd, (void *)&sou, len) < 0) {
                        close(conn->fd);
                        conn->fd = -1;
                }
        }
        if (conn->fd >= 0) {
                /*
                 * Issue command
                 */
                conn->fpr = fdopen(conn->fd, "r");
                conn->fpw = fdopen(dup(conn->fd), "w");
                conn->fd = -1;
                setvbuf(conn->fpr, NULL, _IOFBF, 0);
                setvbuf(conn->fpw, NULL, _IOFBF, 0);
                remote_issue(conn, cmd);
                while ((ptr = fgetln(conn->fpr, &len)) != NULL) {
                        if (len == 2 && ptr[0] == '.' && ptr[1] == '\n')
                                break;

                        /*
                         * de-escape ..
                         */
                        if (len == 3 && ptr[0] == '.' && ptr[1] == '.' &&
                            ptr[2] == '\n') {
                                ++ptr;
                                --len;
                        }
                        fwrite(ptr, 1, len, cmd->fp);
                        fflush(cmd->fp);
                }
                conn->rc = 0;
                conn->active = 0;
                fclose(conn->fpr);
                fclose(conn->fpw);
                conn->fpr = NULL;
                conn->fpw = NULL;
        } else {
                /*
                 * Connection failed
                 */
                fprintf(cmd->fp,
                        "Unable to connect to service %s\n",
                        conn->label);
                conn->rc = 1;
                conn->active = 0;
                if (cmd->force_remove_files) {
                        fprintf(cmd->fp,
                                "Removing pid and socket files for %s\n",
                                conn->label);
                        remove_pid_and_socket(cmd, conn->label);
                }
        }
        return NULL;
}

/*
 * Called only by master process
 *
 * Collect status from all remote commands.
 */
int
remote_wait(void)
{
        connect_t *scan;
        int rc = 0;

        while ((scan = CHead) != NULL) {
                if (pthread_join(scan->td, NULL) < 0)
                        continue;
                assert(scan->active == 0);
                rc += scan->rc;
                CHead = scan->next;

                if (scan->fpr) {
                        fclose(scan->fpr);
                        scan->fpr = NULL;
                }
                if (scan->fpw) {
                        fclose(scan->fpw);
                        scan->fpw = NULL;
                }
                if (scan->fd >= 0) {
                        close(scan->fd);
                        scan->fd = -1;
                }
                if (scan->label) {
                        free(scan->label);
                        scan->label = NULL;
                }
                scan->cmd = NULL;
                free(scan);
        }
        return rc;
}

/*
 * Create the unix domain socket and pid file for the service
 * and start a thread to accept and process connections.
 *
 * Return 0 on success, non-zero if the socket could not be created.
 */
void
remote_listener(command_t *cmd, int lfd)
{
        connect_t *conn;

        /*
         * child, create our unix domain socket listener thread.
         */
        conn = calloc(1, sizeof(*conn));
        conn->fd = lfd;
        conn->cmd = cmd;
        conn->label = strdup(cmd->label);
        conn->active = 1;

        conn->next = *CNextP;
        *CNextP = conn;
        pthread_create(&conn->td, NULL, remote_listener_thread, conn);
}

static void *
remote_listener_thread(void *arg)
{
        connect_t *lconn = arg;
        connect_t *conn;
        struct sockaddr_un sou;
        socklen_t len;

        conn = calloc(1, sizeof(*conn));
        for (;;) {
                len = sizeof(sou);
                conn->fd = accept(lconn->fd, (void *)&sou, &len);
                conn->label = strdup(lconn->label);
                if (conn->fd < 0) {
                        if (errno == EINTR)
                                continue;
                        break;
                }
                pthread_create(&conn->td, NULL, remote_accepted_thread, conn);
                conn = calloc(1, sizeof(*conn));
        }
        free(conn);

        return NULL;
}

static void *
remote_accepted_thread(void *arg)
{
        connect_t *conn = arg;
        command_t cmd;
        char *ptr;
        size_t len;
        int rc;
        int ac;
        char **av;

        pthread_detach(conn->td);
        conn->fpr = fdopen(conn->fd, "r");
        conn->fpw = fdopen(dup(conn->fd), "w");
        conn->fd = -1;
        setvbuf(conn->fpr, NULL, _IOFBF, 0);
        setvbuf(conn->fpw, NULL, _IOFBF, 0);

        while ((ptr = fgetln(conn->fpr, &len)) != NULL) {
                ac = decode_args(conn, &av, ptr, len);
                rc = process_cmd(&cmd, conn->fpw, ac, av);
                cmd.cmdline = 0;        /* we are the remote */
                cmd.commanded = 1;      /* commanded action (vs automatic) */
                sreplace(&cmd.label, conn->label);
                if (rc == 0) {
                        pthread_mutex_lock(&serial_mtx);
                        rc = execute_cmd(&cmd);
                        pthread_mutex_unlock(&serial_mtx);
                }
                free_cmd(&cmd);
                afree(&av);
                fwrite(".\n", 2, 1, conn->fpw);
                fflush(conn->fpw);
        }
        fclose(conn->fpr);
        fclose(conn->fpw);
        conn->fpr = NULL;
        conn->fpw = NULL;
        free(conn->label);
        free(conn);

        return NULL;
}

/*
 * Issue the command to the remote, encode the arguments.
 */
static
void
remote_issue(connect_t *conn, command_t *cmd)
{
        int i;

        for (i = 1; i < cmd->orig_ac; ++i) {
                const char *str = cmd->orig_av[i];

                if (i != 1)
                        putc(' ', conn->fpw);
                while (*str) {
                        if (*str == ' ' || *str == '\\' || *str == '\n')
                                putc('\\', conn->fpw);
                        putc(*str, conn->fpw);
                        ++str;
                }
        }
        putc('\n', conn->fpw);
        fflush(conn->fpw);
}

/*
 * Decode arguments
 */
static int
decode_args(connect_t *conn __unused, char ***avp, const char *ptr, size_t len)
{
        char **av;
        char *arg;
        size_t i;
        size_t j;
        int acmax;
        int ac;

        if (len && ptr[len-1] == '\n')
                --len;

        acmax = 3;      /* av[0], first arg, terminating NULL */
        for (i = 0; i < len; ++i) {
                if (ptr[i] == ' ')
                        ++acmax;
        }
        av = calloc(acmax, sizeof(char *));
        av[0] = NULL;
        ac = 1;

        i = 0;
        while (i < len) {
                for (j = i; j < len; ++j) {
                        if (ptr[j] == ' ')
                                break;
                }
                arg = malloc(j - i + 1);        /* worst case arg size */
                j = 0;
                while (i < len) {
                        if (ptr[i] == ' ')
                                break;
                        if (ptr[i] == '\\' && i + 1 < len) {
                                arg[j++] = ptr[i+1];
                                i += 2;
                        } else {
                                arg[j++] = ptr[i];
                                i += 1;
                        }
                }
                arg[j] = 0;
                av[ac++] = arg;
                if (i < len && ptr[i] == ' ')
                        ++i;
        }
        av[ac] = NULL;

#if 0
        fprintf(conn->fpw, "DECODE ARGS: ");
        for (i = 1; i < (size_t)ac; ++i)
                fprintf(conn->fpw, " \"%s\"", av[i]);
        fprintf(conn->fpw, "\n");
        fflush(conn->fpw);
#endif

        *avp = av;
        return ac;
}