	/* (c) 2007,2008 Andrei Nigmatulin */

#include "fpm_config.h"

#include <sys/types.h>
#include <sys/wait.h>
#include <time.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>

#include "fpm.h"
#include "fpm_children.h"
#include "fpm_signals.h"
#include "fpm_worker_pool.h"
#include "fpm_sockets.h"
#include "fpm_process_ctl.h"
#include "fpm_php.h"
#include "fpm_conf.h"
#include "fpm_cleanup.h"
#include "fpm_events.h"
#include "fpm_clock.h"
#include "fpm_stdio.h"
#include "fpm_unix.h"
#include "fpm_env.h"
#include "fpm_scoreboard.h"
#include "fpm_status.h"
#include "fpm_log.h"

#include "zlog.h"

#ifdef HAVE_LVE
#include <dlfcn.h>

typedef void liblve;
enum liblve_enter_flags {
        LVE_NO_UBC      = 1 << 0,
        LVE_NO_NAMESPACE = 1 << 1,
        LVE_NO_MAXENTER = 1 << 2,
        LVE_SILENCE     = 1 << 3,
};
typedef void *(*liblve_alloc)(size_t size);
typedef void (*liblve_free)(void *ptr);
void *handle_liblve = NULL;
struct liblve *lve = NULL;
int (*lve_jail_uid_ptr)(struct passwd *, unsigned int, char *) = NULL;
int (*destroy_lve_ptr)(struct liblve *) = NULL;
struct liblve * (*init_lve_ptr)(liblve_alloc, liblve_free) = NULL;
int (*lve_exit_ptr)(struct liblve *, uint32_t *) = NULL;
int (*lve_enter_flags_ptr)(struct liblve *,
                    uint32_t, uint32_t *, enum liblve_enter_flags) = NULL;
#endif

static time_t *last_faults;
static int fault;


static void fpm_children_cleanup(int which, void *arg) /* {{{ */
{
	free(last_faults);
#ifdef HAVE_LVE
	if(handle_liblve){
	    if(destroy_lve_ptr && lve) destroy_lve_ptr(lve);
	    dlclose(handle_liblve);
	    lve_enter_flags_ptr = NULL;
	    destroy_lve_ptr = NULL;
	    lve_exit_ptr = NULL;
	    init_lve_ptr = NULL;
	    lve_jail_uid_ptr = NULL;
	}
#endif
}
/* }}} */

static struct fpm_child_s *fpm_child_alloc(void)
{
	struct fpm_child_s *ret;

	ret = malloc(sizeof(struct fpm_child_s));

	if (!ret) {
		return 0;
	}

	memset(ret, 0, sizeof(*ret));
	ret->scoreboard_i = -1;
	return ret;
}

static void fpm_child_free(struct fpm_child_s *child) /* {{{ */
{
	if (child->log_stream) {
		zlog_stream_close(child->log_stream);
		free(child->log_stream);
	}
	free(child);
}
/* }}} */

static void fpm_child_close(struct fpm_child_s *child, int in_event_loop) /* {{{ */
{
	if (child->fd_stdout != -1) {
		if (in_event_loop) {
			fpm_event_fire(&child->ev_stdout);
		}
		if (child->fd_stdout != -1) {
			close(child->fd_stdout);
		}
	}

	if (child->fd_stderr != -1) {
		if (in_event_loop) {
			fpm_event_fire(&child->ev_stderr);
		}
		if (child->fd_stderr != -1) {
			close(child->fd_stderr);
		}
	}

	fpm_child_free(child);
}
/* }}} */

static void fpm_child_link(struct fpm_child_s *child) /* {{{ */
{
	struct fpm_worker_pool_s *wp = child->wp;

	++wp->running_children;
	++fpm_globals.running_children;

	child->next = wp->children;
	if (child->next) {
		child->next->prev = child;
	}
	child->prev = 0;
	wp->children = child;
}
/* }}} */

