diff --git a/init/.gitignore b/init/.gitignore index be9f2b05..7af9ceaf 100644 --- a/init/.gitignore +++ b/init/.gitignore @@ -1,2 +1,3 @@ init +service *.o diff --git a/init/Makefile b/init/Makefile index 6ec3626d..97400b99 100644 --- a/init/Makefile +++ b/init/Makefile @@ -8,26 +8,26 @@ CFLAGS?=$(OPTLEVEL) CFLAGS:=$(CFLAGS) -Wall -Wextra -BINARY=init +BINARIES=\ +init \ +service \ -OBJS=\ -init.o \ +MANPAGES8=\ +init.8 \ +service.8 \ -all: $(BINARY) +all: $(BINARIES) .PHONY: all install clean -$(BINARY): $(OBJS) - $(CC) $(CFLAGS) $(OBJS) -o $(BINARY) -lmount $(LIBS) - -%.o: %.c - $(CC) -std=gnu11 $(CFLAGS) $(CPPFLAGS) -c $< -o $@ +%: %.c + $(CC) -std=gnu11 $(CFLAGS) $(CPPFLAGS) $< -o $@ -lmount $(LIBS) install: all mkdir -p $(DESTDIR)$(SBINDIR) - install $(BINARY) $(DESTDIR)$(SBINDIR) + install $(BINARIES) $(DESTDIR)$(SBINDIR) mkdir -p $(DESTDIR)$(MANDIR)/man8 - cp init.8 $(DESTDIR)$(MANDIR)/man8/init.8 + install $(MANPAGES8) $(DESTDIR)$(MANDIR)/man8 clean: - rm -f $(BINARY) $(OBJS) *.o + rm -f $(BINARIES) $(OBJS) *.o diff --git a/init/init.8 b/init/init.8 index fa4019a5..26106934 100644 --- a/init/init.8 +++ b/init/init.8 @@ -301,6 +301,7 @@ exits with the same exit status as its target session if it terminates normally. .Xr login 8 , .Xr poweroff 8 , .Xr reboot 8 , +.Xr service 8 , .Xr sysmerge 8 , .Xr update-initrd 8 .Sh SECURITY CONSIDERATIONS diff --git a/init/init.c b/init/init.c index 44795c9b..55f1b474 100644 --- a/init/init.c +++ b/init/init.c @@ -20,8 +20,10 @@ #include #include #include +#include #include #include +#include #include #include @@ -125,6 +127,19 @@ enum daemon_state #define NUM_DAEMON_STATES (DAEMON_STATE_FINISHED + 1) +const char* daemon_state_names[] = +{ + "terminated", + "scheduled", + "waiting", + "satisfied", + "starting", + "running", + "terminating", + "finishing", + "finished", +}; + struct daemon; struct dependency @@ -137,6 +152,7 @@ struct dependency #define DEPENDENCY_FLAG_REQUIRE (1 << 0) #define DEPENDENCY_FLAG_AWAIT (1 << 1) #define DEPENDENCY_FLAG_EXIT_CODE (1 << 2) +#define DEPENDENCY_FLAG_REFERENCED (1 << 30) enum log_method { @@ -223,7 +239,7 @@ struct daemon bool need_tty; bool was_ready; bool was_terminated; - bool was_dereferenced; + bool want_restart; bool timeout_set; }; @@ -255,10 +271,32 @@ struct daemon_config mode_t log_file_mode; }; +struct server +{ + size_t index; + const char* path; + int fd; +}; + +struct connection +{ + size_t index; + char* input; + size_t input_used; + size_t input_size; + char* output; + size_t output_used; + size_t output_size; + int fd; + size_t state; +}; + enum communication_type { COMMUNICATION_TYPE_OUTPUT, COMMUNICATION_TYPE_READY, + COMMUNICATION_TYPE_SERVER, + COMMUNICATION_TYPE_CONNECTION, }; struct communication @@ -323,6 +361,77 @@ static char chain_location[] = "/tmp/fs.XXXXXX"; static bool chain_location_dev_made = false; static char chain_location_dev[] = "/tmp/fs.XXXXXX/dev"; +// TODO: Provide this functionality in libc. +static int signal_lookup(const char* signame) +{ + if ( !strcmp(signame, "SIGHUP") ) return SIGHUP; + if ( !strcmp(signame, "SIGINT") ) return SIGINT; + if ( !strcmp(signame, "SIGQUIT") ) return SIGQUIT; + if ( !strcmp(signame, "SIGILL") ) return SIGILL; + if ( !strcmp(signame, "SIGTRAP") ) return SIGTRAP; + if ( !strcmp(signame, "SIGABRT") ) return SIGABRT; + if ( !strcmp(signame, "SIGBUS") ) return SIGBUS; + if ( !strcmp(signame, "SIGFPE") ) return SIGFPE; + if ( !strcmp(signame, "SIGKILL") ) return SIGKILL; + if ( !strcmp(signame, "SIGUSR1") ) return SIGUSR1; + if ( !strcmp(signame, "SIGSEGV") ) return SIGSEGV; + if ( !strcmp(signame, "SIGUSR2") ) return SIGUSR2; + if ( !strcmp(signame, "SIGPIPE") ) return SIGPIPE; + if ( !strcmp(signame, "SIGALRM") ) return SIGALRM; + if ( !strcmp(signame, "SIGTERM") ) return SIGTERM; + if ( !strcmp(signame, "SIGSYS") ) return SIGSYS; + if ( !strcmp(signame, "SIGCHLD") ) return SIGCHLD; + if ( !strcmp(signame, "SIGCONT") ) return SIGCONT; + if ( !strcmp(signame, "SIGSTOP") ) return SIGSTOP; + if ( !strcmp(signame, "SIGTSTP") ) return SIGTSTP; + if ( !strcmp(signame, "SIGTTIN") ) return SIGTTIN; + if ( !strcmp(signame, "SIGTTOU") ) return SIGTTOU; + if ( !strcmp(signame, "SIGURG") ) return SIGURG; + if ( !strcmp(signame, "SIGXCPU") ) return SIGXCPU; + if ( !strcmp(signame, "SIGXFSZ") ) return SIGXFSZ; + if ( !strcmp(signame, "SIGVTALRM") ) return SIGVTALRM; + if ( !strcmp(signame, "SIGPWR") ) return SIGPWR; + if ( !strcmp(signame, "SIGWINCH") ) return SIGWINCH; + return 0; +} + +// TODO: Provide this functionality in libc. +static const char* signal_describe(int sig) +{ + switch ( sig ) + { + case SIGHUP: return "SIGHUP"; + case SIGINT: return "SIGINT"; + case SIGQUIT: return "SIGQUIT"; + case SIGILL: return "SIGILL"; + case SIGTRAP: return "SIGTRAP"; + case SIGABRT: return "SIGABRT"; + case SIGBUS: return "SIGBUS"; + case SIGFPE: return "SIGFPE"; + case SIGKILL: return "SIGKILL"; + case SIGUSR1: return "SIGUSR1"; + case SIGSEGV: return "SIGSEGV"; + case SIGUSR2: return "SIGUSR2"; + case SIGPIPE: return "SIGPIPE"; + case SIGALRM: return "SIGALRM"; + case SIGTERM: return "SIGTERM"; + case SIGSYS: return "SIGSYS"; + case SIGCHLD: return "SIGCHLD"; + case SIGCONT: return "SIGCONT"; + case SIGSTOP: return "SIGSTOP"; + case SIGTSTP: return "SIGTSTP"; + case SIGTTIN: return "SIGTTIN"; + case SIGTTOU: return "SIGTTOU"; + case SIGURG: return "SIGURG"; + case SIGXCPU: return "SIGXCPU"; + case SIGXFSZ: return "SIGXFSZ"; + case SIGVTALRM: return "SIGVTALRM"; + case SIGPWR: return "SIGPWR"; + case SIGWINCH: return "SIGWINCH"; + default: return "UNKNOWN"; + } +} + static void signal_handler(int signum) { if ( getpid() != main_pid ) @@ -849,10 +958,18 @@ static void log_status(const char* status, const char* format, ...) fprintf(stderr, "[\e[91mFAILED\e[m] "); else if ( !strcmp(status, "stopping") ) fprintf(stderr, "[ ] "); + else if ( !strcmp(status, "killing") ) + fprintf(stderr, "[ ] "); + else if ( !strcmp(status, "restart") ) + fprintf(stderr, "[ ] "); + else if ( !strcmp(status, "restarting") ) + fprintf(stderr, "[ ] "); else if ( !strcmp(status, "stopped") ) fprintf(stderr, "[ \e[92mOK\e[m ] "); else if ( !strcmp(status, "timeout") ) fprintf(stderr, "[\e[93m TIME \e[m] "); + else if ( !strcmp(status, "signal") ) + fprintf(stderr, "[SIGNAL] "); else fprintf(stderr, "[ ?? ] "); vfprintf(stderr, format, ap); @@ -1743,6 +1860,29 @@ static struct daemon* daemon_create_unconfigured(const char* name) return daemon; } +// TODO: Variant that checks if the daemon exists in the filesystem and returns +// early so the RPCs can avoid a later error. +static struct daemon* daemon_find_or_create(const char* name) +{ + // TODO: What happens for virtual daemons? + struct daemon* daemon = daemon_find_by_name(name); + // TODO: non-fatal daemon_create_unconfigured + if ( !daemon ) + daemon = daemon_create_unconfigured(name); + return daemon; +} + +static struct dependency* daemon_find_dependency(struct daemon* daemon, + const char* target) +{ + for ( size_t i = 0; i < daemon->dependencies_used; i++ ) + { + if ( !strcmp(daemon->dependencies[i]->target->name, target) ) + return daemon->dependencies[i]; + } + return NULL; +} + static bool daemon_add_dependency(struct daemon* daemon, struct daemon* target, int flags) @@ -1773,6 +1913,7 @@ static bool daemon_add_dependency(struct daemon* daemon, if ( flags & DEPENDENCY_FLAG_EXIT_CODE ) daemon->exit_code_from = dependency; target->reference_count++; + dependency->flags |= DEPENDENCY_FLAG_REFERENCED; return true; } @@ -1879,12 +2020,73 @@ static struct daemon* daemon_create(struct daemon_config* daemon_config) return daemon; } -static void schedule_daemon(struct daemon* daemon) +static void daemon_schedule(struct daemon* daemon) { assert(daemon->state == DAEMON_STATE_TERMINATED); daemon_change_state_list(daemon, DAEMON_STATE_SCHEDULED); } +// TODO: Untagle recursion. +static void daemon_on_finished(struct daemon* daemon); + +static void daemon_terminate(struct daemon* daemon) +{ + if ( daemon->state != DAEMON_STATE_SCHEDULED && + daemon->state != DAEMON_STATE_SATISFIED && + daemon->state != DAEMON_STATE_STARTING && + daemon->state != DAEMON_STATE_RUNNING ) + return; + if ( daemon->was_terminated ) + return; + daemon->was_terminated = true; + daemon->want_restart = false; + daemon_change_state_list(daemon, DAEMON_STATE_TERMINATING); + if ( 0 < daemon->pid ) + { + log_status("stopping", "Stopping %s...\n", daemon->name); + kill(daemon->pid, SIGTERM); + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + daemon->timeout = timespec_add(now, timespec_make(30, 0)); + daemon->timeout_set = true; + } + else + daemon_on_finished(daemon); +} + +static void daemon_request_restart(struct daemon* daemon) +{ + if ( !daemon->reference_count ) + return; + if ( daemon->state == DAEMON_STATE_TERMINATED || + daemon->state == DAEMON_STATE_SCHEDULED || + daemon->state == DAEMON_STATE_WAITING || + daemon->state == DAEMON_STATE_SATISFIED ) + return; + if ( !daemon->want_restart ) + { + log_status("restart", "Restart requested of %s.\n", daemon->name); + if ( daemon->state == DAEMON_STATE_STARTING || + daemon->state == DAEMON_STATE_RUNNING ) + daemon_terminate(daemon); + daemon->want_restart = true; + } + if ( daemon->state == DAEMON_STATE_FINISHED || + daemon->state == DAEMON_STATE_FINISHED ) + { + log_status("restarting", "Restarting %s.\n", daemon->name); + daemon_change_state_list(daemon, DAEMON_STATE_SCHEDULED); + // TODO: Update dependencies_ready. + // TODO: Update dependencies_finished. + // TODO: Update dependencies_failed. + daemon->was_ready = false; + daemon->was_terminated = false; + daemon->want_restart = false; + // TODO: was_dereferenced? + daemon->timeout_set = false; // TODO: Reset before going to FINISHING. + } +} + static void daemon_on_finished(struct daemon* daemon) { assert(daemon->state != DAEMON_STATE_FINISHING); @@ -1896,17 +2098,36 @@ static void daemon_on_finished(struct daemon* daemon) else log_status("finished", "Finished %s.\n", daemon->name); daemon_change_state_list(daemon, DAEMON_STATE_FINISHING); + if ( daemon->want_restart ) + daemon_request_restart(daemon); } -static void daemon_terminate(struct daemon* daemon) +static void daemon_request_start(struct daemon* daemon) { - assert(!daemon->was_terminated); + if ( daemon->state == DAEMON_STATE_TERMINATED ) + daemon_schedule(daemon); + else if ( daemon->state == DAEMON_STATE_TERMINATING || + daemon->state == DAEMON_STATE_FINISHING || + daemon->state == DAEMON_STATE_FINISHED ) + daemon_request_restart(daemon); +} + +static void daemon_kill(struct daemon* daemon) +{ + if ( daemon->state != DAEMON_STATE_SCHEDULED && + daemon->state != DAEMON_STATE_SATISFIED && + daemon->state != DAEMON_STATE_STARTING && + daemon->state != DAEMON_STATE_RUNNING && + daemon->state != DAEMON_STATE_TERMINATING ) + return; daemon->was_terminated = true; + daemon->want_restart = false; daemon_change_state_list(daemon, DAEMON_STATE_TERMINATING); if ( 0 < daemon->pid ) { - log_status("stopping", "Stopping %s.\n", daemon->name); - kill(daemon->pid, SIGTERM); + log_status("killing", "Killing %s.\n", daemon->name); + kill(daemon->pid, SIGKILL); + // TODO: Is this needed? Or better to do with zero timeout? struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); daemon->timeout = timespec_add(now, timespec_make(30, 0)); @@ -1916,9 +2137,37 @@ static void daemon_terminate(struct daemon* daemon) daemon_on_finished(daemon); } +static void daemon_reload(struct daemon* daemon) +{ + // TODO: If starting, schedule reload upon RUNNING. + if ( daemon->state != DAEMON_STATE_RUNNING ) + return; + if ( 0 < daemon->pid ) + { + log_status("reload", "Reloading %s.\n", daemon->name); + kill(daemon->pid, SIGHUP); // TODO: Configuration. + } +} + +static void daemon_signal(struct daemon* daemon, const char* signame, int sig) +{ + // TODO: If starting, schedule reload upon RUNNING. + if ( daemon->state != DAEMON_STATE_STARTING && + daemon->state != DAEMON_STATE_RUNNING && + daemon->state != DAEMON_STATE_TERMINATING ) + return; + + if ( 0 < daemon->pid ) + { + log_status("signal", "Sending %s to %s.\n", signame, daemon->name); + kill(daemon->pid, sig); + } +} + static void daemon_on_not_referenced(struct daemon* daemon) { assert(daemon->reference_count == 0); + daemon->want_restart = false; switch ( daemon->state ) { case DAEMON_STATE_TERMINATED: @@ -1948,12 +2197,19 @@ static void daemon_dereference(struct daemon* daemon) daemon_on_not_referenced(daemon); } +static void daemon_dereference_dependency(struct dependency* dependency) +{ + if ( dependency->flags & DEPENDENCY_FLAG_REFERENCED ) + { + daemon_dereference(dependency->target); + dependency->flags &= ~(DEPENDENCY_FLAG_REFERENCED); + } +} + static void daemon_dereference_dependencies(struct daemon* daemon) { - assert(!daemon->was_dereferenced); - daemon->was_dereferenced = true; for ( size_t i = 0; i < daemon->dependencies_used; i++ ) - daemon_dereference(daemon->dependencies[i]->target); + daemon_dereference_dependency(daemon->dependencies[i]); } static void daemon_on_dependency_ready(struct dependency* dependency) @@ -2016,12 +2272,108 @@ static void daemon_on_dependency_finished(struct dependency* dependency) daemon_on_finished(daemon); } +static bool daemon_depend(struct daemon* source, + struct daemon* target, + int flags, + bool ensure_start) +{ + struct dependency* dependency = + daemon_find_dependency(source, target->name); + if ( dependency ) + { + // TODO: Updating the flags could be tricky and unsafe. + return true; + } + else if ( !daemon_add_dependency(source, target, flags) ) + return false; + // No need to start the target if the source isn't supposed to be running. + if ( source->state == DAEMON_STATE_TERMINATED ) + return true; + // No need to start the target if the source is a non-virtual daemon that is + // terminating, or a virtual daemon with exit-code that is terminating. + if ( (source->argv || source->exit_code_from) && + (source->state == DAEMON_STATE_TERMINATING || + source->state == DAEMON_STATE_FINISHING || + source->state == DAEMON_STATE_FINISHED) ) + return true; + // No need to start the target if it is finishing, unless we are asked to + // ensure it is started up again. + // TODO: ensure_start can be always true once there's a proper one shot + // vs persistent daemon concept. + if ( !ensure_start && + (target->state == DAEMON_STATE_TERMINATING || + target->state == DAEMON_STATE_FINISHING || + target->state == DAEMON_STATE_FINISHED) ) + return true; + if ( target->state == DAEMON_STATE_TERMINATED || + target->state == DAEMON_STATE_TERMINATING || + target->state == DAEMON_STATE_FINISHING || + target->state == DAEMON_STATE_FINISHED ) + daemon_request_start(target); + if ( (target->state == DAEMON_STATE_TERMINATED || + target->state == DAEMON_STATE_SCHEDULED || + target->state == DAEMON_STATE_WAITING) ) + { + if ( source->state == DAEMON_STATE_SATISFIED ) + daemon_change_state_list(target, DAEMON_STATE_WAITING); + else if ( source->state == DAEMON_STATE_TERMINATING || + source->state == DAEMON_STATE_FINISHING || + source->state == DAEMON_STATE_FINISHED ) + { + // TODO: Propagate recursive dependents to RUNNING. + // TODO: dependencies_finished and such + daemon_change_state_list(source, DAEMON_STATE_RUNNING); + } + } + return true; +} + +static void daemon_undepend(struct daemon* source, + struct daemon* target, + bool ensure_stop) +{ + for ( size_t i = 0; i < source->dependencies_used; i++ ) + { + if ( source->dependencies[i]->target != target ) + continue; + struct dependency* dependency = source->dependencies[i]; + size_t last = --source->dependencies_used; + if ( i != last ) + source->dependencies[i] = source->dependencies[last]; + for ( size_t n = 0; n < target->dependents_used; n++ ) + { + if ( target->dependents[n] != dependency ) + continue; + last = --target->dependents_used; + if ( n != last ) + target->dependents[n] = target->dependents[last]; + break; + } + daemon_dereference_dependency(dependency); + free(dependency); + // TODO: Correct dependencies_ready. + // TODO: Correct dependencies_finished. + // TODO: Correct dependencies_failed. + // TODO: State transition if now SATISFIED or FINISHED. + break; + } + if ( ensure_stop && + (target->state == DAEMON_STATE_STARTING || + target->state == DAEMON_STATE_RUNNING) ) + daemon_terminate(target); +} + static void daemon_finish(struct daemon* daemon) { assert(daemon->state != DAEMON_STATE_FINISHED); if ( !daemon->was_ready ) daemon_mark_ready(daemon); daemon_change_state_list(daemon, DAEMON_STATE_FINISHED); + if ( daemon->want_restart ) + { + daemon_request_restart(daemon); + return; + } for ( size_t i = 0; i < daemon->dependents_used; i++ ) daemon_on_dependency_finished(daemon->dependents[i]); daemon_dereference_dependencies(daemon); @@ -2032,6 +2384,8 @@ static void daemon_on_startup_error(struct daemon* daemon) assert(daemon->state != DAEMON_STATE_FINISHING); assert(daemon->state != DAEMON_STATE_FINISHED); daemon_change_state_list(daemon, DAEMON_STATE_FINISHING); + if ( daemon->want_restart ) + daemon_request_restart(daemon); } static void daemon_wait(struct daemon* daemon) @@ -2065,7 +2419,7 @@ static void daemon_wait(struct daemon* daemon) switch ( dependency->target->state ) { case DAEMON_STATE_TERMINATED: - schedule_daemon(dependency->target); + daemon_schedule(dependency->target); if ( !(dependency->flags & DEPENDENCY_FLAG_AWAIT) ) daemon_on_dependency_ready(dependency); break; @@ -2345,6 +2699,7 @@ static void daemon_on_exit(struct daemon* daemon, int exit_code) { assert(daemon->state != DAEMON_STATE_FINISHING); assert(daemon->state != DAEMON_STATE_FINISHED); + daemon->pid = 0; daemon->exit_code = exit_code; if ( 0 <= daemon->readyfd ) { @@ -2379,6 +2734,497 @@ static void daemon_on_exit(struct daemon* daemon, int exit_code) daemon_on_finished(daemon); } +static void daemon_get_exit_string(struct daemon* daemon, char* buf, size_t len) +{ + if ( daemon->state != DAEMON_STATE_FINISHED ) + snprintf(buf, len, "n/a"); + else if ( WIFEXITED(daemon->exit_code) ) + snprintf(buf, len, "%i", WEXITSTATUS(daemon->exit_code)); + else if ( WIFSIGNALED(daemon->exit_code) ) + snprintf(buf, len, signal_describe(WTERMSIG(daemon->exit_code))); + else + snprintf(buf, len, "%#x", WEXITSTATUS(daemon->exit_code)); +} + +static const char* daemon_get_state_string(struct daemon* daemon) +{ + if ( daemon->state == DAEMON_STATE_TERMINATED && daemon_is_failed(daemon) ) + return "failed"; + return daemon_state_names[daemon->state]; +} + +static void connection_close(struct connection* conn) +{ + communication_unregister(conn->index); + close(conn->fd); + conn->fd = -1; +} + +static void connection_free(struct connection* conn) +{ + if ( 0 <= conn->fd ) + connection_close(conn); + free(conn->input); + free(conn->output); + free(conn); +} + +static struct connection* connection_new(int fd) +{ + size_t buffer_size = 4096; + struct connection* conn = calloc(1, sizeof(struct connection)); + char* input = malloc(buffer_size); + char* output = malloc(buffer_size); + if ( !conn || !input || !output ) + { + free(conn); + free(input); + free(output); + return NULL; + } + conn->input = input; + conn->input_used = 0; + conn->input_size = buffer_size; + conn->output = output; + conn->output_used = 0; + conn->output_size = buffer_size; + conn->fd = fd; + if ( !communication_reserve(1) ) + return connection_free(conn), NULL; + struct communication comm; + comm.type = COMMUNICATION_TYPE_CONNECTION; + comm.index_ptr = &conn->index; + comm.connection = conn; + communication_register(&comm, fd, POLLIN); + return conn; +} + +static void connection_reply(struct connection* conn, const char* string) +{ + if ( conn->fd < 0 ) + return; + size_t length = strlen(string); + size_t available = conn->output_size - conn->output_used; + if ( available < length ) + return connection_close(conn); + memcpy(conn->output + conn->output_used, string, length); + conn->output_used += length; +} + +static void connection_replyf(struct connection* conn, + const char* restrict format, + ...) +{ + if ( conn->fd < 0 ) + return; + char* buffer = conn->output + conn->output_used; + size_t available = conn->output_size - conn->output_used; + va_list list; + va_start(list, format); + size_t length = vsnprintf(buffer, available, format, list); + va_end(list); + if ( available <= length ) + return connection_close(conn); + conn->output_used += length; +} + +static void connection_ok(struct connection* conn) +{ + connection_reply(conn, "ok\n"); +} + +static void connection_error(struct connection* conn, const char* code) +{ + connection_reply(conn, "error "); + connection_reply(conn, code); + connection_reply(conn, "\n"); +} + +static void reply_status(struct connection* conn, struct daemon* daemon) +{ + char exit_str[64]; + daemon_get_exit_string(daemon, exit_str, sizeof(exit_str)); + connection_replyf(conn, " daemon=%s state=%s pid=%ji exit=%s", + daemon->name, daemon_get_state_string(daemon), + (intmax_t) daemon->pid, exit_str); +} + +static void reply_dependency(struct connection* conn, + struct dependency* dependency) +{ + char flags[sizeof(" optional no-await exit-code")]; + snprintf(flags, sizeof(flags), "%s%s%s", + !(dependency->flags & DEPENDENCY_FLAG_REQUIRE) ? " optional" : "", + !(dependency->flags & DEPENDENCY_FLAG_AWAIT) ? " no-await" : "", + dependency->flags & DEPENDENCY_FLAG_EXIT_CODE ? " exit-code" : ""); + connection_replyf(conn, "require %s %s %s", dependency->source->name, + dependency->target->name, flags); +} + +static void request_require(struct connection* conn, int argc, char** argv) +{ + if ( argc < 3 ) + return connection_error(conn, "missing_operand"); + const char* source_name = argv[1]; + const char* target_name = argv[2]; + bool start = false; + int flags = DEPENDENCY_FLAG_REQUIRE | DEPENDENCY_FLAG_AWAIT; + for ( int i = 3; i < argc; i++ ) + { + if ( !strcmp(argv[i], "optional") ) + flags &= ~DEPENDENCY_FLAG_REQUIRE; + else if ( !strcmp(argv[i], "no-await") ) + flags &= ~DEPENDENCY_FLAG_AWAIT; + else if ( !strcmp(argv[i], "exit-code") ) + flags |= DEPENDENCY_FLAG_EXIT_CODE; + else if ( !strcmp(argv[i], "start") ) + start = true; + else + return connection_error(conn, "unknown_flag"); + } + struct daemon* source = daemon_find_by_name(source_name); + if ( !source ) + return connection_error(conn, "no_such_source_daemon"); + // TODO: Relax this restriction. + if ( source->argv ) + return connection_error(conn, "source_daemon_was_not_virtual"); + struct daemon* target = daemon_find_or_create(target_name); + if ( !target ) + return connection_error(conn, "creating_daemon_failed"); + if ( !daemon_depend(source, target, flags, start) ) + return connection_error(conn, "adding_connection_failed"); + connection_ok(conn); +} + +static void request_unrequire(struct connection* conn, int argc, char** argv) +{ + if ( argc < 3 ) + return connection_error(conn, "missing_operand"); + const char* source_name = argv[1]; + const char* target_name = argv[2]; + bool stop = 4 <= argc && !strcmp(argv[3], "stop"); + struct daemon* source = daemon_find_by_name(source_name); + if ( !source ) + return connection_error(conn, "no_such_source_daemon"); + struct daemon* target = daemon_find_by_name(target_name); + if ( !target ) + return connection_error(conn, "no_such_daemon"); + daemon_undepend(source, target, stop); + connection_ok(conn); +} + +static void request_reload(struct connection* conn, int argc, char** argv) +{ + if ( argc != 2 ) + return connection_error(conn, "missing_operand"); + const char* name = argv[1]; + struct daemon* daemon = daemon_find_or_create(name); + if ( !daemon ) + return connection_error(conn, "creating_daemon_failed"); + daemon_reload(daemon); + connection_ok(conn); +} + +static void request_restart(struct connection* conn, int argc, char** argv) +{ + if ( argc != 2 ) + return connection_error(conn, "missing_operand"); + const char* name = argv[1]; + struct daemon* daemon = daemon_find_or_create(name); + if ( !daemon ) + return connection_error(conn, "creating_daemon_failed"); + daemon_request_restart(daemon); + connection_ok(conn); +} + +static void request_terminate(struct connection* conn, int argc, char** argv) +{ + if ( argc != 2 ) + return connection_error(conn, "missing_operand"); + const char* name = argv[1]; + struct daemon* daemon = daemon_find_by_name(name); + if ( !daemon ) + return connection_error(conn, "no_such_daemon"); + daemon_terminate(daemon); + connection_ok(conn); +} + +static void request_kill(struct connection* conn, int argc, char** argv) +{ + if ( argc != 2 ) + return connection_error(conn, "missing_operand"); + const char* name = argv[1]; + struct daemon* daemon = daemon_find_by_name(name); + if ( !daemon ) + return connection_error(conn, "no_such_daemon"); + daemon_kill(daemon); + connection_ok(conn); +} + +static void request_signal(struct connection* conn, int argc, char** argv) +{ + if ( argc != 3 ) + return connection_error(conn, "missing_operand"); + const char* name = argv[1]; + struct daemon* daemon = daemon_find_by_name(name); + if ( !daemon ) + return connection_error(conn, "no_such_daemon"); + const char* signame = argv[2]; + int sig = signal_lookup(signame); + if ( !sig ) + return connection_error(conn, "no_such_signal"); + daemon_signal(daemon, signame, sig); + connection_ok(conn); +} + +static void request_status(struct connection* conn, int argc, char** argv) +{ + if ( argc != 2 ) + return connection_error(conn, "missing_operand"); + const char* name = argv[1]; + struct daemon* daemon = daemon_find_by_name(name); + if ( !daemon ) + return connection_error(conn, "no_such_daemon"); + connection_reply(conn, "ok"); + reply_status(conn, daemon); + connection_reply(conn, "\n"); +} + +static void request_list(struct connection* conn, int argc, char** argv) +{ + (void) argc; + (void) argv; + + if ( !conn->state ) + { + connection_reply(conn, "ok"); + conn->state = 1; + return; + } + while ( conn->state - 1 < daemons_used ) + { + size_t i = conn->state - 1; + if ( i ) + connection_reply(conn, " , "); + reply_status(conn, daemons[i]); + conn->state++; + return; + } + connection_reply(conn, "\n"); + conn->state = 0; +} + +static void request_requirements(struct connection* conn, int argc, char** argv) +{ + if ( argc != 2 ) + return connection_error(conn, "missing_operand"); + const char* name = argv[1]; + struct daemon* daemon = daemon_find_by_name(name); + if ( !daemon ) + return connection_error(conn, "no_such_daemon"); + if ( !conn->state ) + { + connection_reply(conn, "ok"); + conn->state = 1; + return; + } + bool in = !strcmp(argv[0], "edges") || !strcmp(argv[0], "dependents"); + bool out = !strcmp(argv[0], "edges") || !strcmp(argv[0], "requirements"); + size_t dependents_used = in ? daemon->dependents_used : 0; + size_t dependencies_used = out ? daemon->dependencies_used : 0; + while ( conn->state - 1 < dependents_used ) + { + size_t i = conn->state - 1; + connection_reply(conn, conn->state == 1 ? " " : " , "); + reply_dependency(conn, daemon->dependents[i]); + conn->state++; + return; + } + while ( conn->state - 1 < dependents_used + dependencies_used ) + { + size_t i = conn->state - (1 + dependents_used); + connection_reply(conn, conn->state == 1 ? " " : " , "); + reply_dependency(conn, daemon->dependencies[i]); + conn->state++; + return; + } + connection_reply(conn, "\n"); + conn->state = 0; +} + +static void connection_on_message(struct connection* conn, const char* message) +{ + size_t argc = 0; + char** argv = tokenize(&argc, message); + if ( !argv ) + { + connection_error(conn, !errno ? "syntax_error" : "out_of_memory"); + return false; + } + + // TODO: poweroff, reboot, halt, reinit + if ( argc == 0 ) {} + else if ( !strcmp(argv[0], "require") ) + request_require(conn, argc, argv); + else if ( !strcmp(argv[0], "unrequire") ) + request_unrequire(conn, argc, argv); + else if ( !strcmp(argv[0], "reload") ) + request_reload(conn, argc, argv); + // TODO: request_reconfigure + else if ( !strcmp(argv[0], "restart") ) + request_restart(conn, argc, argv); + else if ( !strcmp(argv[0], "terminate") ) + request_terminate(conn, argc, argv); + else if ( !strcmp(argv[0], "kill") ) + request_kill(conn, argc, argv); + else if ( !strcmp(argv[0], "signal") ) + request_signal(conn, argc, argv); + else if ( !strcmp(argv[0], "status") ) + request_status(conn, argc, argv); + else if ( !strcmp(argv[0], "list") ) + request_list(conn, argc, argv); + else if ( !strcmp(argv[0], "edges") || + !strcmp(argv[0], "dependents") || + !strcmp(argv[0], "requirements") ) + request_requirements(conn, argc, argv); + else + connection_error(conn, "unknown_command"); + + for ( size_t i = 0; i < argc; i++ ) + free(argv[i]); + free(argv); +} + +static bool connection_on_event(struct connection* conn, int revents) +{ + if ( !(revents & (POLLIN | POLLOUT | POLLHUP | POLLERR)) ) + return true; + size_t from = 0; + if ( (revents & POLLIN) && conn->input_used < conn->input_size ) + { + + ssize_t amount = recv(conn->fd, conn->input + conn->input_used, + conn->input_size - conn->input_used, 0); + if ( 0 < amount ) + { + from = conn->input_used; + conn->input_used += amount; + } + else if ( amount == 0 || + (amount < 0 && errno != EWOULDBLOCK && errno != EAGAIN ) ) + return connection_free(conn), false; + } + if ( !conn->output_used ) + { + size_t i = from; + while ( i < conn->input_used ) + { + if ( conn->input[i] == '\n' ) + { + size_t n = i + 1; + conn->input[i] = '\0'; + connection_on_message(conn, conn->input); + conn->input[i] = '\n'; + if ( conn->fd < 0 ) + return connection_free(conn), false; + if ( conn->state ) + break; + memmove(conn->input, conn->input + n, conn->input_used - n); + conn->input_used -= n; + i = 0; + } + else + i++; + } + // Disconnect if the input line is too long. + if ( i == conn->input_size ) + return connection_free(conn), false; + } + if ( (revents & POLLOUT) && conn->output_used ) + { + ssize_t amount = send(conn->fd, conn->output, conn->output_used, + MSG_NOSIGNAL); + if ( 0 < amount ) + { + if ( (size_t) amount < conn->output_used ) + memmove(conn->output, conn->output + amount, + conn->output_used - amount); + conn->output_used -= amount; + } + else if ( errno != EWOULDBLOCK && errno != EAGAIN ) + return connection_free(conn), false; + } + if ( (revents & (POLLHUP | POLLERR)) ) + return connection_free(conn), false; + pfds[conn->index].events = + (conn->input_used < conn->input_size ? POLLIN : 0) | + (conn->output_used || conn->state ? POLLOUT : 0); + return true; +} + +static int open_local_server_socket(const char* path, int flags) +{ + size_t path_length = strlen(path); + size_t addr_size = offsetof(struct sockaddr_un, sun_path) + path_length + 1; + struct sockaddr_un* sockaddr = malloc(addr_size); + if ( !sockaddr ) + return -1; + sockaddr->sun_family = AF_LOCAL; + strcpy(sockaddr->sun_path, path); + int fd = socket(AF_LOCAL, SOCK_STREAM | flags, 0); + if ( fd < 0 ) + return free(sockaddr), -1; + if ( fchmod(fd, 0600) < 0 ) + return close(fd), free(sockaddr), -1; + if ( bind(fd, (const struct sockaddr*) sockaddr, addr_size) < 0 ) + return close(fd), free(sockaddr), -1; + if ( listen(fd, SOMAXCONN) < 0 ) + return close(fd), free(sockaddr), -1; + free(sockaddr); + return fd; +} + +static struct server* server_start(const char* path) +{ + if ( !communication_reserve(1) ) + return NULL; + struct server* server = malloc(sizeof(struct server)); + if ( !server ) + return NULL; + server->fd = open_local_server_socket(path, SOCK_NONBLOCK | SOCK_CLOEXEC); + if ( server->fd < 0 ) + { + free(server); + return NULL; + } + struct communication comm; + comm.type = COMMUNICATION_TYPE_SERVER; + comm.index_ptr = &server->index; + comm.server = server; + communication_register(&comm, server->fd, POLLIN); + return server; +} + +static bool server_on_event(struct server* server, int revents) +{ + if ( revents & POLLIN ) + { + int fd = accept4(server->fd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC); + if ( 0 <= fd ) + { + struct connection* conn = connection_new(fd); + if ( !conn ) + { + warning("Failed to allocate connection: %s: %m", server->path); + close(fd); + } + } + else if ( errno != EAGAIN && errno != EWOULDBLOCK ) + warning("accept: %s: %m", server->path); + } + return true; +} + static void init(void) { int default_daemon_exit_code = -1; @@ -2404,6 +3250,9 @@ static void init(void) DAEMON_STATE_FINISHING); default_daemon_exit_code = WCONSTRUCT(WNATURE_EXITED, caught_exit_signal, 0); + default_daemon->exit_code = default_daemon_exit_code; + default_daemon->exit_code_meaning = + EXIT_CODE_MEANING_POWEROFF_REBOOT; } caught_exit_signal = -1; @@ -2501,6 +3350,12 @@ static void init(void) case COMMUNICATION_TYPE_READY: closed = daemon_on_ready_event(comm->daemon, pfd->revents); break; + case COMMUNICATION_TYPE_SERVER: + closed = server_on_event(comm->server, pfd->revents); + break; + case COMMUNICATION_TYPE_CONNECTION: + closed = connection_on_event(comm->connection, pfd->revents); + break; } if ( closed ) i--; // Process this index again (something new there). @@ -3713,13 +4568,18 @@ int main(int argc, char* argv[]) reinit(); } + // TODO: Avoid conflicts with chroots. + const char* server_path = "/var/run/init"; + if ( !server_start(server_path) ) + fatal("Failed to start init server: %s: %m", server_path); + // TODO: Use the arguments to specify additional things the default daemon // should depend on, as well as a denylist of things not to start // even if in default's dependencies. The easiest thing is probably to // be able to inject require and unset require lines into default. // Request the default daemon be run. - schedule_daemon(default_daemon); + daemon_schedule(default_daemon); // Initialize the operating system. init(); diff --git a/init/service.8 b/init/service.8 new file mode 100644 index 00000000..dddd5b4c --- /dev/null +++ b/init/service.8 @@ -0,0 +1,391 @@ +.Dd May 24, 2024 +.Dt SERVICE 8 +.Os +.Sh NAME +.Nm service +.Nd daemon maintenance +.Sh SYNOPSIS +.Nm service +.Op Fl lr +.Op Fl \-s Ns "=" Ns source-daemon +.Op Fl \-exit-code +.Op Fl \-no-await +.Op Fl \-no-optional +.Op Fl \-raw +.Op Fl \-source Ns "=" Ns source-daemon +.Ar daemon +.Oo +.Sy dependents +| +.Sy disable +| +.Sy edges +| +.Sy enable +| +.Sy exit-code +| +.Sy kill +| +.Sy pid +| +.Sy reconfigure +| +.Sy reload +| +.Sy requirements +| +.Sy restart +| +.Sy signal +| +.Sy start +| +.Sy state +| +.Sy status +| +.Sy stop +| +.Sy terminate +.Oc +.Sh DESCRIPTION +.Nm +performs maintenance of daemons run by +.Xr init 8 +as configured in +.Xr init 5 . +The daemons are serviced by connecting to the +.Nm init +process and writing the requested command to the +.Pa /var/run/init +filesytem socket. +.Pp +The options are as follows: +.Bl -tag -width "12345678" +.It Fl s , Fl \-source-daemon Ns "=" Ns Ar source-daemon +When modifying a dependency using the +.Sy enable , disable , start +and +.Sy stop +commands, use the +.Ar source-daemon +as the source daemon in the dependency on the target +.Ar daemon . +The default the +.Sy local +daemon which is the parent of the locally configured daemons. +.It Fl \-exit-code +Set the +.Sy exit-code +flag on the dependency created in the +.Sy enable +and +.Sy start +commands. +.It Fl \-no-await +Set the +.Sy no-await +flag on the dependency created in the +.Sy enable +and +.Sy start +commands. +.It Fl \-no-optional +Unset the +.Sy optional +flag on the dependency created in the +.Sy enable +and +.Sy start +commands. +The default is to set the +.Sy optional +flag, which is the opposite of the +.Xr init 5 +.Sy require +declaration where dependencies are mandatory by default. +.It Fl l , \-list +Write a table containing the status of every loaded daemon in the format of the +.Sy status +command. +.It Fl r , \-raw +Write the command and additional operands as a raw message without verification +on the +.Nm init +socket +and output the raw reply sent from +.Nm init. +.El +.Pp +The commands are as follows: +.Bl -tag -width "requirements" +.It Sy dependents +Write the incoming dependencies on the +.Ar daemon +in the format of the +.Sy edges +command, which explains why a daemon is running. +.It Sy disable +Permanently disable the +.Ar daemon +by removing the dependency on it from the configuration of the +.Ar source-daemon +(the +.Sy local +daemon by default, see +.Xr init 5 ) +and then stopping it using the +.Sy stop +command. +The daemon will continue to run if any other daemon depends on it, which can +be diagnosed using the +.Sy dependents +command. +.It Sy edges +Write the incoming dependencies on the +.Ar daemon +and its outgoing dependencies in the format of the +.Sy require +declaration in +.Xr init 5 +with an extra +.Ar source +operand: +.Pp +.Sy require +.Ar source +.Ar target +[exit-code] +[no-await] +[optional] +.It Sy enable +Permanently enable the +.Ar daemon +by adding it to the configuration of the +.Ar source-daemon +(the +.Sy local +daemon by default, see +.Xr init 5 ) +with the dependency flags per +.Fl \-exit-code , +.Fl \-no-await , +and +.Fl \-no-optional +and starting it by sending the +.Sy start +command. +The daemon only starts if the +.Ar source-daemon +is running. +.It Sy exit-code +Write the exit code of the +.Ar daemon , +or the empty string if it has not exited. +.It Sy kill +Kill the +.Ar daemon +by sending the +.Sy SIGKILL +signal as a last resort that may cause data loss. +.It Sy pid +Write the process id of the +.Ar daemon +if it is running, or the empty output if it +does not have a process. +Process ids can be recycled and are subject to inherent race conditions. +Prefer to use the other commands in +.Nm +that addresses the daemon by its symbolic name as +.Xr init 8 +will ensure the command operates on the correct process. +.It Sy reconfigure +Reread the +.Xr init 5 +configuration for the +.Ar daemon +and apply it after restarting the daemon. +.It Sy reload +Request the +.Ar daemon +gracefully reload its own configuration sending +the reload signal (usually +.Sy SIGHUP ) +without restarting the daemon and without reloading the +.Xr init 5 +configuration. +.It Sy requirements +Write the outgoing dependencies from the +.Ar daemon +in the format of the +.Sy edges +command, which explains what daemons the daemon needs. +.It Sy restart +Restart the +.Ar daemon +by terminating it and starting it up again afterwards. +.It Sy signal Ar signal +Send the +.Ar signal +in the symbolic signal name format (e.g. +.Sy SIGUSR1 ) +to the +.Ar daemon . +.It Sy start +Start the +.Ar daemon +by asking +.Xr init 8 +to add a runtime dependency from the +.Ar source-daemon +(the +.Sy local +daemon by default, see +.Xr init 5 ) +to the daemon with the dependency flags per +.Fl \-exit-code , +.Fl \-no-await , +and +.Fl \-no-optional +and starting it by sending the +.Sy start +command. +The daemon only starts if the +.Ar source-daemon +is running. +.It Sy state +Write which the state the +.Ar daemon +is in. +.It Sy status +Write the status of the +.Ar daemon +as a single table row in the format: +.Pp +.Ar name +.Ar state +.Li pid Ns "=" Ns Ar pid +.Li exit Ns "=" Ns Ar exit-code +.Pp +The +.Ar state +is one of +.Sy terminated , +.Sy scheduled , +.Sy waiting , +.Sy satisfied , +.Sy starting , +.Sy running , +.Sy terminating , +.Sy finishing , +.Sy finished , +or +.Sy failed . +The +.Ar pid +is the process id if any, or +.Li 0 +otherwise. +The +.Ar exit-code +is the exit code of the daemon if it has exited, or the name of a signal that +killed it, or +.Li n/a +if the daemon has not exited. +.It Sy stop +Stop the +.Ar daemon +by asking +.Xr init 8 +to remove the dependency from the +.Ar source-daemon +(the +.Sy local +daemon by default, see +.Xr init 5 ) +on the daemon. +The daemon will continue to run as long if other daemon depends on it, which can +be diagnosed using the +.Sy dependents +command. +.It Sy terminate +Terminate the +.Ar daemon +gracefully by sending the +.Sy SIGTERM +signal and +.Sy SIGKILL +after a timeout. +Prefer the +.Sy stop +command if possible as the +.Sy terminate +command bypasses the reference count and may cause data loss if other daemons +malfunction when the daemon is unexpectedly terminated. +.El +.Sh ENVIRONMENT +.Bl -tag -width "INIT_SOCKET" +.It Ev INIT_SOCKET +.Xr init 8 Ns 's +filesystem socket for communication, +.Pa /var/run/init +by default. +.El +.Sh FILES +.Bl -tag -width "/etc/init/local" -compact +.It Pa /etc/init/ +Daemon configuration for the local system (first in search path) (see +.Xr init 5 ) +.It Pa /etc/init/local +Configuration for the +.Sy local +daemon (see +.Xr init 5 ) +.El +.Sh EXIT STATUS +.Nm +will exit 0 on success and non-zero otherwise. +.Sh EXAMPLES +Permanently enable the sshd daemon: +.Bd -literal +$ service sshd enable +.Ed +.Pp +Permanently disable the ntpd daemon: +.Bd -literal +$ service ntpd disable +.Ed +.Pp +Temporarily start the nginx daemon without changing +.Pa /etc/init/local : +.Bd -literal +$ service nginx start +.Ed +.Pp +Temporarily disable the sshd daemon without changing +.Pa /etc/init/local : +.Bd -literal +$ service sshd stop +.Ed +.Pp +Temporarily stop the ntpd daemon and diagnose why it kept running due to the +.Sy time +daemon depending on it: +.Bd -literal +$ service ntpd stop +$ service ntpd state +running +$ service ntpd dependents +require time ntpd exit-code +$ service --source-daemon=time ntpd stop +$ service ntpd state +finished +.Ed +.Sh SEE ALSO +.Xr kill 1 , +.Xr init 5 , +.Xr halt 8 , +.Xr init 8 , +.Xr poweroff 8 , +.Xr reboot 8 diff --git a/init/service.c b/init/service.c new file mode 100644 index 00000000..7651b816 --- /dev/null +++ b/init/service.c @@ -0,0 +1,594 @@ +/* + * Copyright (c) 2024 Jonas 'Sortie' Termansen. + * + * Permission to use, copy, modify, and distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + * + * service.c + * Start and stop services. + */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +static bool array_add(void*** array_ptr, + size_t* used_ptr, + size_t* length_ptr, + void* value) +{ + void** array; + memcpy(&array, array_ptr, sizeof(array)); // Strict aliasing. + + if ( *used_ptr == *length_ptr ) + { + size_t length = *length_ptr; + if ( !length ) + length = 4; + void** new_array = reallocarray(array, length, 2 * sizeof(void*)); + if ( !new_array ) + return false; + array = new_array; + memcpy(array_ptr, &array, sizeof(array)); // Strict aliasing. + *length_ptr = length * 2; + } + + memcpy(array + (*used_ptr)++, &value, sizeof(value)); // Strict aliasing. + + return true; +} + +static char** tokenize(size_t* out_tokens_used, const char* string) +{ + size_t tokens_used = 0; + size_t tokens_length = 0; + char** tokens = malloc(sizeof(char*)); + if ( !tokens ) + return NULL; + bool failed = false; + bool invalid = false; + while ( *string ) + { + if ( isspace((unsigned char) *string) ) + { + string++; + continue; + } + if ( *string == '#' ) + break; + char* token; + size_t token_size; + FILE* fp = open_memstream(&token, &token_size); + if ( !fp ) + { + failed = true; + break; + } + bool singly = false; + bool doubly = false; + bool escaped = false; + while ( *string ) + { + char c = *string++; + if ( !escaped && !singly && !doubly && isspace((unsigned char) c) ) + break; + if ( !escaped && !doubly && c == '\'' ) + { + singly = !singly; + continue; + } + if ( !escaped && !singly && c == '"' ) + { + doubly = !doubly; + continue; + } + if ( !singly && !escaped && c == '\\' ) + { + escaped = true; + continue; + } + if ( escaped ) + { + switch ( c ) + { + case 'a': c = '\a'; break; + case 'b': c = '\b'; break; + case 'e': c = '\e'; break; + case 'f': c = '\f'; break; + case 'n': c = '\n'; break; + case 'r': c = '\r'; break; + case 't': c = '\t'; break; + case 'v': c = '\v'; break; + default: break; + }; + } + escaped = false; + if ( fputc((unsigned char) c, fp) == EOF ) + { + failed = true; + break; + } + } + if ( singly || doubly || escaped ) + { + fclose(fp); + free(token); + invalid = true; + break; + } + if ( fflush(fp) == EOF ) + { + fclose(fp); + free(token); + failed = true; + break; + } + fclose(fp); + if ( !array_add((void***) &tokens, &tokens_used, &tokens_length, + token) ) + { + free(token); + failed = true; + break; + } + } + if ( failed || invalid ) + { + for ( size_t i = 0; i < tokens_used; i++ ) + free(tokens[i]); + free(tokens); + if ( invalid ) + errno = 0; + return NULL; + } + char** new_tokens = reallocarray(tokens, tokens_used, sizeof(char*)); + if ( new_tokens ) + tokens = new_tokens; + *out_tokens_used = tokens_used; + return tokens; +} + +static char** receive(FILE* fp, size_t* out_tokens_used) +{ + char* line = NULL; + size_t line_size; + if ( getline(&line, &line_size, fp) < 0 ) + { + if ( ferror(fp) ) + errx(1, "receiving reply: Unexpected end of connection"); + else + err(1, "receiving reply"); + } + char** result = tokenize(out_tokens_used, line); + free(line); + if ( !result ) + { + if ( errno ) + errx(1, "invalid reply: %s", line); + else + errx(1, "failed to parse replyt"); + } + if ( !*out_tokens_used ) + errx(1, "invalid empty reply"); + if ( !strcmp(result[0], "ok") ) + return result; + else if ( !strcmp(result[0], "error") ) + errx(1, "error: %s", 2 <= *out_tokens_used ? result[1] : "Unknown"); + else + errx(1, "unknown reply: %s", result[0]); +} + +static int open_local_client_socket(const char* path, int flags) +{ + size_t path_length = strlen(path); + size_t addr_size = offsetof(struct sockaddr_un, sun_path) + path_length + 1; + struct sockaddr_un* sockaddr = malloc(addr_size); + if ( !sockaddr ) + return -1; + sockaddr->sun_family = AF_LOCAL; + strcpy(sockaddr->sun_path, path); + int fd = socket(AF_LOCAL, SOCK_STREAM | flags, 0); + if ( fd < 0 ) + return free(sockaddr), -1; + if ( connect(fd, (const struct sockaddr*) sockaddr, addr_size) < 0 ) + return close(fd), free(sockaddr), -1; + free(sockaddr); + return fd; +} + +static void rewrite(const char* path, const char* daemon, const char* flags) +{ + FILE* fp = fopen(path, "r"); + if ( !fp && errno != ENOENT ) + err(1, "%s", path); + char* out_path; + if ( asprintf(&out_path, "%s.XXXXXX", path) < 0 ) + err(1, "malloc"); + int out_fd = mkstemp(out_path); + if ( out_fd < 0 ) + err(1, "mkstemp: %s.XXXXXX", path); + FILE* out = fdopen(out_fd, "w"); + if ( !out ) + { + unlink(out_path); + err(1, "fdopen"); + } + bool found = false; + char* line = NULL; + size_t line_size = 0; + ssize_t line_length; + off_t line_number = 0; + while ( fp && 0 < (line_length = getline(&line, &line_size, fp)) ) + { + line_number++; + size_t tokenc; + char** tokens = tokenize(&tokenc, line); + if ( !tokens ) + { + unlink(out_path); + if ( errno ) + err(1, "%s", path); + else + errx(1, "%s:%ji: Syntax error", path, (intmax_t) line_number); + } + if ( 2 <= tokenc && + !strcmp(tokens[0], "require") && !strcmp(tokens[1], daemon) ) + { + found = true; + if ( flags ) + fprintf(out, "require %s%s\n", daemon, flags); + } + else + fputs(line, out); + } + free(line); + if ( !found && flags ) + fprintf(out, "require %s%s\n", daemon, flags); + if ( (fp && ferror(fp)) || ferror(out) || fflush(out) == EOF ) + { + unlink(out_path); + err(1, "%s", path); + } + if ( fp ) + { + struct stat st; + fstat(fileno(fp), &st); + fchmod(out_fd, st.st_mode & 07777); + fchown(out_fd, st.st_uid, st.st_gid); + fclose(fp); + } + else + fchmod(out_fd, 0666 & ~getumask()); + if ( rename(out_path, path) < 0 ) + { + unlink(out_path); + err(1, "rename: %s -> %s", out_path, path); + } + fclose(out); +} + +static bool check_daemon_exists_in_dir(const char* dir, const char* daemon) +{ + char* path; + if ( asprintf(&path, "%s/%s", dir, daemon) < 0 ) + err(1, "malloc"); + bool result = !access(path, F_OK); + free(path); + return result; +} + + +static void check_daemon_exists(const char* daemon) +{ + if ( !check_daemon_exists_in_dir("/etc/init", daemon) && + !check_daemon_exists_in_dir("/share/init", daemon) ) + errx(1, "%s: Daemon does not exist", daemon); +} + +static size_t string_display_length(const char* str) +{ + size_t display_length = 0; + mbstate_t ps; + memset(&ps, 0, sizeof(ps)); + while ( true ) + { + wchar_t wc; + size_t amount = mbrtowc(&wc, str, SIZE_MAX, &ps); + if ( amount == 0 ) + break; + if ( amount == (size_t) -1 || amount == (size_t) -2 ) + { + display_length++; + str++; + memset(&ps, 0, sizeof(ps)); + continue; + } + int width = wcwidth(wc); + if ( width < 0 ) + width = 0; + if ( SIZE_MAX - display_length < (size_t) width ) + display_length = SIZE_MAX; + else + display_length += (size_t) width; + str += amount; + } + return display_length; +} + +static void pad(const char* string, size_t padding) +{ + fputs(string, stdout); + for ( size_t length = string_display_length(string); + length < padding; padding-- ) + putchar(' '); +} + +static void format_statuses(char** tokens, size_t count) +{ + size_t daemon_length = 0; + size_t state_length = 10; + for ( size_t i = 0; i < count; i++ ) + { + if ( !strncmp(tokens[i], "daemon=", strlen("daemon=")) ) + { + const char* arg = tokens[i] + strlen("daemon="); + size_t length = string_display_length (arg); + if ( daemon_length < length ) + daemon_length = length; + } + else if ( !strncmp(tokens[i], "state=", strlen("state=")) ) + { + const char* arg = tokens[i] + strlen("state="); + size_t length = string_display_length(arg); + if ( state_length < length ) + state_length = length; + } + } + size_t i = 0; + while ( i < count ) + { + size_t next = i; + while ( next < count && strcmp(tokens[next], ",") ) + next++; + const char* daemon = NULL; + const char* state = NULL; + for ( size_t n = i; n < next; n++ ) + { + if ( !strncmp(tokens[n], "state=", strlen("state=")) ) + state = tokens[n] + strlen("state="); + else if ( !strncmp(tokens[n], "daemon=", strlen("daemon=")) ) + daemon = tokens[n] + strlen("daemon="); + } + if ( !state || !daemon ) + errx(1, "missing information in reply"); + pad(daemon, daemon_length + 2); + pad(state, state_length); + for ( size_t n = i; n < next; n++ ) + { + if ( strncmp(tokens[n], "state=", strlen("state=")) && + strncmp(tokens[n], "daemon=", strlen("daemon=")) ) + printf(" %s", tokens[n]); + } + putchar('\n'); + i = next; + if ( i < count && !strcmp(tokens[i], ",") ) + i++; + } +} + +int main(int argc, char* argv[]) +{ + const char* init_socket = getenv("INIT_SOCKET"); + if ( !init_socket ) + init_socket = "/var/run/init"; + + bool exit_code = false; + bool list = false; + bool no_await = false; + bool optional = true; + bool raw = false; + const char* source = "local"; + + const struct option longopts[] = + { + {"exit-code", no_argument, NULL, 256}, + {"list", no_argument, NULL, 'l'}, + {"no-await", no_argument, NULL, 257}, + {"no-optional", no_argument, NULL, 258}, + {"source", required_argument, NULL, 's'}, + {"raw", no_argument, NULL, 'r'}, + {0, 0, 0, 0} + }; + const char* opts = "lrs:"; + int opt; + while ( (opt = getopt_long(argc, argv, opts, longopts, NULL)) != -1 ) + { + switch ( opt ) + { + case 'l': list = true; break; + case 'r': raw = true; break; + case 's': source = optarg; break; + case 256: exit_code = true; break; + case 257: no_await = true; break; + case 258: optional = false; break; + default: return 2; + } + } + + int fd = open_local_client_socket(init_socket, 0); + if ( fd < 0 ) + err(1, "%s", init_socket); + FILE* fp = fdopen(fd, "r+"); + if ( !fp ) + err(1, "fdopen"); + + if ( raw ) + { + for ( int i = optind; i < argc; i++ ) + { + if ( fprintf(fp, "%s%c", argv[i], i + 1 == argc ? '\n' : ' ') < 0 ) + err(1, "%s", init_socket); + } + size_t tokens_count; + char** tokens = receive(fp, &tokens_count); + for ( size_t i = 0; i < tokens_count; i++ ) + printf("%s%c", tokens[i], i + 1 == tokens_count ? '\n' : ' '); + return 0; + } + + char flags[sizeof(" optional no-await exit-code")]; + snprintf(flags, sizeof(flags), "%s%s%s", + optional ? " optional" : "", + no_await ? " no-await" : "", + exit_code ? " exit-code" : ""); + char* source_path; + if ( asprintf(&source_path, "/etc/init/%s", source) < 0 ) + err(1, "malloc"); + + bool no_command = list; + + if ( no_command && 0 < argc - optind ) + errx(1, "unexpected extra operand: %s", argv[optind]); + else if ( !no_command && argc - optind < 2 ) + errx(1, "usage: "); + + const char* daemon = !no_command ? argv[optind++] : NULL; + const char* command = !no_command ? argv[optind++] : NULL; + + if ( command && strcmp(command, "signal") && 0 < argc - optind ) + errx(1, "unexpected extra operand: %s", argv[optind]); + + if ( list ) + fprintf(fp, "list\n"); + else if ( !strcmp(command, "enable") ) + { + check_daemon_exists(daemon); + rewrite(source_path, daemon, flags); + fprintf(fp, "require %s %s %s start\n", source, daemon, flags); + } + else if ( !strcmp(command, "disable") ) + { + rewrite(source_path, daemon, NULL); + fprintf(fp, "unrequire %s %s\n", source, daemon); + } + else if ( !strcmp(command, "start") ) + fprintf(fp, "require %s %s %s start\n", source, daemon, flags); + else if ( !strcmp(command, "stop") ) + fprintf(fp, "unrequire %s %s\n", source, daemon); + else if ( !strcmp(command, "restart") ) + fprintf(fp, "restart %s\n", daemon); + else if ( !strcmp(command, "reload") ) + fprintf(fp, "reload %s\n", daemon); + else if ( !strcmp(command, "reconfigure") ) + fprintf(fp, "reconfigure %s\n", daemon); + else if ( !strcmp(command, "terminate") ) + fprintf(fp, "terminate %s\n", daemon); + else if ( !strcmp(command, "kill") ) + fprintf(fp, "kill %s\n", daemon); + else if ( !strcmp(command, "signal") ) + { + if ( argc - optind < 1 ) + errx(1, "expected signal name"); + const char* signal = argv[optind++]; + if ( 0 < argc - optind ) + errx(1, "unexpected extra operand: %s", argv[optind]); + fprintf(fp, "signal %s %s\n", daemon, signal); + } + else if ( !strcmp(command, "status") ) + fprintf(fp, "status %s\n", daemon); + else if ( !strcmp(command, "state") ) + fprintf(fp, "status %s\n", daemon); + else if ( !strcmp(command, "pid") ) + fprintf(fp, "status %s\n", daemon); + else if ( !strcmp(command, "exit-code") ) + fprintf(fp, "status %s\n", daemon); + else if ( !strcmp(command, "requirements") || + !strcmp(command, "dependents") || + !strcmp(command, "edges") ) + fprintf(fp, "%s %s\n", command, daemon); + else + errx(1, "unknown command: %s", command); + + if ( ferror(fp) || fflush(fp) == EOF ) + err(1, "%s", init_socket); + + size_t tokens_count; + char** tokens = receive(fp, &tokens_count); + + if ( list ) + format_statuses(tokens + 1, tokens_count - 1); + else if ( !strcmp(command, "status") ) + format_statuses(tokens + 1, tokens_count - 1); + else if ( !strcmp(command, "state") ) + { + for ( size_t i = 1; i < tokens_count; i++ ) + { + if ( !strncmp(tokens[i], "state=", strlen("state=")) ) + { + puts(tokens[i] + strlen("state=")); + break; + } + } + } + else if ( !strcmp(command, "pid") ) + { + for ( size_t i = 1; i < tokens_count; i++ ) + { + if ( !strncmp(tokens[i], "pid=", strlen("pid=")) ) + { + const char* arg = tokens[i] + strlen("pid="); + if ( strcmp(arg, "0") ) + puts(arg); + break; + } + } + } + else if ( !strcmp(command, "exit-code") ) + { + for ( size_t i = 1; i < tokens_count; i++ ) + { + if ( !strncmp(tokens[i], "exit=", strlen("exit=")) ) + { + const char* arg = tokens[i] + strlen("exit="); + if ( strcmp(arg, "n/a") ) + puts(arg); + break; + } + } + } + else if ( !strcmp(command, "requirements") || + !strcmp(command, "dependents") || + !strcmp(command, "edges") ) + { + for ( size_t i = 1; i < tokens_count; i++ ) + { + if ( !strcmp(tokens[i], ",") ) + continue; + size_t eol = i + 1 == tokens_count || !strcmp(tokens[i + 1], ","); + printf("%s%c", tokens[i], eol ? '\n' : ' '); + } + } + + return 0; +} diff --git a/share/man/man5/init.5 b/share/man/man5/init.5 index 93d59676..ffa0b13b 100644 --- a/share/man/man5/init.5 +++ b/share/man/man5/init.5 @@ -653,7 +653,8 @@ require exampled exit-code .Ed .Sh SEE ALSO .Xr daemon 7 , -.Xr init 8 +.Xr init 8 , +.Xr service 8 .Sh BUGS The control messages mentioned in .Sy log-control-messages