-rw-r--r-- | src/.gitignore | 5 | ||||
-rw-r--r-- | src/Makefile.am | 18 | ||||
-rw-r--r-- | src/configuration.cc | 149 | ||||
-rw-r--r-- | src/configuration.h | 23 | ||||
-rw-r--r-- | src/dudki.cc | 244 | ||||
-rw-r--r-- | src/process.cc | 184 | ||||
-rw-r--r-- | src/process.h | 36 | ||||
-rw-r--r-- | src/util.cc | 21 | ||||
-rw-r--r-- | src/util.h | 20 |
9 files changed, 700 insertions, 0 deletions
diff --git a/src/.gitignore b/src/.gitignore new file mode 100644 index 0000000..e26d996 --- a/dev/null +++ b/src/.gitignore @@ -0,0 +1,5 @@ +.deps +Makefile +Makefile.in +COPYING.cc +dudki diff --git a/src/Makefile.am b/src/Makefile.am new file mode 100644 index 0000000..3810272 --- a/dev/null +++ b/src/Makefile.am @@ -0,0 +1,18 @@ +sbin_PROGRAMS = dudki + +INCLUDES = ${DOTCONF_CFLAGS} +LIBS += ${DOTCONF_LIBS} +AM_CPPFLAGS = \ + -DDEFAULT_CONF_FILE=\"${sysconfdir}/${PACKAGE}.conf\" \ + -DDEFAULT_PID_FILE=\"/var/run/${PACKAGE}.pid\" + +dudki_SOURCES = dudki.cc \ + process.cc process.h \ + configuration.cc configuration.h \ + util.cc util.h \ + COPYING.cc + +COPYING.cc: ${top_srcdir}/COPYING + echo "const char * COPYING =" >$@ || (rm $@;exit 1) + sed 's/"/\\"/g' $< | sed 's/^/\"/' | sed 's/$$/\\n\"/' >>$@ || (rm $@;exit 1) + echo ";" >>$@ || (rm $@;exit 1) diff --git a/src/configuration.cc b/src/configuration.cc new file mode 100644 index 0000000..eb010c1 --- a/dev/null +++ b/src/configuration.cc @@ -0,0 +1,149 @@ +#include <stdexcept> +using namespace std; +#include <dotconf.h> +#include "configuration.h" + +#ifndef DEFAULT_PID_FILE +# define DEFAULT_PID_FILE "/var/run/dudki.pid" +#endif + +configuration::configuration() + : check_interval(60), pidfile(DEFAULT_PID_FILE), + daemonize(true) { + } + +enum dc_ctx { + DCC_ROOT = 1, + DCC_PROCESS = 2 +}; +struct dc_context { + dc_ctx ctx; + configuration* cf; + process* ps; + + dc_context() + : ctx(DCC_ROOT), cf(NULL), ps(NULL) { } +}; + +static DOTCONF_CB(dco_check_interval) { dc_context *dcc = (dc_context*)ctx; + dcc->cf->check_interval = cmd->data.value; + return NULL; +} +static DOTCONF_CB(dco_daemonize) { dc_context *dcc = (dc_context*)ctx; + dcc->cf->daemonize = cmd->data.value; + return NULL; +} + +static DOTCONF_CB(dco_pid_file) { dc_context *dcc = (dc_context*)ctx; + switch(dcc->ctx) { + case DCC_ROOT: + dcc->cf->pidfile = cmd->data.str; + break; + case DCC_PROCESS: + dcc->ps->pidfile = cmd->data.str; + break; + default: + return "Unexpected PidFile"; + } + return NULL; +} +static DOTCONF_CB(dco_mailto_header) { dc_context *dcc = (dc_context*)ctx; + if(cmd->arg_count!=2) + return "Invalid number of arguments"; + string h = cmd->data.list[0]; + string v = cmd->data.list[1]; + switch(dcc->ctx) { + case DCC_ROOT: + dcc->cf->mailto_headers[h] = v; + break; + case DCC_PROCESS: + dcc->ps->mailto_headers[h] = v; + break; + default: + return "Unexpected MailtoHeader"; + } + return NULL; +} +static DOTCONF_CB(dco_notify) { dc_context *dcc = (dc_context*)ctx; + switch(dcc->ctx) { + case DCC_ROOT: + dcc->cf->notify = cmd->data.str; + break; + case DCC_PROCESS: + dcc->ps->notify = cmd->data.str; + break; + default: + return "Unexpected Notify"; + } + return NULL; +} + +static DOTCONF_CB(dco_process) { dc_context *dcc = (dc_context*)ctx; + string id = cmd->data.str; + if(id[id.length()-1]=='>') + id.erase(id.length()-1); + dcc->ps = &(dcc->cf->processes[id]); + dcc->ctx = DCC_PROCESS; + return NULL; +} +static DOTCONF_CB(dco__process) { dc_context *dcc = (dc_context*)ctx; + dcc->ps = NULL; + dcc->ctx = DCC_ROOT; + return NULL; +} + +static DOTCONF_CB(dco_restart_command) { dc_context *dcc = (dc_context*)ctx; + dcc->ps->restart_cmd = cmd->data.str; + return NULL; +} +static DOTCONF_CB(dco_user) { dc_context *dcc = (dc_context*)ctx; + dcc->ps->user = cmd->data.str; + return NULL; +} +static DOTCONF_CB(dco_group) { dc_context *dcc = (dc_context*)ctx; + dcc->ps->group = cmd->data.str; + return NULL; +} +static DOTCONF_CB(dco_chroot) { dc_context *dcc = (dc_context*)ctx; + dcc->ps->chroot = cmd->data.str; + return NULL; +} + +static const configoption_t dc_options[] = { + { "CheckInterval", ARG_INT, dco_check_interval, NULL, DCC_ROOT }, + { "Daemonize", ARG_TOGGLE, dco_daemonize, NULL, DCC_ROOT }, + { "PidFile", ARG_STR, dco_pid_file, NULL, DCC_ROOT|DCC_PROCESS }, + { "MailtoHeader", ARG_STR, dco_mailto_header, NULL, DCC_ROOT|DCC_PROCESS }, + { "Notify", ARG_STR, dco_notify, NULL, DCC_ROOT|DCC_PROCESS }, + { "<Process", ARG_STR, dco_process, NULL, DCC_ROOT }, + { "RestartCommand", ARG_STR, dco_restart_command, NULL, DCC_PROCESS }, + { "User", ARG_STR, dco_user, NULL, DCC_PROCESS }, + { "Group", ARG_STR, dco_group, NULL, DCC_PROCESS }, + { "Chroot", ARG_STR, dco_chroot, NULL, DCC_PROCESS }, + { "</Process>", ARG_NONE, dco__process, NULL, DCC_PROCESS }, + LAST_OPTION +}; + +static const char *dc_context_checker(command_t *cmd,unsigned long mask) { + dc_context *dcc = (dc_context*)cmd->context; + if( (mask==CTX_ALL) || ((mask&dcc->ctx)==dcc->ctx) ) + return NULL; + return "misplaced option"; +} +static FUNC_ERRORHANDLER(dc_error_handler) { + throw runtime_error(string("error parsing config file: ")+msg); +} + +void configuration::parse(const string& cfile) { + struct dc_context dcc; + dcc.cf = this; + dcc.ctx = DCC_ROOT; + configfile_t *cf = dotconf_create((char*)cfile.c_str(),dc_options,(context_t*)&dcc,CASE_INSENSITIVE); + if(!cf) + throw runtime_error(string(__PRETTY_FUNCTION__)+": failed to dotconf_create()"); + cf->errorhandler = (dotconf_errorhandler_t) dc_error_handler; + cf->contextchecker = (dotconf_contextchecker_t) dc_context_checker; + if(!dotconf_command_loop(cf)) + throw runtime_error(string(__PRETTY_FUNCTION__)+": failed to dotconf_command_loop()"); + dotconf_cleanup(cf); +} diff --git a/src/configuration.h b/src/configuration.h new file mode 100644 index 0000000..314af92 --- a/dev/null +++ b/src/configuration.h @@ -0,0 +1,23 @@ +#ifndef __CONFIGURATION_H +#define __CONFIGURATION_H + +#include <string> +using namespace std; +#include "process.h" + +class configuration { + public: + processes_t processes; + + int check_interval; + string pidfile; + bool daemonize; + headers_t mailto_headers; + string notify; + + configuration(); + + void parse(const string& cfile); +}; + +#endif /* __CONFIGURATION_H */ diff --git a/src/dudki.cc b/src/dudki.cc new file mode 100644 index 0000000..3c50e56 --- a/dev/null +++ b/src/dudki.cc @@ -0,0 +1,244 @@ +#include <unistd.h> +#include <signal.h> +#include <syslog.h> +#include <iostream> +#include <fstream> +#include <stdexcept> +using namespace std; +#include "configuration.h" +#include "util.h" + +#include "config.h" +#ifdef HAVE_GETOPT_H +# include <getopt.h> +#endif + +#ifndef DEFAULT_CONF_FILE +# define DEFAULT_CONF_FILE "/etc/dudki.conf" +#endif + +#define PHEADER PACKAGE " Version " VERSION +#define PCOPY "Copyright (c) 2004 Klever Group" + +bool finishing = false; +static char **_argv = NULL; + +static void lethal_signal_handler(int signum) { + syslog(LOG_NOTICE,"Lethal signal received. Terminating."); + finishing = true; +} +static void sighup_handler(int signum) { + syslog(LOG_NOTICE,"SUGHUP received, reloading."); + execvp(_argv[0],_argv); +} + +void check_herd(configuration& config) { + for(processes_t::iterator i=config.processes.begin();i!=config.processes.end();++i) + i->second.check(i->first,config); +} + +void signal_self(const configuration& config,int signum) { + ifstream pids(config.pidfile.c_str(),ios::in); + if(!pids) + throw runtime_error("Can't detect running instance"); + pid_t pid = 0; + pids >> pid; + if(!pid) + throw runtime_error("Can't detect running instance"); + if(pid==getpid()) + throw 0; + if(kill(pid,signum)) + throw runtime_error("Failed to signal running instance"); +} + +int main(int argc,char **argv) { + try { + _argv = new char*[argc+1]; + if(!_argv) + throw runtime_error("memory allocation problem at the very start"); + memmove(_argv,argv,sizeof(*_argv)*(argc+1)); + string config_file = DEFAULT_CONF_FILE; + enum { + op_default, + op_work, + op_hup, + op_term, + op_check, + op_ensure, + op_test + } op = op_default; + while(true) { +#define SHORTOPTSTRING "f:hVLrkcet" +#ifdef HAVE_GETOPT_LONG + static struct option opts[] = { + { "help", no_argument, 0, 'h' }, + { "usage", no_argument, 0, 'h' }, + { "version", no_argument, 0, 'V' }, + { "license", no_argument, 0, 'L' }, + { "config", required_argument, 0, 'f' }, + { "kill", no_argument, 0, 'k' }, + { "reload", no_argument, 0, 'r' }, + { "check", no_argument, 0, 'c' }, + { "ensure", no_argument, 0, 'e' }, + { "test", no_argument, 0, 't' }, + { NULL, 0, 0, 0 } + }; + int c = getopt_long(argc,argv,SHORTOPTSTRING,opts,NULL); +#else /* !HAVE_GETOPT_LONG */ + int c = getopt(argc,argv,SHORTOPTSTRING); +#endif /* /HAVE_GETOPT_LONG */ + if(c==-1) + break; + switch(c) { + case 'h': + cerr << PHEADER << endl + << PCOPY << endl << endl << +#ifdef HAVE_GETOPT_LONG + " -h, --help\n" + " --usage display this text\n" + " -V, --version display version number\n" + " -L, --license show license\n" + " -f filename, --config=filename\n" + " specify the configuration file to use\n" + "\n" + " -k, --kill stop running instance\n" + " -r, --reload reload running instance (send SIGHUP)\n" + " -c, --check check if dudki is running\n" + " -e, --ensure ensure that dudki is running\n" + " -t, --test test configuration file and exit" +#else /* !HAVE_GETOPT_LONG */ + " -h display this text\n" + " -V display version number\n" + " -L show license\n" + " -f filename specify the configuration file to use\n" + "\n" + " -k stop running instance\n" + " -r reload running instance (send SIGHUP)\n" + " -c check if dudki is running\n" + " -e ensure that dudki is running\n" + " -t test configuration file and exit" +#endif /* /HAVE_GETOPT_LONG */ + << endl; + exit(0); + break; + case 'V': + cerr << VERSION << endl; + exit(0); + break; + case 'L': + extern const char *COPYING; + cerr << COPYING << endl; + exit(0); + break; + case 'f': + config_file = optarg; + break; + case 'k': + if(op!=op_default) { + cerr << "Can't obey two or more orders at once" << endl; + exit(1); + } + op = op_term; + break; + case 'r': + if(op!=op_default) { + cerr << "Can't obey two or more orders at once" << endl; + exit(1); + } + op = op_hup; + break; + case 'c': + if(op!=op_default) { + cerr << "Can't obey two or more orders at once" << endl; + exit(1); + } + op = op_check; + break; + case 'e': + if(op!=op_default) { + cerr << "Can't obey two or more orders at once" << endl; + exit(1); + } + op = op_ensure; + break; + case 't': + if(op!=op_default) { + cerr << "Can't obey two or more orders at once" << endl; + exit(1); + } + op = op_test; + break; + default: + cerr << "Huh??" << endl; + exit(1); + break; + } + } + const char *sid = *argv; + const char *t; + while(t = index(sid,'/')) { + sid = t; sid++; + } + openlog(sid,LOG_CONS|LOG_PERROR|LOG_PID,LOG_DAEMON); + configuration config; + config.parse(config_file); + switch(op) { + case op_test: + cerr << "Configuration OK" << endl; + break; + case op_hup: + signal_self(config,SIGHUP); + break; + case op_term: + signal_self(config,SIGTERM); + break; + case op_check: + try{ + signal_self(config,0); + exit(0); + }catch(exception& e) { + exit(1); + } + case op_ensure: + try { + signal_self(config,0); + break; + }catch(exception& e) { + syslog(LOG_NOTICE,"The dudki process is down, taking its place"); + config.daemonize = true; + }catch(int zero) { + // we throw zero in case we're ensuring that this very process is running. + // we don't have to daemonize if we're daemonic. + config.daemonize = false; + } + case op_default: + case op_work: + { + if(config.daemonize) { + pid_t pf = fork(); + if(pf<0) + throw runtime_error(string(__PRETTY_FUNCTION__)+": failed to fork()"); + if(pf) { + _exit(0); + } + } + pid_file pidfile; + pidfile.set(config.pidfile); + signal(SIGINT,lethal_signal_handler); + signal(SIGABRT,lethal_signal_handler); + signal(SIGTERM,lethal_signal_handler); + signal(SIGHUP,sighup_handler); + while(!finishing) { + check_herd(config); + sleep(config.check_interval); + } + } + break; + default: + throw runtime_error(string(__PRETTY_FUNCTION__)+": internal error"); + } + }catch(exception& e) { + cerr << "Oops: " << e.what() << endl; + return 1; + } +} diff --git a/src/process.cc b/src/process.cc new file mode 100644 index 0000000..fda35e8 --- a/dev/null +++ b/src/process.cc @@ -0,0 +1,184 @@ +#include <stdio.h> +#include <sys/types.h> +#include <unistd.h> +#include <signal.h> +#include <pwd.h> +#include <grp.h> +#include <sys/wait.h> +#include <syslog.h> +#include <errno.h> +#include <iostream> +#include <fstream> +#include <stdexcept> +using namespace std; +#include "process.h" +#include "configuration.h" + +void process::check(const string& id,configuration& config) { + bool running = false; + ifstream pids(pidfile.c_str(),ios::in); + if(pids) { + pid_t pid = 0; + pids >> pid; + pids.close(); + if(pid) { + if(!kill(pid,0)) { + running = true; + } + } + } + if(running){ + patience = 0; + }else{ + if(patience>60) { // TODO: configurable + patience = 0; + }else{ + if(patience<10) { // TODO: configurable + syslog(LOG_NOTICE,"The process '%s' is down, trying to launch.",id.c_str()); + do_notify(id,"Starting up", + "The named process seems to be down. Dudki will try\n" + "to revive it by running the specified command.\n", + config); + try { + launch(id,config); + }catch(exception& e) { + syslog(LOG_ERR,"Error trying to launch process '%s': %s",id.c_str(),e.what()); + } + }else if(patience==10){ // TODO: configurable like the above + syslog(LOG_NOTICE,"Giving up on process '%s' for a while",id.c_str()); + do_notify(id,"Giving up", + "After a number of attempts to relaunch the named process\n" + "It still seems to be down. Dudki is giving up attempts\n" + "to revive the process for a while.\n", + config); + } + patience++; + } + } +} + +void process::launch(const string& id,configuration& config) { + uid_t uid = 0; + if(!user.empty()) { + struct passwd *ptmp = getpwnam(user.c_str()); + if(ptmp) { + uid = ptmp->pw_uid; + }else{ + errno=0; + uid = strtol(user.c_str(),NULL,0); + if(errno) + throw runtime_error("Failed to resolve User value to uid"); + } + } + gid_t gid = 0; + if(!group.empty()) { + struct group *gtmp = getgrnam(group.c_str()); + if(gtmp) { + gid = gtmp->gr_gid; + }else{ + errno = 0; + gid = strtol(group.c_str(),NULL,0); + if(errno) + throw runtime_error("Failed to reslove Group value to gid"); + } + } + pid_t p = fork(); + if(p<0) + throw runtime_error(string(__PRETTY_FUNCTION__)+": failed to fork()"); + if(!p) { + // child + try { + setsid(); + if(!chroot.empty()) { + if(::chroot(chroot.c_str())) + throw runtime_error(string(__PRETTY_FUNCTION__)+": failed to chroot()"); + } + if(!group.empty()) { + // TODO: initgroups()? + if((getgid()!=gid) && setgid(gid)) + throw runtime_error(string(__PRETTY_FUNCTION__)+": failed to setgid()"); + } + if(!user.empty()) { + if((getuid()!=uid) && setuid(uid)) + throw runtime_error(string(__PRETTY_FUNCTION__)+": failed to setuid()"); + } + char *argv[] = { "/bin/sh", "-c", (char*)restart_cmd.c_str(), NULL }; + close(0); close(1); close(2); + execv("/bin/sh",argv); + }catch(exception& e) { + syslog(LOG_ERR,"Error trying to launch process '%s': %s",id.c_str(),e.what()); + } + _exit(-1); + } + // parent + int rv; + if(waitpid(p,&rv,0)<0) + throw runtime_error(string(__PRETTY_FUNCTION__)+": failed to waitpid()"); +} + +void process::do_notify(const string& id,const string& event,const string& description,configuration& config) { + string the_notify; + if(!notify.empty()) + the_notify=notify; + else if(!config.notify.empty()) + the_notify=config.notify; + else + return; + try { + string::size_type colon = the_notify.find(':'); + if(colon==string::npos) + throw runtime_error("invalid notify action specification"); + string nschema = the_notify.substr(0,colon); + string ntarget = the_notify.substr(colon+1); + if(nschema=="mailto") { + notify_mailto(ntarget,id,event,description,config); + }else + throw runtime_error("unrecognized notification schema"); + }catch(exception& e) { + syslog(LOG_ERR,"Notification error: %s",e.what()); + } +} + +void process::notify_mailto(const string& email,const string& id,const string& event,const string& description,configuration& config) { + int files[2]; + if(pipe(files)) + throw runtime_error("Failed to pipe()"); + pid_t pid = vfork(); + if(pid==-1) { + close(files[0]); + close(files[1]); + throw runtime_error("Failed to vfork()"); + } + if(!pid) { + // child + if(dup2(files[0],0)!=0) + _exit(-1); + close(1); + close(files[0]); + close(files[1]); + execl("/usr/sbin/sendmail","usr/sbin/sendmail","-i",email.c_str(),NULL); + _exit(-1); + } + // parent + close(files[0]); + FILE *mta = fdopen(files[1],"w"); + for(headers_t::const_iterator i=mailto_headers.begin();i!=mailto_headers.end();++i) { + fprintf(mta,"%s: %s\n",i->first.c_str(),i->second.c_str()); + } + for(headers_t::const_iterator i=config.mailto_headers.begin();i!=config.mailto_headers.end();++i) { + if(mailto_headers.find(i->first)!=mailto_headers.end()) + continue; + fprintf(mta,"%s: %s\n",i->first.c_str(),i->second.c_str()); + } + fprintf(mta, + "Subject: [%s] %s\n\n" + "%s\n" + "---\n" + "This message was sent automatically by the 'dudki' daemon\n", + id.c_str(), event.c_str(), + description.c_str() ); + fclose(mta); + int status; + waitpid(pid,&status,0); + // TODO: check the return code +} diff --git a/src/process.h b/src/process.h new file mode 100644 index 0000000..b6d7091 --- a/dev/null +++ b/src/process.h @@ -0,0 +1,36 @@ +#ifndef __PROCESS_H +#define __PROCESS_H + +#include <string> +#include <map> +using namespace std; + +class configuration; + +typedef map<string,string> headers_t; + +class process { + public: + string pidfile; + string restart_cmd; + string notify; + string user; + string group; + string chroot; + headers_t mailto_headers; + + int patience; + + process() + : patience(0) { } + + void check(const string& id,configuration& config); + void launch(const string& id,configuration& config); + void do_notify(const string& id,const string& event,const string& description,configuration& config); + void notify_mailto(const string& email,const string& id,const string& event, + const string& description,configuration& config); +}; + +typedef map<string,process> processes_t; + +#endif /* __PROCESS_H */ diff --git a/src/util.cc b/src/util.cc new file mode 100644 index 0000000..afb2641 --- a/dev/null +++ b/src/util.cc @@ -0,0 +1,21 @@ +#include <unistd.h> +#include <fstream> +#include <stdexcept> +using namespace std; +#include "util.h" + +void pid_file::set(const string& f,bool u) { + ofstream of(f.c_str(),ios::trunc); + if(!of) + throw runtime_error(string(__PRETTY_FUNCTION__)+": failed to open file for writing pid"); + of << getpid() << endl; + of.close(); + file_name = f; + unlink_pid = u; +} +void pid_file::unlink() { + if(!unlink_pid) + return; + ::unlink(file_name.c_str()); +} + diff --git a/src/util.h b/src/util.h new file mode 100644 index 0000000..314d8e2 --- a/dev/null +++ b/src/util.h @@ -0,0 +1,20 @@ +#ifndef __UTIL_H +#define __UTIL_H + +#include <string> +using namespace std; + +class pid_file { + public: + string file_name; + bool unlink_pid; + + pid_file() + : unlink_pid(false) { } + ~pid_file() { unlink(); } + + void set(const string& f,bool u=true); + void unlink(); +}; + +#endif /* __UTIL_H */ |