root/usr/src/cmd/cmd-inet/usr.lib/inetd/wait.c
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */
/*
 * Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

/*
 * This file contains a set of routines used to perform wait based method
 * reaping.
 */

#include <wait.h>
#include <sys/param.h>
#include <fcntl.h>
#include <libcontract.h>
#include <errno.h>
#include <libintl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <sys/resource.h>
#include "inetd_impl.h"

/* inetd's open file limit, set in method_init() */
#define INETD_NOFILE_LIMIT RLIM_INFINITY

/* structure used to represent an active method process */
typedef struct {
        int                     fd;     /* fd of process's /proc psinfo file */
        /* associated contract id if known, else -1 */
        ctid_t                  cid;
        pid_t                   pid;
        instance_t              *inst;  /* pointer to associated instance */
        instance_method_t       method; /* the method type running */
        /* associated endpoint protocol name if known, else NULL */
        char                    *proto_name;
        uu_list_node_t          link;
} method_el_t;


static void unregister_method(method_el_t *);


/* list of currently executing method processes */
static uu_list_pool_t           *method_pool = NULL;
static uu_list_t                *method_list = NULL;

/*
 * File limit saved during initialization before modification, so that it can
 * be reverted back to for inetd's exec'd methods.
 */
static struct rlimit            saved_file_limit;

/*
 * Setup structures used for method termination monitoring.
 * Returns -1 if an allocation failure occurred, else 0.
 */
int
method_init(void)
{
        struct rlimit rl;

        /*
         * Save aside the old file limit and impose one large enough to support
         * all the /proc file handles we could have open.
         */

        (void) getrlimit(RLIMIT_NOFILE, &saved_file_limit);

        rl.rlim_cur = rl.rlim_max = INETD_NOFILE_LIMIT;
        if (setrlimit(RLIMIT_NOFILE, &rl) == -1) {
                error_msg("Failed to set file limit: %s", strerror(errno));
                return (-1);
        }

        if ((method_pool = uu_list_pool_create("method_pool",
            sizeof (method_el_t), offsetof(method_el_t, link), NULL,
            UU_LIST_POOL_DEBUG)) == NULL) {
                error_msg("%s: %s", gettext("Failed to create method pool"),
                    uu_strerror(uu_error()));
                return (-1);
        }

        if ((method_list = uu_list_create(method_pool, NULL, 0)) == NULL) {
                error_msg("%s: %s",
                    gettext("Failed to create method list"),
                    uu_strerror(uu_error()));
                /* let method_fini() clean-up */
                return (-1);
        }

        return (0);
}

/*
 * Tear-down structures created in method_init().
 */
void
method_fini(void)
{
        if (method_list != NULL) {
                method_el_t *me;

                while ((me = uu_list_first(method_list)) != NULL)
                        unregister_method(me);

                (void) uu_list_destroy(method_list);
                method_list = NULL;
        }
        if (method_pool != NULL) {
                (void) uu_list_pool_destroy(method_pool);
                method_pool = NULL;
        }

        /* revert file limit */
        method_preexec();
}

/*
 * Revert file limit back to pre-initialization one. This shouldn't fail as
 * long as its called *after* descriptor cleanup.
 */
void
method_preexec(void)
{
        (void) setrlimit(RLIMIT_NOFILE, &saved_file_limit);
}


/*
 * Callback function that handles the timeout of an instance's method.
 * 'arg' points at the method_el_t representing the method.
 */
/* ARGSUSED0 */
static void
method_timeout(iu_tq_t *tq, void *arg)
{
        method_el_t *mp = arg;

        error_msg(gettext("The %s method of instance %s timed-out"),
            methods[mp->method].name, mp->inst->fmri);

        mp->inst->timer_id = -1;

        if (mp->method == IM_START) {
                process_start_term(mp->inst, mp->proto_name);
        } else {
                process_non_start_term(mp->inst, IMRET_FAILURE);
        }

        unregister_method(mp);
}

/*
 * Registers the attributes of a running method passed as arguments so that
 * the method's termination is noticed and any further processing of the
 * associated instance is carried out. The function also sets up any
 * necessary timers so we can detect hung methods.
 * Returns -1 if either it failed to open the /proc psinfo file which is used
 * to monitor the method process, it failed to setup a required timer or
 * memory allocation failed; else 0.
 */