static void fpm_child_unlink(struct fpm_child_s *child) /* {{{ */
{
	--child->wp->running_children;
	--fpm_globals.running_children;

	if (child->prev) {
		child->prev->next = child->next;
	} else {
		child->wp->children = child->next;
	}

	if (child->next) {
		child->next->prev = child->prev;
	}
}
/* }}} */

static struct fpm_child_s *fpm_child_find(pid_t pid) /* {{{ */
{
	struct fpm_worker_pool_s *wp;
	struct fpm_child_s *child = 0;

	for (wp = fpm_worker_all_pools; wp; wp = wp->next) {

		for (child = wp->children; child; child = child->next) {
			if (child->pid == pid) {
				break;
			}
		}

		if (child) break;
	}

	if (!child) {
		return 0;
	}

	return child;
}
/* }}} */

static void fpm_child_init(struct fpm_worker_pool_s *wp) /* {{{ */
{
	fpm_globals.max_requests = wp->config->pm_max_requests;
	fpm_globals.listening_socket = dup(wp->listening_socket);

	if (0 > fpm_stdio_init_child(wp)  ||
	    0 > fpm_log_init_child(wp)    ||
	    0 > fpm_status_init_child(wp) ||
	    0 > fpm_unix_init_child(wp)   ||
	    0 > fpm_signals_init_child()  ||
	    0 > fpm_env_init_child(wp)    ||
	    0 > fpm_php_init_child(wp)) {

		zlog(ZLOG_ERROR, "[pool %s] child failed to initialize", wp->config->name);
		exit(FPM_EXIT_SOFTWARE);
	}
}
/* }}} */

int fpm_children_free(struct fpm_child_s *child) /* {{{ */
{
	struct fpm_child_s *next;

	for (; child; child = next) {
		next = child->next;
		fpm_child_close(child, 0 /* in_event_loop */);
	}

	return 0;
}
/* }}} */

void fpm_children_bury(void)
{
	int status;
	pid_t pid;
	struct fpm_child_s *child;

	while ( (pid = waitpid(-1, &status, WNOHANG | WUNTRACED)) > 0) {
		char buf[128];
		int severity = ZLOG_NOTICE;
		int restart_child = 1;

		child = fpm_child_find(pid);

		if (WIFEXITED(status)) {

			snprintf(buf, sizeof(buf), "with code %d", WEXITSTATUS(status));

			/* if it's been killed because of dynamic process management
			 * don't restart it automatically
			 */
			if (child && child->idle_kill) {
				restart_child = 0;
			}

			if (WEXITSTATUS(status) != FPM_EXIT_OK) {
				severity = ZLOG_WARNING;
			}

		} else if (WIFSIGNALED(status)) {
			const char *signame = fpm_signal_names[WTERMSIG(status)];
#ifdef WCOREDUMP
			const char *have_core = WCOREDUMP(status) ? " - core dumped" : "";
#else
			const char* have_core = "";
#endif

			if (signame == NULL) {
				signame = "";
			}

			snprintf(buf, sizeof(buf), "on signal %d (%s%s)", WTERMSIG(status), signame, have_core);

			/* if it's been killed because of dynamic process management
			 * don't restart it automatically
			 */
			if (child && child->idle_kill && WTERMSIG(status) == SIGQUIT) {
				restart_child = 0;
			}

			if (WTERMSIG(status) != SIGQUIT) { /* possible request loss */
				severity = ZLOG_WARNING;
			}
		} else if (WIFSTOPPED(status)) {

			zlog(ZLOG_NOTICE, "child %d stopped for tracing", (int) pid);

			if (child && child->tracer) {
				child->tracer(child);
			}

			continue;
		}

		if (child) {
			struct fpm_worker_pool_s *wp = child->wp;
			struct timeval tv1, tv2;

			fpm_child_unlink(child);

			fpm_scoreboard_proc_free(child);

			fpm_clock_get(&tv1);

			timersub(&tv1, &child->started, &tv2);

			if (restart_child) {
				if (!fpm_pctl_can_spawn_children()) {
					severity = ZLOG_DEBUG;
				}
				zlog(severity, "[pool %s] child %d exited %s after %ld.%06d seconds from start", wp->config->name, (int) pid, buf, tv2.tv_sec, (int) tv2.tv_usec);
			} else {
				zlog(ZLOG_DEBUG, "[pool %s] child %d has been killed by the process management after %ld.%06d seconds from start", wp->config->name, (int) pid, tv2.tv_sec, (int) tv2.tv_usec);
			}

			fpm_child_close(child, 1 /* in event_loop */);

			fpm_pctl_child_exited();

			if (last_faults && (WTERMSIG(status) == SIGSEGV || WTERMSIG(status) == SIGBUS)) {
				time_t now = tv1.tv_sec;
				int restart_condition = 1;
				int i;

				last_faults[fault++] = now;

				if (fault == fpm_global_config.emergency_restart_threshold) {
					fault = 0;
				}

				for (i = 0; i < fpm_global_config.emergency_restart_threshold; i++) {
					if (now - last_faults[i] > fpm_global_config.emergency_restart_interval) {
						restart_condition = 0;
						break;
					}
				}

				if (restart_condition) {

					zlog(ZLOG_WARNING, "failed processes threshold (%d in %d sec) is reached, initiating reload", fpm_global_config.emergency_restart_threshold, fpm_global_config.emergency_restart_interval);

					fpm_pctl(FPM_PCTL_STATE_RELOADING, FPM_PCTL_ACTION_SET);
				}
			}

			if (restart_child) {
				fpm_children_make(wp, 1 /* in event loop */, 1, 0);

				if (fpm_globals.is_child) {
					break;
				}
			}
		} else {
			zlog(ZLOG_ALERT, "oops, unknown child (%d) exited %s. Please open a bug report (https://github.com/php/php-src/issues).", pid, buf);
		}
	}
}

