/* * Copyright (C) 2019 Weber Yann * * This file is part of PyFCGI. * * PyFCGI is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by * the Free Software Foundation, either version 3 of the License, or * any later version. * * PyFCGI is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with PyFCGI. If not, see . */ /**@defgroup proc_intern Internal process management * @brief Groups all information on internal sub-processes management */ /**@defgroup processes PyFCGI process hierarchy * * PyFCGI is organised in three layer : * A @ref main_proc : simple, that keep running and spawn a * @ref work_master_proc . This process handles @ref worker_process creation * and try to maintain a pool able to reply efficiently to CGI requests. * @ingroup proc_intern */ /**@defgroup main_proc Main process * @brief The main process in the @ref main() function * @ingroup processes */ #include /* fcgi library; put it first*/ #include #include #include #include #include #include #include #include #include #include "conf.h" #include "logger.h" #include "responder.h" #include "ipc.h" #define IDENT_FMT "pyfcgi[%d]" #define MAX_REQS 1024 #define EARLY_ERR(err_str) write(2, err_str, strlen(err_str)) extern pyfcgi_conf_t PyFCGI_conf; pid_t pool_handler_pid; pid_t monitor_serv_pid; void sighandler(int signum) { int status, ret, i; struct timespec req; req.tv_sec = 0; req.tv_nsec = 100000000; //0.1s if(signum == SIGALRM) { pyfcgi_log(LOG_WARNING, "Master process received SIGALRM !"); return; } else if(signum == SIGINT) { pyfcgi_log(LOG_INFO, "Master process received ctrl+c, exiting..."); } else { pyfcgi_log(LOG_INFO, "Master process received signal %s(%d), exiting...", strsignal(signum), signum); } if(pool_handler_pid) { pyfcgi_log(LOG_INFO, "Killing pool_handler(%d)", pool_handler_pid); kill(pool_handler_pid, SIGTERM); for(i=0; i<5; i++) { ret = waitpid(pool_handler_pid, &status, WNOHANG); if(ret <= 0) { nanosleep(&req, NULL); continue; } pool_handler_pid = 0; break; } } if(monitor_serv_pid) { pyfcgi_log(LOG_INFO, "Killing monitor(%d)", monitor_serv_pid); kill(monitor_serv_pid, SIGTERM); for(i=0; i<5; i++) { ret = waitpid(monitor_serv_pid, &status, WNOHANG); if(ret <= 0) { nanosleep(&req, NULL); continue; } monitor_serv_pid = 0; break; } } if(pool_handler_pid) { pyfcgi_log(LOG_WARNING, "pool_handler(%d) seems to freeze, sending SIGKILL", pool_handler_pid); kill(pool_handler_pid, SIGKILL); } if(monitor_serv_pid) { pyfcgi_log(LOG_WARNING, "pool_handler(%d) seems to freeze, sending SIGKILL", monitor_serv_pid); kill(monitor_serv_pid, SIGKILL); } pyfcgi_log(LOG_INFO, "Master process exiting."); exit(0); } void debug_sighandler(int signum) { pyfcgi_log(LOG_WARNING, "Master process received signal %s(%d)", strsignal(signum), signum); return; } int main(int argc, char **argv) { int emerg_sleep = 3; int child_ret; pid_t rpid; struct sigaction act; short fails, need_wait; //Sleeping on waitpid WNOHANG struct timespec tsleep; char *child_name; act.sa_handler = sighandler; sigemptyset(&act.sa_mask); act.sa_flags = 0; act.sa_restorer = NULL; if(sigaction(SIGTERM, &act, NULL)) { perror("Sigaction error"); exit(4); } if(sigaction(SIGINT, &act, NULL)) { perror("Sigaction error"); exit(4); } act.sa_handler = debug_sighandler; if(sigaction(SIGALRM, &act, NULL)) { perror("Sigaction2 error"); exit(4); } pool_handler_pid = 0; monitor_serv_pid = 0; default_conf(); pyfcgi_name_IPC(getpid()); //name semaphore using master proc PID pyfcgi_logger_init(); pyfcgi_logger_set_ident("MainProc"); if(parse_args(argc, argv)) { return 1; } pyfcgi_log(LOG_INFO, "New server started"); while(1) { if(!pool_handler_pid) { pool_handler_pid = spawn_pool_handler(); if(pool_handler_pid < 0) { fails = 1; pool_handler_pid = 0; } } if(PyFCGI_conf.mon_socket && !monitor_serv_pid) { monitor_serv_pid = pyfcgi_spawn_monitor(); if(monitor_serv_pid < 0) { fails = 1; monitor_serv_pid = 0; } } if(fails) { if(emerg_sleep > 600) { pyfcgi_log(LOG_EMERG, "Abording..."); exit(PYFCGI_FATAL); } fails = 0; pyfcgi_log(LOG_WARNING, "Sleeping %ds", emerg_sleep); emerg_sleep *= 2; continue; } else { emerg_sleep = 3; } need_wait = ((!PyFCGI_conf.mon_socket || monitor_serv_pid) && pool_handler_pid); rpid = waitpid(0, &child_ret, need_wait?0:WNOHANG); if(rpid == pool_handler_pid) { child_name = "Pool handler"; pool_handler_pid = 0; if(!child_ret) { pyfcgi_log(LOG_NOTICE, "Restarting main process after %d requests", MAX_REQS); continue; } } else if(rpid == monitor_serv_pid) { child_name = "Monitor server"; monitor_serv_pid = 0; } else if(rpid == 0 && !need_wait) { tsleep.tv_sec = 0; tsleep.tv_nsec = 100000000; nanosleep(&tsleep, NULL); continue; } else if(rpid < 0) { pyfcgi_log(LOG_EMERG, "Unable to waitpid : %s", strerror(errno)); exit(PYFCGI_FATAL); } else { child_name = "Unknown child"; pyfcgi_log(LOG_WARNING, "Unknown child (PID %d) exited...", rpid); } if(child_ret) { pyfcgi_log(LOG_ERR, "%s exits with error code : %s", child_name, status2str(WEXITSTATUS(child_ret))); if(WIFSIGNALED(child_ret)) { pyfcgi_log(LOG_ERR, "%s terminated by sig %s(%d)", child_name, strsignal(WTERMSIG(child_ret)), WTERMSIG(child_ret)); } } else { pyfcgi_log(LOG_WARNING, "%s exits with no error status 0", child_name); } } closelog(); } /**@mainpage PyFCGI * @section main_what What is PyFCGI ? * PyFCGI is a simple python3 fastcgi runner. * * Usage : Usage : spawn-fcgi [OPTIONS] -- pyfcgi -e PYMODULE [-E PYFUN] [OPTIONS] * * To run foo.entrypoint() python function. * * @subsection main_how_use How to use it ? * * @warning For the moment PyFCGI is under heavy developpement and in early * stage. Everything will change ;o) * * PyFCGI should be runned with spawn-fcgi (or something similar), allowing * to configure & forward environnement variables to libFCGI. * * For the moment no configuration files exists. You have to pass arguments * to pyfcgi using -- argument of spawnn-fcgi * * When called this function will have to send valid CGI data to the webserver * using sys.stdin (the print() function for exemple) like : *Content-type: text/html\\r\\n\\r\\nHello world !\\n * * The function will have access to updated CGI os.environ containing * all informations about a request * * @subsubsection main_how_use_syslog Logging, using syslog * * Right now PyFCGI uses pyfcgi_log() to log stuff using a pyfcgi ident. * PyFCGI logs can be filtered using /etc/rsyslog.d/pyfcgi.conf : *
if ($programname contains 'pyfcgi') then {
	-/var/log/pyfcgi/pyfcgi.log
	stop
}
* * @subsubsection main_how_use_hardcode Hardcoded stuffs * * Right now there is a lot of hardcodd stuff. Fortunatly a vast majority * is in @ref main.c like the minimum and maximum number of workers, or * the number of requests before a worker restart. Some timers and stuff * are harcoded but should not in @ref pyworker.c & @ref responder.c * * @subsection main_how_works How it works ? * * - @ref processes * - @ref main_proc * - @ref work_master_proc * - @ref worker_process * */ /**@page pyfcgi * @brief Python Fast CGI runner * @section man_syn SYNOPSIS * pyfcgi [OPTIONS] * @section man_desc DESCRIPTION * Run WSGI python application in a pool of worker. * * @section man_opt OPTIONS * @subsection man_opt_gen General OPTIONS * @par -h --help * Display help and exit * @par -V --version * Display pyfcgi and Python version and exit * @par -C --config=FILE * load a configuration file * @par -l --listen=SOCK_PATH * fcgi listen socket path. For TCP socket use "IPv4:PORT" syntax ( * "127.0.0.1:9000" by default) * @par -e --pymodule=MODULE_NAME * python entrypoint module name * @par -E --pyapp=FUNC_NAME * python entrypoint function name * @par -A --alt-io * use stdout to communicate with web server instead of entrypoint return as * specified in PEP333 * @par -P --pid-file=PATH * Create a pidfile with master process PID * * @subsection man_opt_pool Worker pool OPTIONS * @par -w --min-worker * minimum worker in the pool * @par -W --max-worker * maximum worker in the pool * @par -f --fast-spawn * If not given there is at least 1s between two child creation. When given * childs may be spawned in small burst. * @par -t --timeout=SECONDS * Request timeout. If the timeout expires the worker process is restarted. * * @subsection man_opt_log Logging & monitoring OPTIONS * @par -L --log=LOGGERSPEC * Add a logfile using syntax "LOGFILE[;FILT][;FMT]" See @ref man_logger_specs * @par -S --syslog * Use syslog for logging * @par -v --verbose * Send all loglines on STDERR * @par -s --socket-server=SOCKURL * Indicate a socket like "tcp://localhost:8765" to listen on, replying status * and statistics. See @ref man_url_spec * * @section man_logger_specs Logfile format * A logger specification use the following format : LOGILE[;FILT][;FMT] with * - LOGFILE the logfile name * - FILT a number (in decimal or hex 0xHH) indicating wich facility/level to * log * - FMT the logline format, following the markup format described bellow * @subsection man_logger_fmt Logline format * Logline's format is indicated using a simple format using fields between * '{' and '}'. Supported fields are : * - {datetime} {datetime:SIZE} {datetime:SIZE:FMT} defines a format and a * constant length for a datetime field. Default : {datetime:25:%F %T%z} * - {level} the loglevel * - {facility} the log facility * - {pid} the process PID * - {ident} the process ident (friendly name) * - {msg} the log message (can appear only once) * * Specials chars '{' & '}' can be escpaed using "{{" and "}}", and all field * names can be abreviated to one character. * * @section man_url_spec Statistics socket URL format * Statistics server listen on a socket specified by an URL like : * * PROT://HOST:[PORT] with PROT one of : * - tcp with HOST a valid INET address * - udp with HOST a valid INET address * - unix with HOST a valid PATH * * @section EXAMPLES * To run foo_pep333.entrypoint() PEP333 application : * * src/pyfcgi -l '127.0.0.1:9000' -S -e foo_pep333 -E entrypoint * * Logfile example : * * src/pyfcgi -l '127.0.0.1:9000' -S -e foo_pep333 -E entrypoint * -L '/tmp/foo.log;0xff;{datetime} {msg} {ident}' * * @section AUTHOR * Written by Yann Weber <yann.weber@member.fsf.org> * * @section COPYRIGHT * Copyright @ 2019 Yann Weber License GPLv3+: GNU GPL version 3 or later * <http://gnu.org/licences/gpl.html>. * * This is free software: you are free to change and redistribute it. * There is NO WARRANTY, to extent permitted by law. * * PyFCGI git repositorie <https://git.yannweb.net/yannweb/pyfcgi> * */