int
register_method(instance_t *ins, pid_t pid, ctid_t cid, instance_method_t mthd,
    char *proto_name)
{
        char            path[MAXPATHLEN];
        int             fd;
        method_el_t     *me;

        /* open /proc psinfo file of process to listen for POLLHUP events on */
        (void) snprintf(path, sizeof (path), "/proc/%u/psinfo", pid);
        for (;;) {
                if ((fd = open(path, O_RDONLY)) >= 0) {
                        break;
                } else if (errno != EINTR) {
                        /*
                         * Don't output an error for ENOENT; we get this
                         * if a method has gone away whilst we were stopped,
                         * and we're now trying to re-listen for it.
                         */
                        if (errno != ENOENT) {
                                error_msg(gettext("Failed to open %s: %s"),
                                    path, strerror(errno));
                        }
                        return (-1);
                }
        }

        /* add method record to in-memory list */
        if ((me = calloc(1, sizeof (method_el_t))) == NULL) {
                error_msg(strerror(errno));
                (void) close(fd);
                return (-1);
        }
        me->fd = fd;
        me->inst = (instance_t *)ins;
        me->method = mthd;
        me->pid = pid;
        me->cid = cid;
        if (proto_name != NULL) {
                if ((me->proto_name = strdup(proto_name)) == NULL) {
                        error_msg(strerror(errno));
                        free(me);
                        (void) close(fd);
                        return (-1);
                }
        } else
                me->proto_name = NULL;

        /* register a timeout for the method, if required */
        if (mthd != IM_START) {
                method_info_t *mi = ins->config->methods[mthd];

                if (mi->timeout > 0) {
                        assert(ins->timer_id == -1);
                        ins->timer_id = iu_schedule_timer(timer_queue,
                            mi->timeout, method_timeout, me);
                        if (ins->timer_id == -1) {
                                error_msg(gettext(
                                    "Failed to schedule method timeout"));
                                if (me->proto_name != NULL)
                                        free(me->proto_name);
                                free(me);
                                (void) close(fd);
                                return (-1);
                        }
                }
        }

        /*
         * Add fd of psinfo file to poll set, but pass 0 for events to
         * poll for, so we should only get a POLLHUP event on the fd.
         */
        if (set_pollfd(fd, 0) == -1) {
                cancel_inst_timer(ins);
                if (me->proto_name != NULL)
                        free(me->proto_name);
                free(me);
                (void) close(fd);
                return (-1);
        }

        uu_list_node_init(me, &me->link, method_pool);
        (void) uu_list_insert_after(method_list, NULL, me);

        return (0);
}

/*
 * A counterpart to register_method(), this function stops the monitoring of a
 * method process for its termination.
 */
static void
unregister_method(method_el_t *me)
{
        /* cancel any timer associated with the method */
        if (me->inst->timer_id != -1)
                cancel_inst_timer(me->inst);

        /* stop polling on the psinfo file fd */
        clear_pollfd(me->fd);
        (void) close(me->fd);

        /* remove method record from list */
        uu_list_remove(method_list, me);

        if (me->proto_name != NULL)
                free(me->proto_name);
        free(me);
}

/*
 * Unregister all methods associated with instance 'inst'.
 */
void
unregister_instance_methods(const instance_t *inst)
{
        method_el_t *me = uu_list_first(method_list);

        while (me != NULL) {
                if (me->inst == inst) {
                        method_el_t *tmp = me;

                        me = uu_list_next(method_list, me);
                        unregister_method(tmp);
                } else  {
                        me = uu_list_next(method_list, me);
                }
        }
}

/*
 * Process any terminated methods. For each method determined to have
 * terminated, the function determines its return value and calls the
 * appropriate handling function, depending on the type of the method.
 */
void
process_terminated_methods(void)
{
        method_el_t     *me = uu_list_first(method_list);

        while (me != NULL) {
                struct pollfd   *pfd;
                pid_t           pid;
                int             status;
                int             ret;
                method_el_t     *tmp;

                pfd = find_pollfd(me->fd);

                /*
                 * We expect to get a POLLHUP back on the fd of the process's
                 * open psinfo file from /proc when the method terminates.
                 * A POLLERR could(?) mask a POLLHUP, so handle this
                 * also.
                 */
                if ((pfd->revents & (POLLHUP|POLLERR)) == 0) {
                        me = uu_list_next(method_list, me);
                        continue;
                }

                /* get the method's exit code (no need to loop for EINTR) */
                pid = waitpid(me->pid, &status, WNOHANG);

                switch (pid) {
                case 0:                                 /* child still around */
                        /*
                         * Either poll() is sending us invalid POLLHUP events
                         * or is flagging a POLLERR on the fd. Neither should
                         * happen, but in the event they do, ignore this fd
                         * this time around and wait out the termination
                         * of its associated method. This may result in
                         * inetd swiftly looping in event_loop(), but means
                         * we don't miss the termination of a method.
                         */
                        me = uu_list_next(method_list, me);
                        continue;

                case -1:                                /* non-existent child */
                        assert(errno == ECHILD);
                        /*
                         * the method must not be owned by inetd due to it
                         * persisting over an inetd restart. Let's assume the
                         * best, that it was successful.
                         */
                        ret = IMRET_SUCCESS;
                        break;

                default:                                /* child terminated */
                        if (WIFEXITED(status)) {
                                ret = WEXITSTATUS(status);
                                debug_msg("process %ld of instance %s returned "
                                    "%d", pid, me->inst->fmri, ret);
                        } else if (WIFSIGNALED(status)) {
                                /*
                                 * Terminated by signal.  This may be due
                                 * to a kill that we sent from a disable or
                                 * offline event. We flag it as a failure, but
                                 * this flagged failure will only be processed
                                 * in the case of non-start methods, or when
                                 * the instance is still enabled.
                                 */
                                debug_msg("process %ld of instance %s exited "
                                    "due to signal %d", pid, me->inst->fmri,
                                    WTERMSIG(status));
                                ret = IMRET_FAILURE;
                        } else {
                                /*
                                 * Can we actually get here?  Don't think so.
                                 * Treat it as a failure, anyway.
                                 */
                                debug_msg("waitpid() for %s method of "
                                    "instance %s returned %d",
                                    methods[me->method].name, me->inst->fmri,
                                    status);
                                ret = IMRET_FAILURE;
                        }
                }

                remove_method_ids(me->inst, me->pid, me->cid, me->method);

                /* continue state transition processing of the instance */
                if (me->method != IM_START) {
                        process_non_start_term(me->inst, ret);
                } else {
                        process_start_term(me->inst, me->proto_name);
                }

                if (me->cid != -1)
                        (void) abandon_contract(me->cid);

                tmp = me;
                me = uu_list_next(method_list, me);
                unregister_method(tmp);
        }
}