static struct fpm_child_s *fpm_resources_prepare(struct fpm_worker_pool_s *wp) /* {{{ */
{
	struct fpm_child_s *c;

	c = fpm_child_alloc();

	if (!c) {
		zlog(ZLOG_ERROR, "[pool %s] unable to malloc new child", wp->config->name);
		return 0;
	}

	c->wp = wp;
	c->fd_stdout = -1; c->fd_stderr = -1;

	if (0 > fpm_stdio_prepare_pipes(c)) {
		fpm_child_free(c);
		return 0;
	}

	if (0 > fpm_scoreboard_proc_alloc(c)) {
		fpm_stdio_discard_pipes(c);
		fpm_child_free(c);
		return 0;
	}

	return c;
}
/* }}} */

static void fpm_resources_discard(struct fpm_child_s *child) /* {{{ */
{
	fpm_scoreboard_proc_free(child);
	fpm_stdio_discard_pipes(child);
	fpm_child_free(child);
}
/* }}} */

static void fpm_child_resources_use(struct fpm_child_s *child) /* {{{ */
{
	struct fpm_worker_pool_s *wp;
	for (wp = fpm_worker_all_pools; wp; wp = wp->next) {
		if (wp == child->wp || wp == child->wp->shared) {
			continue;
		}
		fpm_scoreboard_free(wp);
	}

	fpm_scoreboard_child_use(child, getpid());
	fpm_stdio_child_use_pipes(child);
	fpm_child_free(child);
}
/* }}} */

static void fpm_parent_resources_use(struct fpm_child_s *child) /* {{{ */
{
	fpm_stdio_parent_use_pipes(child);
	fpm_child_link(child);
}
/* }}} */

#ifdef HAVE_LVE
int read_cagefs_min_uid(void)
{
    static int min_uid_cfg = MIN_UID_UNINITIALIZED;
    int buffer = 0, rc = 0;

    if (min_uid_cfg != MIN_UID_UNINITIALIZED)
      return min_uid_cfg;

    min_uid_cfg = MIN_UID_DEFAULT;

    FILE *fp = fopen(MIN_UID_FILE, "rb");
    if (!fp) {
      return min_uid_cfg;
    }

    rc = fread(&buffer, sizeof (int), 1, fp);
    if ((rc > 0) && (buffer > 0)) {
      min_uid_cfg = buffer;
    } 

    fclose(fp);
    return min_uid_cfg;
}

void fpm_lve_leave(struct liblve *lve, struct fpm_worker_pool_s *wp, uint32_t p_cookie, int was_enter) /* {{{ */
{
    int rc = 0;
    if (was_enter == 0)
        return;

    zlog(ZLOG_DEBUG, "[pool %s] Exiting LVE: LVE(%d)", wp->config->name, wp->set_uid);
    rc = (*lve_exit_ptr)(lve, &p_cookie);
    if (rc) {
        zlog(ZLOG_ERROR, "[pool %s] Error existing LVE: LVE(%d)", wp->config->name, wp->set_uid);
    }
}
/* }}} */

uint32_t fpm_lve_enter(struct liblve *lve, struct fpm_worker_pool_s *wp) /* {{{ */
{
  uint32_t p_cookie = 0;
  int rc = 0;
  int keep_errno = 0;
  int min_uid = read_cagefs_min_uid();

  if (wp->set_uid < min_uid) {
    zlog(ZLOG_DEBUG, "[pool %s] Skipped LVE enter because of min_uid: LVE(%d) min_uid(%d)", wp->config->name, wp->set_uid, min_uid);
    return 0;
  }
  zlog(ZLOG_DEBUG, "[pool %s] Entering LVE: LVE(%d)", wp->config->name, wp->set_uid);
  rc = (*lve_enter_flags_ptr)(lve, wp->set_uid, &p_cookie, LVE_NO_MAXENTER | LVE_SILENCE);
  keep_errno = errno;
  if (rc) {
    if (keep_errno == EPERM) {
      zlog(ZLOG_ERROR, "[pool %s] Already inside LVE: LVE(%d)", wp->config->name, wp->set_uid);
    } else {
      zlog(ZLOG_ERROR, "[pool %s] Error on LVE enter: LVE(%d)", wp->config->name, wp->set_uid);
    }
    return 0;
  } else {
    zlog(ZLOG_DEBUG, "[pool %s] Entered LVE successfully: LVE(%d)", wp->config->name, wp->set_uid);
    return 1;
  }
}
/* }}} */
#endif

int fpm_children_make(struct fpm_worker_pool_s *wp, int in_event_loop, int nb_to_spawn, int is_debug) /* {{{ */
{
	pid_t pid;
	struct fpm_child_s *child;
	int max;
	static int warned = 0;

	if (wp->config->pm == PM_STYLE_DYNAMIC) {
		if (!in_event_loop) { /* starting */
			max = wp->config->pm_start_servers;
		} else {
			max = wp->running_children + nb_to_spawn;
		}
	} else if (wp->config->pm == PM_STYLE_ONDEMAND) {
		if (!in_event_loop) { /* starting */
			max = 0; /* do not create any child at startup */
		} else {
			max = wp->running_children + nb_to_spawn;
		}
	} else { /* PM_STYLE_STATIC */
		max = wp->config->pm_max_children;
	}

	/*
	 * fork children while:
	 *   - fpm_pctl_can_spawn_children : FPM is running in a NORMAL state (aka not restart, stop or reload)
	 *   - wp->running_children < max  : there is less than the max process for the current pool
	 *   - (fpm_global_config.process_max < 1 || fpm_globals.running_children < fpm_global_config.process_max):
	 *     if fpm_global_config.process_max is set, FPM has not fork this number of processes (globaly)
	 */
	while (fpm_pctl_can_spawn_children() && wp->running_children < max && (fpm_global_config.process_max < 1 || fpm_globals.running_children < fpm_global_config.process_max)) {

		warned = 0;
		child = fpm_resources_prepare(wp);

		if (!child) {
			return 2;
		}

		zlog(ZLOG_DEBUG, "blocking signals before child birth");
		if (0 > fpm_signals_child_block()) {
			zlog(ZLOG_WARNING, "child may miss signals");
		}

		pid = fork();

		switch (pid) {

			case 0 :
#ifdef HAVE_LVE
				if (lve_enter_flags_ptr) {
					fpm_lve_enter(lve, wp);
				}
#endif
				fpm_child_resources_use(child);
				fpm_globals.is_child = 1;
				fpm_child_init(wp);
				return 0;

			case -1 :
				zlog(ZLOG_DEBUG, "unblocking signals");
				fpm_signals_unblock();
				zlog(ZLOG_SYSERROR, "fork() failed");
				fpm_resources_discard(child);
				return 2;

			default :
				zlog(ZLOG_DEBUG, "unblocking signals, child born");
				fpm_signals_unblock();
				child->pid = pid;
				fpm_clock_get(&child->started);
				fpm_parent_resources_use(child);

				zlog(is_debug ? ZLOG_DEBUG : ZLOG_NOTICE, "[pool %s] child %d started", wp->config->name, (int) pid);
		}

	}

	if (!warned && fpm_global_config.process_max > 0 && fpm_globals.running_children >= fpm_global_config.process_max) {
               if (wp->running_children < max) {
                       warned = 1;
                       zlog(ZLOG_WARNING, "The maximum number of processes has been reached. Please review your configuration and consider raising 'process.max'");
               }
	}

	return 1; /* we are done */
}
/* }}} */

int fpm_children_create_initial(struct fpm_worker_pool_s *wp) /* {{{ */
{
	if (wp->config->pm == PM_STYLE_ONDEMAND) {
		wp->ondemand_event = (struct fpm_event_s *)malloc(sizeof(struct fpm_event_s));

		if (!wp->ondemand_event) {
			zlog(ZLOG_ERROR, "[pool %s] unable to malloc the ondemand socket event", wp->config->name);
			// FIXME handle crash
			return 1;
		}

		memset(wp->ondemand_event, 0, sizeof(struct fpm_event_s));
		fpm_event_set(wp->ondemand_event, wp->listening_socket, FPM_EV_READ | FPM_EV_EDGE, fpm_pctl_on_socket_accept, wp);
		wp->socket_event_set = 1;
		fpm_event_add(wp->ondemand_event, 0);

		return 1;
	}
	return fpm_children_make(wp, 0 /* not in event loop yet */, 0, 1);
}
/* }}} */

#ifdef HAVE_LVE
static int load_liblve_fpm(){
    handle_liblve = dlopen("liblve.so.0", RTLD_LOCAL | RTLD_LAZY);
    if(handle_liblve){
	lve_enter_flags_ptr = dlsym(handle_liblve, "lve_enter_flags");
	destroy_lve_ptr = dlsym(handle_liblve, "destroy_lve");
	lve_exit_ptr = dlsym(handle_liblve, "lve_exit");
	init_lve_ptr = dlsym(handle_liblve, "init_lve");
	lve_jail_uid_ptr = dlsym(handle_liblve, "lve_jail_uid");
	if(lve_exit_ptr && destroy_lve_ptr && lve_enter_flags_ptr && init_lve_ptr && lve_jail_uid_ptr){
	    return 0;
	}
	lve_enter_flags_ptr = NULL;
	destroy_lve_ptr = NULL;
	lve_exit_ptr = NULL;
	init_lve_ptr = NULL;
	lve_jail_uid_ptr = NULL;
    }
    return -1;
}
#endif

int fpm_children_init_main(void)
{
#ifdef HAVE_LVE
	if(!load_liblve_fpm()){
	    zlog(ZLOG_DEBUG, "Work with LVE and CageFS");
    	    lve = (*init_lve_ptr)(malloc, free);
	} else {
	    zlog(ZLOG_DEBUG, "Work without  LVE and CageFS");
	}
#endif
	if (fpm_global_config.emergency_restart_threshold &&
		fpm_global_config.emergency_restart_interval) {

		last_faults = malloc(sizeof(time_t) * fpm_global_config.emergency_restart_threshold);

		if (!last_faults) {
			return -1;
		}

		memset(last_faults, 0, sizeof(time_t) * fpm_global_config.emergency_restart_threshold);
	}

	if (0 > fpm_cleanup_add(FPM_CLEANUP_ALL, fpm_children_cleanup, 0)) {
		return -1;
	}

	return 0;
}
