From f2d50bbf9c92a4373cf2ab7c9447ec7b6ba2c59b Mon Sep 17 00:00:00 2001 From: Jonas 'Sortie' Termansen Date: Sun, 25 Sep 2022 01:35:35 +0200 Subject: [PATCH] Add daemon support to init(8). This change implements a dependency tracking daemon(7) system in init with overridable init(5) configuration, parallel startup, readiness signaling, rotating logs, reliable stopping, and handling of leaked processes. The /etc/init/target file is replaced by the new /etc/init/default per the new init(5) format. The old configuration is migrated upon upgrade using an upgrade hook. extfs(8) now signals readiness using READYFD for fast mounting. Filesystems that fail to be repaired are now mounted read-only. The mounting and filesystem checking code is synchronized with sysinstall. The duplicated array_add utility function now protects against overflows. tix-iso-bootconfig(8) gains the --init-target option. tix-iso-liveconfig(8) gains the --daemons option. --- Makefile | 2 +- ext/fsmarshall.cpp | 14 + init/init.8 | 256 +- init/init.c | 3266 ++++++++++++++++++++---- libmount/util.h | 15 +- sh/util.c | 15 +- share/init/base | 1 + share/init/local | 0 share/init/multi-user | 8 + share/init/no-user | 2 + share/init/single-user | 9 + share/init/sysinstall | 8 + share/init/sysupgrade | 8 + share/init/time | 0 share/man/man5/init.5 | 587 +++++ share/man/man7/daemon.7 | 113 + share/man/man7/following-development.7 | 15 + share/man/man7/portability.7 | 4 +- share/sysinstall/hooks/sortix-1.1-init | 0 sysinstall/Makefile | 1 + sysinstall/devices.c | 159 +- sysinstall/hooks.c | 39 + sysinstall/sysinstall.c | 3 +- tix/tix-iso-bootconfig | 10 + tix/tix-iso-bootconfig.8 | 18 + tix/tix-iso-liveconfig | 14 +- tix/tix-iso-liveconfig.8 | 10 + update-initrd/update-initrd | 8 +- 28 files changed, 3942 insertions(+), 643 deletions(-) create mode 100644 share/init/base create mode 100644 share/init/local create mode 100644 share/init/multi-user create mode 100644 share/init/no-user create mode 100644 share/init/single-user create mode 100644 share/init/sysinstall create mode 100644 share/init/sysupgrade create mode 100644 share/init/time create mode 100644 share/man/man5/init.5 create mode 100644 share/man/man7/daemon.7 create mode 100644 share/sysinstall/hooks/sortix-1.1-init diff --git a/Makefile b/Makefile index 60520303..d91ee97c 100644 --- a/Makefile +++ b/Makefile @@ -441,7 +441,7 @@ $(LIVE_INITRD): sysroot mkdir -p $(LIVE_INITRD).d mkdir -p $(LIVE_INITRD).d/etc mkdir -p $(LIVE_INITRD).d/etc/init - echo single-user > $(LIVE_INITRD).d/etc/init/target + echo require single-user exit-code > $(LIVE_INITRD).d/etc/init/default echo "root::0:0:root:/root:sh" > $(LIVE_INITRD).d/etc/passwd echo "root::0:root" > $(LIVE_INITRD).d/etc/group mkdir -p $(LIVE_INITRD).d/home diff --git a/ext/fsmarshall.cpp b/ext/fsmarshall.cpp index 688c32de..2c567a4f 100644 --- a/ext/fsmarshall.cpp +++ b/ext/fsmarshall.cpp @@ -720,6 +720,18 @@ void TerminationHandler(int) should_terminate = true; } +static void ready(void) +{ + const char* readyfd_env = getenv("READYFD"); + if ( !readyfd_env ) + return; + int readyfd = atoi(readyfd_env); + char c = '\n'; + write(readyfd, &c, 1); + close(readyfd); + unsetenv("READYFD"); +} + int fsmarshall_main(const char* argv0, const char* mount_path, bool foreground, @@ -756,6 +768,8 @@ int fsmarshall_main(const char* argv0, exit(0); setpgid(0, 0); } + else + ready(); dev->SpawnSyncThread(); diff --git a/init/init.8 b/init/init.8 index bbf7ce5e..04e9701f 100644 --- a/init/init.8 +++ b/init/init.8 @@ -6,16 +6,27 @@ .Nd system initialization .Sh SYNOPSIS .Nm init -.Op Fl \-target Ns "=" Ns Ar init-target +.Op Fl qsv +.Op Fl \-target Ns "=" Ns Ar default-daemon .Op Fl \- .Op Ar chain-init ... .Sh DESCRIPTION .Nm is the first program run after system startup and is responsible for -initializing the operating system and starting the specified -.Ar init-target . -This is normally a login screen, a root shell, or a dedicated special purpose -program. +initializing the operating system. +.Pp +Each +.Xr daemon 7 +is started in order as its dependencies become ready per its +.Xr init 5 +configuration. +The +.Sy default +daemon is automatically started and its recursive dependencies constitute the +operating system. +The +.Sy default +daemon's single dependency is referred to as the target. .Pp The .Xr kernel 7 @@ -34,56 +45,30 @@ If the system is installed on a harddisk, then the initrd is a minimal system made with .Xr update-initrd 8 that will search for the actual root filesystem and chain init it. -The next stage init will recognize it as the intended system and complete the -system startup. -.Ss Initialization Target -.Nm -first determines its target from the -.Fl \-target -option if specified or -.Pa /etc/init/target -otherwise. -Supported targets are: +The next stage init will recognize itself as the intended system and complete +the system startup. .Pp -.Bl -tag -width "single-user" -compact -offset indent -.It Sy chain -mount real root filesystem and run its -.Nm . -.It Sy chain-merge -like -.Sy chain -but run -.Pa /sysmerge/sbin/init -with the -.Sy merge -target. -.It Sy merge -finish a -.Xr sysmerge 8 -upgrade and then execute the real -.Nm -with its default target. -.It Sy multi-user -boot to -.Xr login 8 . -.It Sy single-user -boot to root shell without password (not secure). -.It Sy sysinstall -boot to operating system installer (not secure). -.It Sy sysupgrade -boot to operating system upgrader (not secure). +The options are as follows: +.Bl -tag -width "12345678" +.It Fl q , \-quiet +Write status updates to the terminal only about failed daemons. +This behavior is the default. +.It Fl s , \-silent +Never write status updates about daemons to the terminal. +.It Fl t , \-target Ns "=" Ns Ar default-daemon +Boot +.Ar default-daemon +as the target. +The +.Sy default +daemon configuration is changed to only require the +.Ar default-daemon +dependency with the +.Sy exit-only +flag. +.It Fl v , \-verbose +Write all status updates about daemons starting and stopping to the terminal .El -.Pp -It is a full system compromise if unauthenticated users are able to boot the -wrong target. -The kernel command line can specify the path to -.Nm -and its arguments. -Unprivileged users can change the kernel command line from the bootloader -command line if it hasn't been password protected. -Likewise unprivileged users can use their own replacement bootloader by booting -a portable device under their control if the firmware configuration has not been -password protected. .Ss Cleanup of /tmp and /var/run .Nm deletes everything inside of @@ -104,23 +89,24 @@ will scan every block device for valid partition tables and create the corresponding partition devices in .Pa /dev . .Ss Chain Initialization -The +If the target is .Sy chain -target mounts the root filesystem as in +or +.Sy chain-merge , +then the real operating system is chain initialized. +.Pp +The root filesystem is mounted per .Pa /etc/fstab (see -.Xr fstab 5 ) -and runs the next -.Nm -program. -This is used by +.Xr fstab 5 ) . +This configuration file is a copy of the real file made by .Xr update-initrd 8 -to make a bootstrap +when it makes the bootstrap .Xr initrd 7 . .Pp -Every block device and partition is scanned to determine if it is the root -filesystem. -It is checked for consistency if necessary. +The root filesystem is found by searching each block device and partition. +It is checked for consistency if necessary and mounted read-only if the check +fails. It is mounted at .Pa /tmp/fs.XXXXXX and the @@ -133,6 +119,34 @@ Finally the program (or .Ar chain-init if specified) of the target root filesystem is run inside a chroot. +If the target is +.Sy chain-merge , +then the +.Fl \-target=merge +option is passed to the next +.Nm . +.Ss Mountpoints +.Nm +mounts all the filesystems according to +.Xr fstab 5 . +The filesystems are checked for consistency if necessary and mounted read-only +if the check fails. +.Ss Logging +Logging to +.Pa /var/log +begins once the filesystems are mounted and +.Nm +writes the log entries from early boot to its +.Pa /var/log/init.log . +.Ss Random Seed +.Nm +will write 256 bytes of randomness to +.Pa /boot/random.seed , +which serves as the initial entropy for the +.Xr kernel 7 +on the next boot. +The file is also written on system shutdown where the system has the most +entropy. .Ss Configuration Once the .Nm @@ -150,46 +164,52 @@ set keyboard layout (see set graphics resolution (see .Xr videomode 5 ) .El -.Ss Mountpoints -.Nm -mounts all the filesystems according to -.Xr fstab 5 . -.Ss Random Seed -.Nm -will write 256 bytes of randomness to -.Pa /boot/random.seed , -which serves as the initial entropy for the -.Xr kernel 7 -on the next boot. -The file is also written on system shutdown where the system has the most -entropy. .Ss Merge -The -.Sy merge -target completes a delayed system upgrade by invoking the +If the target is +.Sy merge , +then a delayed system upgrade is completed by invoking .Xr sysmerge 8 at .Pa /sysmerge/sbin/sysmerge with the .Ar --booting option. +.Pp If the upgrade succeeds, the temporary -.Nm +.Pa /sysmerge/sbin/init deinitializes the system and invokes the real (now upgraded) -.Nm +.Pa /sbin/init , which will restart system initialization in the normal fashion. -.Ss Session -Finally +.Ss Daemons +The +.Sy default +.Xr daemon 7 +is started per its +.Pa /etc/init/default +.Xr init 5 +configuration file, which constitutes the operating system, and once it exits +then .Nm -will start the target program according to its initialization target. -This will be a login screen, a root shell, or something else. -If the process exits abnormally -.Nm -will automatically restart it. -.Nm -will exit with the same exit status as the process if it exits normally. -The kernel decides whether to power off, reboot or halt based on this exit -status. +exits with the same error code and the kernel shuts down the machine. +The +.Sy default +daemon is meant to be a virtual daemon depending on a single top level daemon +(the target), which provide the desired operating system functionality +(e.g. booting to a single user shell or a multi user login screen). +.Pp +The daemons are configured per +.Xr init 5 +where +.Pa /etc/init +contains the installation's local configuration, which overrides the operating +system's default configuration in +.Pa /share/init . +The daemons are started in order as their dependencies become ready and are +stopped in order when they are no longer required. +.Pp +The +.Sy local +daemon is meant to start the installation's local daemon requirements. .Sh ENVIRONMENT .Nm sets the following environment variables. @@ -213,21 +233,44 @@ root .Sh FILES .Bl -tag -width "/boot/random.seed" -compact .It Pa /boot/random.seed -initial kernel entropy -.It Pa /etc/init/target -default initialization target +Initial kernel entropy +.It Pa /etc/init/ +Daemon configuration for the local system (first in search path) (see +.Xr init 5 ) +.It Pa /etc/init/default +Configuration for the default daemon (see +.Xr init 5 ) .It Pa /etc/fstab -filesystem table (see +Filesystem table (see .Xr fstab 5 ) .It Pa /etc/hostname -hostname (see +Hostname (see .Xr hostname 5 ) .It Pa /etc/kblayout -keyboard layout (see +Keyboard layout (see .Xr kblayout 5 ) .It Pa /etc/videomode -graphics resolution (see +Graphics resolution (see .Xr videomode 5 ) +.It Pa /share/init/ +Default daemon configuration provided by the operating system (second in +search path) (see +.Xr init 5 ) +.It Pa /var/log/ +Daemon log files (see +.Xr init 5 ) +.It Pa /var/log/init.log +.Nm Ns 's +own log. +.El +.Sh ASYNCHRONOUS EVENTS +.Bl -tag -width "SIGUSR1" +.It Dv SIGTERM +Request system poweroff. +.It Dv SIGINT +Request system reboot. +.It Dv SIGQUIT +Request system halt. .El .Sh EXIT STATUS .Nm @@ -243,10 +286,23 @@ exits with the same exit status as its target session if it terminates normally. .Sh SEE ALSO .Xr fstab 5 , .Xr hostname 5 , +.Xr init 5 , .Xr kblayout 5 , .Xr videomode 5 , +.Xr daemon 7 , .Xr initrd 7 , .Xr kernel 7 , .Xr login 8 , .Xr sysmerge 8 , .Xr update-initrd 8 +.Sh SECURITY CONSIDERATIONS +It is a full system compromise if unauthenticated users are able to boot the +wrong target. +The kernel command line can specify the path to +.Nm +and its arguments. +Unprivileged users can change the kernel command line from the bootloader +command line if it hasn't been password protected. +Likewise unprivileged users can use their own replacement bootloader by booting +a portable device under their control if the firmware configuration has not been +password protected. diff --git a/init/init.c b/init/init.c index ae6ef693..3a75d0e7 100644 --- a/init/init.c +++ b/init/init.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2017 Jonas 'Sortie' Termansen. + * Copyright (c) 2011-2022 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 @@ -26,27 +26,37 @@ #include #include -#include #include +#include #include #include +#include #include +#include #include +#include #include +#include #include +#include +#include #include #include #include #include #include #include +#include #include #include #include #include #include -#include +// TODO: The Sortix doesn't expose this at the moment. +#if !defined(HOST_NAME_MAX) && defined(__sortix__) +#include +#endif #include #include @@ -69,7 +79,201 @@ struct mountpoint char* absolute; }; +enum verbosity +{ + VERBOSITY_SILENT, + VERBOSITY_QUIET, + VERBOSITY_VERBOSE, +}; + +enum exit_code_meaning +{ + EXIT_CODE_MEANING_DEFAULT, + EXIT_CODE_MEANING_POWEROFF_REBOOT, +}; + +enum daemon_state +{ + // Daemon is not running and should not be running. + DAEMON_STATE_TERMINATED, + + // Daemon is not running but should be scheduled to run. + DAEMON_STATE_SCHEDULED, + + // Daemon is not running but will start after its dependencies are ready. + DAEMON_STATE_WAITING, + + // Daemon is not running and can start now that the dependencies are ready. + DAEMON_STATE_SATISFIED, + + // Daemon is running but isn't ready. + DAEMON_STATE_STARTING, + + // Daemon is running and is ready. + DAEMON_STATE_RUNNING, + + // Daemon is running and is being terminated. + DAEMON_STATE_TERMINATING, + + // Daemon just finished running and the other daemons needs to be notified. + DAEMON_STATE_FINISHING, + + // Daemon has finished running. + DAEMON_STATE_FINISHED, +}; + +#define NUM_DAEMON_STATES (DAEMON_STATE_FINISHED + 1) + +struct daemon; + +struct dependency +{ + struct daemon* source; + struct daemon* target; + int flags; +}; + +#define DEPENDENCY_FLAG_REQUIRE (1 << 0) +#define DEPENDENCY_FLAG_AWAIT (1 << 1) +#define DEPENDENCY_FLAG_EXIT_CODE (1 << 2) + +enum log_method +{ + LOG_METHOD_NONE, + LOG_METHOD_APPEND, + LOG_METHOD_ROTATE, +}; + +enum log_format +{ + LOG_FORMAT_NONE, + LOG_FORMAT_SECONDS, + LOG_FORMAT_NANOSECONDS, + LOG_FORMAT_BASIC, + LOG_FORMAT_FULL, + LOG_FORMAT_SYSLOG, +}; + +struct log +{ + char* name; + pid_t pid; + enum log_method method; + enum log_format format; + bool control_messages; + bool rotate_on_start; + size_t max_rotations; + off_t max_line_size; + size_t skipped; + off_t max_size; + char* path; + char* path_src; + char* path_dst; + size_t path_number_offset; + size_t path_number_size; + char* buffer; + size_t buffer_used; + size_t buffer_size; + off_t size; + int fd; + int last_errno; + bool line_terminated; + bool line_begun; + mode_t file_mode; +}; + +struct daemon +{ + char* name; + struct daemon* next_by_state; + struct daemon* prev_by_state; + struct daemon* parent_of_parameter; + struct daemon* first_by_parameter; + struct daemon* last_by_parameter; + struct daemon* prev_by_parameter; + struct daemon* next_by_parameter; + struct dependency** dependencies; + size_t dependencies_used; + size_t dependencies_length; + size_t dependencies_ready; + size_t dependencies_finished; + size_t dependencies_failed; + struct dependency** dependents; + size_t dependents_used; + size_t dependents_length; + size_t reference_count; + size_t pfd_readyfd_index; + size_t pfd_outputfd_index; + struct dependency* exit_code_from; + char* cd; + char* netif; + int argc; + char** argv; + struct termios oldtio; + struct log log; + struct timespec timeout; + pid_t pid; + enum exit_code_meaning exit_code_meaning; + enum daemon_state state; + int exit_code; + int readyfd; + int outputfd; + bool configured; + bool need_tty; + bool was_ready; + bool was_terminated; + bool was_dereferenced; + bool timeout_set; +}; + +struct dependency_config +{ + char* target; + int flags; +}; + +struct daemon_config +{ + char* name; + struct dependency_config** dependencies; + size_t dependencies_used; + size_t dependencies_length; + char* cd; + int argc; + char** argv; + enum exit_code_meaning exit_code_meaning; + bool need_tty; + enum log_method log_method; + enum log_format log_format; + bool log_control_messages; + bool log_rotate_on_start; + size_t log_rotations; + off_t log_line_size; + off_t log_size; + mode_t log_file_mode; +}; + static pid_t main_pid; +static pid_t forward_signal_pid = -1; + +static volatile sig_atomic_t caught_exit_signal = -1; +static sigset_t handled_signals; + +static struct daemon_config default_config = +{ + .log_method = LOG_METHOD_ROTATE, + .log_format = LOG_FORMAT_NANOSECONDS, + .log_control_messages = true, + .log_rotate_on_start = false, + .log_rotations = 3, + .log_line_size = 4096, + .log_size = 1048576, + .log_file_mode = 0644, +}; + +static struct log init_log = { .fd = -1 }; + +static enum verbosity verbosity = VERBOSITY_QUIET; static struct harddisk** hds = NULL; static size_t hds_used = 0; @@ -79,11 +283,70 @@ static struct mountpoint* mountpoints = NULL; static size_t mountpoints_used = 0; static size_t mountpoints_length = 0; +static struct daemon** daemons = NULL; +static size_t daemons_used = 0; +static size_t daemons_length = 0; + +static struct daemon* first_daemon_by_state[NUM_DAEMON_STATES]; +static struct daemon* last_daemon_by_state[NUM_DAEMON_STATES]; +static size_t count_daemon_by_state[NUM_DAEMON_STATES]; + +static struct pollfd* pfds = NULL; +static size_t pfds_used = 0; +static size_t pfds_length = 0; + +static struct daemon** pfds_daemon = NULL; +static size_t pfds_daemon_length = 0; + static bool chain_location_made = false; static char chain_location[] = "/tmp/fs.XXXXXX"; static bool chain_location_dev_made = false; static char chain_location_dev[] = "/tmp/fs.XXXXXX/dev"; +static void signal_handler(int signum) +{ + if ( getpid() != main_pid ) + return; + + if ( forward_signal_pid != -1 ) + { + if ( 0 < forward_signal_pid ) + kill(forward_signal_pid, signum); + return; + } + + switch ( signum ) + { + case SIGINT: caught_exit_signal = 1; break; + case SIGTERM: caught_exit_signal = 0; break; + case SIGQUIT: caught_exit_signal = 2; break; + } +} + +static void install_signal_handler(void) +{ + sigemptyset(&handled_signals); + sigaddset(&handled_signals, SIGINT); + sigaddset(&handled_signals, SIGQUIT); + sigaddset(&handled_signals, SIGTERM); + sigprocmask(SIG_BLOCK, &handled_signals, NULL); + struct sigaction sa = { .sa_handler = signal_handler, .sa_flags = 0 }; + sigaction(SIGINT, &sa, NULL); + sigaction(SIGQUIT, &sa, NULL); + sigaction(SIGTERM, &sa, NULL); +} + +static void uninstall_signal_handler(void) +{ + struct sigaction sa = { .sa_handler = SIG_DFL, .sa_flags = 0 }; + sa.sa_handler = SIG_DFL; + sa.sa_flags = 0; + sigaction(SIGINT, &sa, NULL); + sigaction(SIGQUIT, &sa, NULL); + sigaction(SIGTERM, &sa, NULL); + sigprocmask(SIG_UNBLOCK, &handled_signals, NULL); +} + static char* read_single_line(FILE* fp) { char* ret = NULL; @@ -110,9 +373,470 @@ static char* join_paths(const char* a, const char* b) return result; } -__attribute__((noreturn)) +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 int exit_code_to_exit_status(int exit_code) +{ + if ( WIFEXITED(exit_code) ) + return WEXITSTATUS(exit_code); + else if ( WIFSIGNALED(exit_code) ) + return 128 + WTERMSIG(exit_code); + else + return 2; +} + +static void log_close(struct log* log) +{ + if ( 0 <= log->fd ) + close(log->fd); + log->fd = -1; + free(log->buffer); + log->buffer = NULL; +} + +static void log_error(struct log* log, const char* prefix, const char* path) +{ + // TODO: Log to the init log unless about the init log. + if ( !errno ) + warnx("%s%s", prefix, path ? path : log->path); + else if ( errno != log->last_errno ) + warn("%s%s", prefix, path ? path : log->path); + log->last_errno = errno; +} + +static bool log_open(struct log* log) +{ + if ( log->method == LOG_METHOD_NONE ) + return true; + int logflags = O_CREAT | O_WRONLY | O_APPEND | O_NOFOLLOW; + if ( log->method == LOG_METHOD_APPEND && log->rotate_on_start ) + logflags |= O_TRUNC; + if ( 0 <= log->fd ) + close(log->fd); + log->fd = open(log->path, logflags, log->file_mode); + if ( log->fd < 0 ) + { + log_error(log, "", NULL); + // Don't block daemon startup on read-only filesystems. + return errno == EROFS; + } + struct stat st; + if ( fstat(log->fd, &st) < 0 ) + { + log_error(log, "stat: ", NULL); + close(log->fd); + log->fd = -1; + return false; + } + if ( (st.st_mode & 07777) != log->file_mode ) + { + if ( fchmod(log->fd, log->file_mode) < 0 ) + { + log_error(log, "fchmod: ", NULL); + close(log->fd); + log->fd = -1; + return false; + } + } + log->size = st.st_size; + log->line_terminated = true; + return true; +} + +static bool log_rotate(struct log* log) +{ + if ( log->method == LOG_METHOD_NONE ) + return true; + if ( 0 <= log->fd ) + { + close(log->fd); + log->fd = -1; + } + for ( size_t i = log->max_rotations; 0 < i; i-- ) + { + snprintf(log->path_dst + log->path_number_offset, + log->path_number_size, ".%zu", i); + snprintf(log->path_src + log->path_number_offset, + log->path_number_size, "%c%zu", i-1 != 0 ? '.' : '\0', i-1); + if ( i == log->max_rotations ) + { + if ( !access(log->path_dst, F_OK) ) + { + // Ensure the file system space usage has an upper bound by + // deleting the oldest log. However if another process has the + // log open, the kernel will keep the file contents alive. The + // file is truncated to zero size to avoid disk space remaining + // temporarily in use that way, although the inode itself does + // remain open temporarily. + // TODO: truncateat should have a flags parameter, to not follow + // symbolic links. Otherwise a symlink in /var/log could be + // used to truncate an arbitrary file, which is avoided here. + int fd = open(log->path_dst, O_WRONLY | O_NOFOLLOW); + if ( fd < 0 ) + { + // Don't rotate logs on read-only filesystems. + if ( errno == EROFS ) + break; + log_error(log, "archiving: opening: ", log->path_dst); + } + else + { + if ( ftruncate(fd, 0) < 0 ) + log_error(log, "archiving: truncate: ", log->path_dst); + close(fd); + } + if ( unlink(log->path_dst) < 0 ) + log_error(log, "archiving: unlink: ", log->path_dst); + } + else if ( errno != ENOENT ) + log_error(log, "archiving: ", log->path_dst); + } + if ( rename(log->path_src, log->path_dst) < 0 ) + { + // Don't rotate logs on read-only filesystems. + if ( errno == EROFS ) + break; + // Ignore non-existent logs. + if ( errno != ENOENT ) + { + log_error(log, "archiving: ", log->path_src); + return errno == EROFS; + } + } + } + return log_open(log); +} + +static bool log_initialize(struct log* log, + const char* name, + struct daemon_config* daemon_config) +{ + memset(log, 0, sizeof(*log)); + log->fd = -1; + log->method = daemon_config->log_method; + log->format = daemon_config->log_format; + log->control_messages = daemon_config->log_control_messages; + log->rotate_on_start = daemon_config->log_rotate_on_start; + log->max_rotations = daemon_config->log_rotations; + log->max_line_size = daemon_config->log_line_size; + log->max_size = daemon_config->log_size; + if ( log->max_size < log->max_line_size ) + log->max_line_size = log->max_size; + log->file_mode = daemon_config->log_file_mode; + log->name = strdup(name); + if ( !log->name ) + return false; + if ( asprintf(&log->path, "/var/log/%s.log", name) < 0 ) + return free(log->name), false; + // Preallocate the paths used when renaming log files so there's no error + // conditions when cycling logs. + if ( asprintf(&log->path_src, "%s.%i", log->path, INT_MAX) < 0 ) + return free(log->path), free(log->name), false; + if ( asprintf(&log->path_dst, "%s.%i", log->path, INT_MAX) < 0 ) + return free(log->path_src), free(log->path), free(log->name), false; + log->path_number_offset = strlen(log->path); + log->path_number_size = strlen(log->path_dst) + 1 - log->path_number_offset; + return true; +} + +static bool log_begin_buffer(struct log* log) +{ + log->buffer_used = 0; + log->buffer_size = 4096; + log->buffer = malloc(log->buffer_size); + if ( !log->buffer ) + return false; + return true; +} + +static void log_data_to_buffer(struct log* log, const char* data, size_t length) +{ + assert(log->buffer); + if ( log->skipped ) + { + log->skipped += length; + return; + } + while ( length ) + { + size_t available = log->buffer_size - log->buffer_used; + if ( !available ) + { + if ( 1048576 <= log->buffer_size ) + { + errno = 0; + log_error(log, "in-memory buffer exhausted: ", NULL); + log->skipped += length; + return; + } + size_t new_size = 2 * log->buffer_size; + char* new_buffer = realloc(log->buffer, new_size); + if ( !new_buffer ) + { + log_error(log, "expanding in-memory buffer: ", NULL); + log->skipped += length; + return; + } + log->buffer = new_buffer; + log->buffer_size = new_size; + available = log->buffer_size - log->buffer_used; + } + size_t amount = length < available ? length : available; + memcpy(log->buffer + log->buffer_used, data, amount); + data += amount; + length -= amount; + log->buffer_used += amount; + } +} + +static void log_data(struct log* log, const char* data, size_t length) +{ + if ( log->method == LOG_METHOD_NONE ) + return; + if ( log->fd < 0 && log->buffer ) + { + log_data_to_buffer(log, data, length); + return; + } + if ( log->skipped ) + { + // TODO: Try to log a mesage about how many bytes were skipped and + // resume logging if that worked. For now, let's just lose data. + } + const off_t chunk_cut_offset = log->max_size - log->max_line_size; + size_t sofar = 0; + while ( sofar < length ) + { + if ( log->fd < 0 ) + { + log->skipped += length - sofar; + return; + } + // If the data is currently line terminated, then cut if we can't add + // another line of the maximum length, otherwise cut if the chunk is + // full. + if ( log->method == LOG_METHOD_ROTATE && + (log->line_terminated ? + chunk_cut_offset : + log->max_size) <= log->size ) + { + if ( !log_rotate(log) ) + { + log->skipped += length - sofar; + return; + } + } + // Decide the size of the new chunk to write out. + const char* next_data = data + sofar; + size_t remaining_length = length - sofar; + size_t next_length = remaining_length; + if ( log->method == LOG_METHOD_ROTATE ) + { + off_t chunk_left = log->max_size - log->size; + next_length = + (uintmax_t) remaining_length < (uintmax_t) chunk_left ? + (size_t) remaining_length : (size_t) chunk_left; + // Attempt to cut the log at a newline. + if ( chunk_cut_offset <= log->size + (off_t) next_length ) + { + // Find where the data becomes eligible for a line cut, and + // search for a newline after that. + size_t first_cut_index = + log->size < chunk_cut_offset ? + 0 : + (size_t) (chunk_cut_offset - log->size); + for ( size_t i = first_cut_index; i < next_length; i++ ) + { + if ( next_data[i] == '\n' ) + { + next_length = i + 1; + break; + } + } + } + } + ssize_t amount = write(log->fd, next_data, next_length); + if ( amount < 0 ) + { + log_error(log, "writing: ", NULL); + log->skipped += length - sofar; + return; + } + sofar += amount; + log->size += amount; + log->line_terminated = next_data[amount - 1] == '\n'; + log->last_errno = 0; + } +} + +static void log_formatted(struct log* log, const char* string, size_t length) +{ + if ( log->format == LOG_FORMAT_NONE ) + { + log_data(log, string, length); + return; + } + size_t log_name_length = strlen(log->name); + for ( size_t i = 0; i < length; ) + { + size_t fragment = 1; + while ( string[i + fragment - 1] != '\n' && i + fragment < length ) + fragment++; + if ( !log->line_begun ) + { + struct timespec now; + clock_gettime(CLOCK_REALTIME, &now); + struct tm tm; + gmtime_r(&now.tv_sec, &tm); + char hostname[HOST_NAME_MAX + 1]; + gethostname(hostname, sizeof(hostname)); + if ( log->format == LOG_FORMAT_SYSLOG ) + { + int pri = 3 /* system daemons */ * 8 + 6 /* informational */; + char header[64]; + snprintf(header, sizeof(header), "<%d>1 ", pri); + log_data(log, header, strlen(header)); + } + char timeformat[64] = "%F %T +0000"; + if ( log->format == LOG_FORMAT_SYSLOG ) + snprintf(timeformat, sizeof(timeformat), + "%%FT%%T.%06liZ", now.tv_nsec / 1000); + else if ( log->format != LOG_FORMAT_SECONDS ) + snprintf(timeformat, sizeof(timeformat), + "%%F %%T.%09li +0000", now.tv_nsec); + char timestamp[64]; + strftime(timestamp, sizeof(timestamp), timeformat, &tm); + log_data(log, timestamp, strlen(timestamp)); + if ( log->format == LOG_FORMAT_FULL || + log->format == LOG_FORMAT_SYSLOG ) + { + log_data(log, " ", 1); + log_data(log, hostname, strlen(hostname)); + } + if ( log->format == LOG_FORMAT_BASIC || + log->format == LOG_FORMAT_FULL || + log->format == LOG_FORMAT_SYSLOG ) + { + log_data(log, " ", 1); + log_data(log, log->name, log_name_length); + } + if ( log->format == LOG_FORMAT_SYSLOG ) + { + pid_t pid = 0 < log->pid ? log->pid : getpid(); + char part[64]; + snprintf(part, sizeof(part), " %ji - - ", (intmax_t) pid); + log_data(log, part, strlen(part)); + } + else + log_data(log, ": ", 2); + } + log_data(log, string + i, fragment); + log->line_begun = string[i + fragment - 1] != '\n'; + i += fragment; + } +} + +static size_t log_callback(void* ctx, const char* str, size_t len) +{ + log_formatted((struct log*) ctx, str, len); + return len; +} + +static bool log_begin(struct log* log) +{ + if ( log->method == LOG_METHOD_NONE ) + return true; + bool opened; + if ( log->method == LOG_METHOD_ROTATE && log->rotate_on_start ) + opened = log_rotate(log); + else + opened = log_open(log); + if ( !opened ) + return false; + if ( log->buffer ) + { + log_data(log, log->buffer, log->buffer_used); + free(log->buffer); + log->buffer = NULL; + log->buffer_used = 0; + log->buffer_size = 0; + // TODO: Warn about any skipped data. + log->skipped = 0; + } + return true; +} + +__attribute__((format(printf, 2, 3))) +static void log_status(const char* status, const char* format, ...) +{ + va_list ap; + va_start(ap, format); + vcbprintf(&init_log, log_callback, format, ap); + va_end(ap); + if ( verbosity == VERBOSITY_SILENT || + (verbosity == VERBOSITY_QUIET && + strcmp(status, "failed") != 0 && + strcmp(status, "timeout") != 0) ) + return; + struct timespec now; + clock_gettime(CLOCK_REALTIME, &now); + struct tm tm; + localtime_r(&now.tv_sec, &tm); + va_start(ap, format); + fprintf(stderr, "%04d-%02d-%02d %02d:%02d:%02d ", + tm.tm_year + 1900, + tm.tm_mon + 1, + tm.tm_mday + 1, + tm.tm_hour, + tm.tm_min, + tm.tm_sec); + if ( !strcmp(status, "starting") ) + fprintf(stderr, "[ ] "); + else if ( !strcmp(status, "started") ) + fprintf(stderr, "[ \e[92mOK\e[m ] "); + else if ( !strcmp(status, "finished") ) + fprintf(stderr, "[ \e[92mDONE\e[m ] "); + else if ( !strcmp(status, "failed") ) + fprintf(stderr, "[\e[91mFAILED\e[m] "); + else if ( !strcmp(status, "stopping") ) + 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 + fprintf(stderr, "[ ?? ] "); + vfprintf(stderr, format, ap); + fflush(stderr); + va_end(ap); +} + __attribute__((format(printf, 1, 2))) -static void fatal(const char* format, ...) +noreturn static void fatal(const char* format, ...) { va_list ap; va_start(ap, format); @@ -121,6 +845,13 @@ static void fatal(const char* format, ...) fprintf(stderr, "\n"); fflush(stderr); va_end(ap); + if ( getpid() == main_pid ) + { + va_start(ap, format); + vcbprintf(&init_log, log_callback, format, ap); + log_formatted(&init_log, "\n", 1); + va_end(ap); + } if ( getpid() == main_pid ) exit(2); _exit(2); @@ -136,6 +867,13 @@ static void warning(const char* format, ...) fprintf(stderr, "\n"); fflush(stderr); va_end(ap); + if ( getpid() == main_pid ) + { + va_start(ap, format); + vcbprintf(&init_log, log_callback, format, ap); + log_formatted(&init_log, "\n", 1); + va_end(ap); + } } __attribute__((format(printf, 1, 2))) @@ -148,6 +886,1613 @@ static void note(const char* format, ...) fprintf(stderr, "\n"); fflush(stderr); va_end(ap); + if ( getpid() == main_pid ) + { + va_start(ap, format); + vcbprintf(&init_log, log_callback, format, ap); + log_formatted(&init_log, "\n", 1); + va_end(ap); + } +} + +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; + for ( char c = *string++; c; 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 void daemon_config_free(struct daemon_config* daemon_config) +{ + free(daemon_config->name); + for ( size_t i = 0; i < daemon_config->dependencies_used; i++ ) + { + free(daemon_config->dependencies[i]->target); + free(daemon_config->dependencies[i]); + } + free(daemon_config->dependencies); + free(daemon_config->cd); + for ( int i = 0; i < daemon_config->argc; i++ ) + free(daemon_config->argv[i]); + free(daemon_config->argv); + free(daemon_config); +} + +static bool daemon_config_load_search(struct daemon_config* daemon_config, + size_t next_search_path_index); + +static bool daemon_process_command(struct daemon_config* daemon_config, + const char* path, + size_t argc, + const char* const* argv, + off_t line_number, + size_t next_search_path_index) +{ + if ( !argc ) + return true; + if ( !strcmp(argv[0], "furthermore") ) + { + if ( 2 <= argc ) + warning("%s:%ji: unexpected parameter to %s: %s", + path, (intmax_t) line_number, argv[0], argv[1]); + // TODO: Only once per search path level. + // TODO: How about requiring it to be the first statement? + if ( !daemon_config_load_search(daemon_config, next_search_path_index) ) + { + if ( errno == ENOENT ) + { + warning("%s:%ji: 'furthermore' failed to locate next '%s' " + "configuration file in search path: %m", + path, (intmax_t) line_number, daemon_config->name); + errno = EINVAL; + } + else + warning("%s: while processing 'furthermore': %m", path); + return false; + } + return true; + } + if ( argc == 1 ) + { + warning("%s:%ji: expected parameter: %s", + path, (intmax_t) line_number, argv[0]); + return false; + } + if ( !strcmp(argv[0], "cd") ) + { + free(daemon_config->cd); + if ( !(daemon_config->cd = strdup(argv[1])) ) + { + warning("strdup: %m"); + return false; + } + } + else if ( !strcmp(argv[0], "exec") ) + { + for ( int i = 0; i < daemon_config->argc; i++ ) + free(daemon_config->argv[i]); + free(daemon_config->argv); + daemon_config->argc = 0; + daemon_config->argv = NULL; + if ( INT_MAX - 1 < argc - 1 ) + { + warning("%s:%ji: too many arguments: %s", + path, (intmax_t) line_number, argv[0]); + return false; + } + int new_argc = argc - 1; + char** new_argv = calloc(new_argc + 1, sizeof(char*)); + if ( !new_argv ) + { + warning("malloc: %m"); + return false; + } + for ( int i = 0; i < new_argc; i++ ) + { + size_t n = 1 + (size_t) i; + if ( !(new_argv[i] = strdup(argv[n])) ) + { + warning("malloc: %m"); + for ( int j = 0; i < j; j++ ) + free(new_argv[j]); + free(new_argv); + return false; + } + } + daemon_config->argc = new_argc; + daemon_config->argv = new_argv; + } + else if ( !strcmp(argv[0], "exit-code-meaning") ) + { + if ( !strcmp(argv[1], "default") ) + daemon_config->exit_code_meaning = EXIT_CODE_MEANING_DEFAULT; + else if ( !strcmp(argv[1], "poweroff-reboot") ) + daemon_config->exit_code_meaning = + EXIT_CODE_MEANING_POWEROFF_REBOOT; + else + warning("%s:%ji: unknown %s: %s", + path, (intmax_t) line_number, argv[0], argv[1]); + } + else if ( !strcmp(argv[0], "log-control-messages") ) + { + if ( !strcmp(argv[1], "true") ) + daemon_config->log_control_messages = true; + else if ( !strcmp(argv[1], "false") ) + daemon_config->log_control_messages = false; + else + warning("%s:%ji: unknown %s: %s", + path, (intmax_t) line_number, argv[0], argv[1]); + } + else if ( !strcmp(argv[0], "log-file-mode") ) + { + char* end; + errno = 0; + uintmax_t value = strtoumax(argv[1], &end, 8); + if ( argv[1] == end || errno || value != (value & 07777) ) + warning("%s:%ji: invalid %s: %s", + path, (intmax_t) line_number, argv[0], argv[1]); + else + daemon_config->log_file_mode = (mode_t) value; + } + else if ( !strcmp(argv[0], "log-format") ) + { + if ( !strcmp(argv[1], "none") ) + daemon_config->log_format = LOG_FORMAT_NONE; + else if ( !strcmp(argv[1], "seconds") ) + daemon_config->log_format = LOG_FORMAT_SECONDS; + else if ( !strcmp(argv[1], "nanoseconds") ) + daemon_config->log_format = LOG_FORMAT_NANOSECONDS; + else if ( !strcmp(argv[1], "basic") ) + daemon_config->log_format = LOG_FORMAT_BASIC; + else if ( !strcmp(argv[1], "full") ) + daemon_config->log_format = LOG_FORMAT_FULL; + else if ( !strcmp(argv[1], "syslog") ) + daemon_config->log_format = LOG_FORMAT_SYSLOG; + else + warning("%s:%ji: unknown %s: %s", + path, (intmax_t) line_number, argv[0], argv[1]); + } + else if ( !strcmp(argv[0], "log-line-size") ) + { + char* end; + errno = 0; + intmax_t value = strtoimax(argv[1], &end, 10); + if ( argv[1] == end || errno || value != (off_t) value || value < 0 ) + warning("%s:%ji: invalid %s: %s", + path, (intmax_t) line_number, argv[0], argv[1]); + else + daemon_config->log_line_size = (off_t) value; + } + else if ( !strcmp(argv[0], "log-method") ) + { + if ( !strcmp(argv[1], "append") ) + daemon_config->log_method = LOG_METHOD_APPEND; + else if ( !strcmp(argv[1], "rotate") ) + daemon_config->log_method = LOG_METHOD_ROTATE; + else + warning("%s:%ji: unknown %s: %s", + path, (intmax_t) line_number, argv[0], argv[1]); + } + else if ( !strcmp(argv[0], "log-rotate-on-start") ) + { + if ( !strcmp(argv[1], "true") ) + daemon_config->log_rotate_on_start = true; + else if ( !strcmp(argv[1], "false") ) + daemon_config->log_rotate_on_start = false; + else + warning("%s:%ji: unknown %s: %s", + path, (intmax_t) line_number, argv[0], argv[1]); + } + else if ( !strcmp(argv[0], "log-size") ) + { + char* end; + errno = 0; + intmax_t value = strtoimax(argv[1], &end, 10); + if ( argv[1] == end || errno || value != (off_t) value || value < 0 ) + warning("%s:%ji: invalid %s: %s", + path, (intmax_t) line_number, argv[0], argv[1]); + else + daemon_config->log_size = (off_t) value; + } + else if ( !strcmp(argv[0], "need") ) + { + if ( !strcmp(argv[1], "tty") ) + daemon_config->need_tty = true; + else + warning("%s:%ji: unknown %s: %s", + path, (intmax_t) line_number, argv[0], argv[1]); + } + else if ( !strcmp(argv[0], "require") ) + { + char* target = strdup(argv[1]); + if ( !target ) + { + warning("strdup: %m"); + return false; + } + int negated_flags = DEPENDENCY_FLAG_REQUIRE | DEPENDENCY_FLAG_AWAIT; + int flags = negated_flags; + for ( size_t i = 2; 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 + warning("%s:%ji: %s %s: unknown flag: %s", path, + (intmax_t) line_number, argv[0], argv[1], argv[i]); + } + bool had_exit_code = false; + struct dependency_config* dependency = NULL; + for ( size_t i = 0; i < daemon_config->dependencies_used; i++ ) + { + struct dependency_config* dep = daemon_config->dependencies[i]; + if ( dep->flags & DEPENDENCY_FLAG_EXIT_CODE ) + had_exit_code = true; + if ( !strcmp(dep->target, target) ) + dependency = dep; + } + if ( (flags & DEPENDENCY_FLAG_EXIT_CODE) && had_exit_code ) + { + warning("%s:%ji: %s %s: exit-code had already been set", + path, (intmax_t) line_number, argv[0], argv[1]); + flags &= ~DEPENDENCY_FLAG_EXIT_CODE; + } + if ( dependency ) + { + dependency->flags &= flags & negated_flags; + dependency->flags |= flags & ~negated_flags; + free(target); + } + else + { + dependency = (struct dependency_config*) + calloc(1, sizeof(struct dependency_config)); + if ( !dependency ) + { + warning("malloc: %m"); + free(target); + return false; + } + dependency->target = target; + dependency->flags = flags; + if ( !array_add((void***) &daemon_config->dependencies, + &daemon_config->dependencies_used, + &daemon_config->dependencies_length, + dependency) ) + { + warning("malloc: %m"); + free(target); + free(dependency); + return false; + } + } + } + else if ( !strcmp(argv[0], "tty") ) + { + // TODO: Implement. + } + else if ( !strcmp(argv[0], "unset") ) + { + if ( !strcmp(argv[1], "cd") ) + { + free(daemon_config->cd); + daemon_config->cd = NULL; + } + else if ( !strcmp(argv[1], "exec") ) + { + for ( int i = 0; i < daemon_config->argc; i++ ) + free(daemon_config->argv[i]); + free(daemon_config->argv); + daemon_config->argc = 0; + daemon_config->argv = NULL; + } + else if ( !strcmp(argv[1], "exit-code-meaning") ) + daemon_config->exit_code_meaning = EXIT_CODE_MEANING_DEFAULT; + else if ( !strcmp(argv[1], "log-control-messages") ) + daemon_config->log_control_messages = + default_config.log_control_messages; + else if ( !strcmp(argv[1], "log-file-mode") ) + daemon_config->log_file_mode = default_config.log_file_mode; + else if ( !strcmp(argv[1], "log-format") ) + daemon_config->log_format = default_config.log_format; + else if ( !strcmp(argv[1], "log-line-size") ) + daemon_config->log_line_size = default_config.log_line_size; + else if ( !strcmp(argv[1], "log-method") ) + daemon_config->log_method = default_config.log_method; + else if ( !strcmp(argv[1], "log-rotate-on-start") ) + daemon_config->log_rotate_on_start = + default_config.log_rotate_on_start; + else if ( !strcmp(argv[1], "log-size") ) + daemon_config->log_line_size = default_config.log_line_size; + else if ( !strcmp(argv[1], "need") ) + { + if ( argc < 3 ) + warning("%s:%ji: expected parameter: %s: %s", + path, (intmax_t) line_number, argv[0], argv[1]); + else if ( !strcmp(argv[2], "tty") ) + daemon_config->need_tty = false; + else + warning("%s:%ji: %s %s: unknown: %s", path, + (intmax_t) line_number, argv[0], argv[1], argv[2]); + } + else if ( !strcmp(argv[1], "require") ) + { + if ( argc < 3 ) + { + for ( size_t i = 0; i < daemon_config->dependencies_used; i++ ) + { + free(daemon_config->dependencies[i]->target); + free(daemon_config->dependencies[i]); + } + free(daemon_config->dependencies); + daemon_config->dependencies_used = 0; + daemon_config->dependencies = NULL; + return true; + } + const char* target = argv[2]; + // TODO: Linear time lookup. + struct dependency_config* dependency = NULL; + size_t i; + for ( i = 0; i < daemon_config->dependencies_used; i++ ) + { + if ( !strcmp(daemon_config->dependencies[i]->target, target) ) + { + dependency = daemon_config->dependencies[i]; + break; + } + } + if ( !dependency ) + { + warning("%s:%ji: dependency wasn't already required: %s", + path, (intmax_t) line_number, target); + return true; + } + if ( argc <= 3 ) + { + free(daemon_config->dependencies[i]->target); + size_t last = daemon_config->dependencies_used - 1; + if ( i != last ) + { + daemon_config->dependencies[i] = + daemon_config->dependencies[last]; + daemon_config->dependencies[last] = NULL; + } + daemon_config->dependencies_used--; + } + else for ( size_t i = 3; i < argc; i++ ) + { + if ( !strcmp(argv[i], "optional") ) + dependency->flags |= DEPENDENCY_FLAG_REQUIRE; + else if ( !strcmp(argv[i], "no-await") ) + dependency->flags |= DEPENDENCY_FLAG_AWAIT; + else if ( !strcmp(argv[i], "exit-code") ) + dependency->flags &= ~DEPENDENCY_FLAG_EXIT_CODE; + else + warning("%s:%ji: %s %s %s: unknown flag: %s", + path, (intmax_t) line_number, argv[0], argv[1], + argv[2], argv[i]); + } + } + else if ( !strcmp(argv[1], "tty") ) + { + // TODO: Implement. + } + else + warning("%s:%ji: unknown unset operation: %s", + path, (intmax_t) line_number, argv[0]); + } + else + warning("%s:%ji: unknown operation: %s", + path, (intmax_t) line_number, argv[0]); + return true; +} + +static bool daemon_process_line(struct daemon_config* daemon_config, + const char* path, + char* line, + off_t line_number, + size_t next_search_path_index) +{ + size_t argc = 0; + char** argv = tokenize(&argc, line); + if ( !argv ) + { + if ( !errno ) + warning("%s:%ji: syntax error", path, (intmax_t) line_number); + else + warning("%s: %m", path); + return false; + } + bool result = daemon_process_command(daemon_config, path, argc, + (const char* const*) argv, line_number, + next_search_path_index); + for ( size_t i = 0; i < argc; i++ ) + free(argv[i]); + free(argv); + return result; +} + +static bool daemon_config_load_from_path(struct daemon_config* daemon_config, + const char* path, + size_t next_search_path_index) +{ + FILE* fp = fopen(path, "r"); + if ( !fp ) + { + if ( errno != ENOENT ) + warning("%s: Failed to open daemon configuration file: %m", path); + return false; + } + char* line = NULL; + size_t line_size = 0; + ssize_t line_length; + off_t line_number = 0; + while ( 0 < (line_length = getline(&line, &line_size, fp)) ) + { + if ( line[line_length-1] == '\n' ) + line[--line_length] = '\0'; + line_number++; + if ( !daemon_process_line(daemon_config, path, line, line_number, + next_search_path_index) ) + { + fclose(fp); + free(line); + if ( errno == ENOENT ) + errno = EINVAL; + return false; + } + } + free(line); + if ( ferror(fp) ) + { + warning("%s: %m", path); + fclose(fp); + return false; + } + fclose(fp); + return true; +} + +static bool daemon_config_load_search(struct daemon_config* daemon_config, + size_t next_search_path_index) +{ + // If the search path ever becomes arbitrarily long, consider handling the + // 'furthermore' feature in a manner using constant stack space rather than + // recursion. + const char* search_paths[] = + { + "/etc/init", + "/share/init", + }; + size_t search_paths_count = sizeof(search_paths) / sizeof(search_paths[0]); + for ( size_t i = next_search_path_index; i < search_paths_count; i++ ) + { + const char* search_path = search_paths[i]; + char* path = join_paths(search_path, daemon_config->name); + if ( !path ) + { + warning("malloc: %m"); + return false; + } + if ( !daemon_config_load_from_path(daemon_config, path, i + 1) ) + { + free(path); + if ( errno == ENOENT ) + continue; + return NULL; + } + free(path); + return true; + } + errno = ENOENT; + return false; +} + +static void daemon_config_initialize(struct daemon_config* daemon_config) +{ + memset(daemon_config, 0, sizeof(*daemon_config)); + daemon_config->log_method = default_config.log_method; + daemon_config->log_format = default_config.log_format; + daemon_config->log_control_messages = default_config.log_control_messages; + daemon_config->log_rotate_on_start = default_config.log_rotate_on_start; + daemon_config->log_rotations = default_config.log_rotations; + daemon_config->log_line_size = default_config.log_line_size; + daemon_config->log_size = default_config.log_size; + daemon_config->log_file_mode = default_config.log_file_mode; +} + +static struct daemon_config* daemon_config_load(const char* name) +{ + struct daemon_config* daemon_config = malloc(sizeof(struct daemon_config)); + if ( !daemon_config ) + { + warning("malloc: %m"); + return NULL; + } + daemon_config_initialize(daemon_config); + if ( !(daemon_config->name = strdup(name)) ) + { + warning("malloc: %m"); + daemon_config_free(daemon_config); + return NULL; + } + if ( !daemon_config_load_search(daemon_config, 0) ) + { + if ( errno == ENOENT ) + warning("Failed to locate daemon configuration: %s: %m", name); + daemon_config_free(daemon_config); + return NULL; + } + return daemon_config; +} + +// TODO: Replace with better data structure. +static struct daemon* add_daemon(void) +{ + struct daemon* daemon = calloc(1, sizeof(struct daemon)); + if ( !daemon ) + fatal("malloc: %m"); + if ( !array_add((void***) &daemons, &daemons_used, &daemons_length, + daemon) ) + fatal("malloc: %m"); + return daemon; +} + +// TODO: This runs in O(n) but could be in O(log n). +static struct daemon* daemon_find_by_name(const char* name) +{ + for ( size_t i = 0; i < daemons_used; i++ ) + if ( !strcmp(daemons[i]->name, name) ) + return daemons[i]; + return NULL; +} + +// TODO: This runs in O(n) but could be in O(log n). +static struct daemon* daemon_find_by_pid(pid_t pid) +{ + for ( size_t i = 0; i < daemons_used; i++ ) + if ( daemons[i]->pid == pid ) + return daemons[i]; + return NULL; +} + +static bool daemon_is_failed(struct daemon* daemon) +{ + if ( daemon->was_terminated && + WIFSIGNALED(daemon->exit_code) && + WTERMSIG(daemon->exit_code) == SIGTERM ) + return false; + switch ( daemon->exit_code_meaning ) + { + case EXIT_CODE_MEANING_DEFAULT: + return !WIFEXITED(daemon->exit_code) || + WEXITSTATUS(daemon->exit_code) != 0; + case EXIT_CODE_MEANING_POWEROFF_REBOOT: + return !WIFEXITED(daemon->exit_code) || + 3 <= WEXITSTATUS(daemon->exit_code); + } + return true; +} + +static void daemon_insert_state_list(struct daemon* daemon) +{ + assert(!daemon->prev_by_state); + assert(!daemon->next_by_state); + assert(first_daemon_by_state[daemon->state] != daemon); + assert(last_daemon_by_state[daemon->state] != daemon); + daemon->prev_by_state = last_daemon_by_state[daemon->state]; + daemon->next_by_state = NULL; + if ( last_daemon_by_state[daemon->state] ) + last_daemon_by_state[daemon->state]->next_by_state = daemon; + else + first_daemon_by_state[daemon->state] = daemon; + last_daemon_by_state[daemon->state] = daemon; + count_daemon_by_state[daemon->state]++; +} + +static void daemon_remove_state_list(struct daemon* daemon) +{ + assert(daemon->prev_by_state || + daemon == first_daemon_by_state[daemon->state]); + assert(daemon->next_by_state || + daemon == last_daemon_by_state[daemon->state]); + assert(0 < count_daemon_by_state[daemon->state]); + if ( daemon->prev_by_state ) + daemon->prev_by_state->next_by_state = daemon->next_by_state; + else + first_daemon_by_state[daemon->state] = daemon->next_by_state; + if ( daemon->next_by_state ) + daemon->next_by_state->prev_by_state = daemon->prev_by_state; + else + last_daemon_by_state[daemon->state] = daemon->prev_by_state; + count_daemon_by_state[daemon->state]--; + daemon->prev_by_state = NULL; + daemon->next_by_state = NULL; +} + +static void daemon_change_state_list(struct daemon* daemon, + enum daemon_state new_state) +{ + daemon_remove_state_list(daemon); + daemon->state = new_state; + daemon_insert_state_list(daemon); +} + +static struct daemon* daemon_create_unconfigured(const char* name) +{ + struct daemon* daemon = add_daemon(); + if ( !(daemon->name = strdup(name)) ) + fatal("malloc: %m"); + daemon->state = DAEMON_STATE_TERMINATED; + daemon->readyfd = -1; + daemon->outputfd = -1; + daemon->log.fd = -1; + daemon_insert_state_list(daemon); + return daemon; +} + +static bool daemon_add_dependency(struct daemon* daemon, + struct daemon* target, + int flags) +{ + struct dependency* dependency = calloc(1, sizeof(struct dependency)); + if ( !dependency ) + return false; + dependency->source = daemon; + dependency->target = target; + dependency->flags = flags; + if ( !array_add((void***) &daemon->dependencies, + &daemon->dependencies_used, + &daemon->dependencies_length, + dependency) ) + { + free(dependency); + return false; + } + if ( !array_add((void***) &target->dependents, + &target->dependents_used, + &target->dependents_length, + dependency) ) + { + daemon->dependencies_used--; + free(dependency); + return false; + } + if ( flags & DEPENDENCY_FLAG_EXIT_CODE ) + daemon->exit_code_from = dependency; + target->reference_count++; + return true; +} + +static void daemon_configure_sub(struct daemon* daemon, + struct daemon_config* daemon_config, + const char* netif) +{ + assert(!daemon->configured); + daemon->dependencies = (struct dependency**) + reallocarray(NULL, daemon_config->dependencies_used, + sizeof(struct dependency*)); + if ( !daemon->dependencies ) + fatal("malloc: %m"); + daemon->dependencies_used = 0; + daemon->dependencies_length = daemon_config->dependencies_length; + for ( size_t i = 0; i < daemon_config->dependencies_used; i++ ) + { + struct dependency_config* dependency_config = + daemon_config->dependencies[i]; + struct daemon* target = daemon_find_by_name(dependency_config->target); + if ( !target ) + target = daemon_create_unconfigured(dependency_config->target); + if ( target->netif ) + { + // daemon_find_by_name cannot create daemons per if. + warning("%s cannot depend on parameterized daemon %s", + daemon->name, target->name); + continue; + } + if ( !daemon_add_dependency(daemon, target, dependency_config->flags) ) + fatal("malloc: %m"); + } + if ( daemon_config->cd && !(daemon->cd = strdup(daemon_config->cd)) ) + fatal("malloc: %m"); + if ( daemon_config->argv ) + { + daemon->argc = daemon_config->argc; + if ( netif ) + { + if ( INT_MAX - 1 <= daemon->argc ) + { + errno = ENOMEM; + fatal("malloc: %m"); + } + daemon->argc++; + } + daemon->argv = calloc(daemon->argc + 1, sizeof(char*)); + if ( !daemon->argv ) + fatal("malloc: %m"); + for ( int i = 0; i < daemon_config->argc; i++ ) + if ( !(daemon->argv[i] = strdup(daemon_config->argv[i])) ) + fatal("malloc: %m"); + if ( netif && !(daemon->argv[daemon_config->argc] = strdup(netif)) ) + fatal("malloc: %m"); + } + daemon->exit_code_meaning = daemon_config->exit_code_meaning; + if ( netif && !(daemon->netif = strdup(netif)) ) + fatal("malloc: %m"); + if ( !log_initialize(&daemon->log, daemon->name, daemon_config) ) + fatal("malloc: %m"); + daemon->need_tty = daemon_config->need_tty; + daemon->configured = true; +} + +static void daemon_configure(struct daemon* daemon, + struct daemon_config* daemon_config) +{ + // Parameterized daemons will be instatiated here later on. + daemon_configure_sub(daemon, daemon_config, NULL); +} + +static struct daemon* daemon_create(struct daemon_config* daemon_config) +{ + struct daemon* daemon = daemon_create_unconfigured(daemon_config->name); + daemon_configure(daemon, daemon_config); + return daemon; +} + +static void schedule_daemon(struct daemon* daemon) +{ + assert(daemon->state == DAEMON_STATE_TERMINATED); + daemon_change_state_list(daemon, DAEMON_STATE_SCHEDULED); +} + +static void daemon_on_finished(struct daemon* daemon) +{ + assert(daemon->state != DAEMON_STATE_FINISHING); + assert(daemon->state != DAEMON_STATE_FINISHED); + if ( daemon_is_failed(daemon) ) + log_status("failed", "%s exited unsuccessfully.\n", daemon->name); + else if ( daemon->state == DAEMON_STATE_TERMINATING ) + log_status("stopped", "Stopped %s.\n", daemon->name); + else + log_status("finished", "Finished %s.\n", daemon->name); + daemon_change_state_list(daemon, DAEMON_STATE_FINISHING); +} + +static void daemon_terminate(struct daemon* daemon) +{ + assert(!daemon->was_terminated); + daemon->was_terminated = true; + 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_on_not_referenced(struct daemon* daemon) +{ + assert(daemon->reference_count == 0); + switch ( daemon->state ) + { + case DAEMON_STATE_TERMINATED: + case DAEMON_STATE_SCHEDULED: + case DAEMON_STATE_WAITING: + case DAEMON_STATE_SATISFIED: + daemon_change_state_list(daemon, DAEMON_STATE_FINISHING); + break; + case DAEMON_STATE_STARTING: + case DAEMON_STATE_RUNNING: + daemon_terminate(daemon); + // Dependencies are dereferenced when the daemon terminates. + break; + case DAEMON_STATE_TERMINATING: + case DAEMON_STATE_FINISHING: + case DAEMON_STATE_FINISHED: + // Dependencies are dereferenced when the daemon terminates. + break; + } +} + +static void daemon_dereference(struct daemon* daemon) +{ + assert(0 < daemon->reference_count); + daemon->reference_count--; + if ( !daemon->reference_count ) + daemon_on_not_referenced(daemon); +} + +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); +} + +static void daemon_on_dependency_ready(struct dependency* dependency) +{ + struct daemon* daemon = dependency->source; + daemon->dependencies_ready++; + if ( daemon->state == DAEMON_STATE_WAITING && + daemon->dependencies_ready == daemon->dependencies_used ) + daemon_change_state_list(daemon, DAEMON_STATE_SATISFIED); +} + +static void daemon_mark_ready(struct daemon* daemon) +{ + daemon_change_state_list(daemon, DAEMON_STATE_RUNNING); + daemon->was_ready = true; + for ( size_t i = 0; i < daemon->dependents_used; i++ ) + { + if ( (daemon->dependents[i]->flags & DEPENDENCY_FLAG_AWAIT) ) + daemon_on_dependency_ready(daemon->dependents[i]); + } +} + +static void daemon_on_ready(struct daemon* daemon) +{ + log_status("started", "Started %s.\n", daemon->name); + daemon_mark_ready(daemon); +} + +static void daemon_on_dependency_finished(struct dependency* dependency) +{ + struct daemon* daemon = dependency->source; + struct daemon* target = dependency->target; + daemon->dependencies_finished++; + if ( daemon->state == DAEMON_STATE_FINISHING || + daemon->state == DAEMON_STATE_FINISHED ) + return; + bool failed = (dependency->flags & DEPENDENCY_FLAG_REQUIRE) && + daemon_is_failed(target); + if ( failed ) + daemon->dependencies_failed++; + // Don't stop a running non-virtual daemon if dependencies failed. + if ( daemon->argv && + (daemon->state == DAEMON_STATE_STARTING || + daemon->state == DAEMON_STATE_RUNNING || + daemon->state == DAEMON_STATE_TERMINATING) ) + return; + if ( daemon->exit_code_from && + (dependency->flags & DEPENDENCY_FLAG_EXIT_CODE) ) + { + daemon->exit_code = target->exit_code; + daemon->exit_code_meaning = target->exit_code_meaning; + daemon_on_finished(daemon); + return; + } + if ( failed ) + daemon->exit_code = WCONSTRUCT(WNATURE_EXITED, 3, 0); + if ( failed || + (!daemon->argv && + daemon->dependencies_finished == daemon->dependencies_used) ) + daemon_on_finished(daemon); +} + +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); + for ( size_t i = 0; i < daemon->dependents_used; i++ ) + daemon_on_dependency_finished(daemon->dependents[i]); + daemon_dereference_dependencies(daemon); +} + +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); +} + +static void daemon_register_pollfd(struct daemon* daemon, + int fd, + size_t* out_index, + short events) +{ + assert(pfds_used < pfds_length); + assert(pfds_used < pfds_daemon_length); + size_t index = pfds_used++; + struct pollfd* pfd = pfds + index; + memset(pfd, 0, sizeof(*pfd)); + pfd->fd = fd; + pfd->events = events; + pfds_daemon[index] = daemon; + *out_index = index; +} + +static void daemon_unregister_pollfd(struct daemon* daemon, size_t index) +{ + assert(pfds_used <= pfds_length); + assert(index < pfds_used); + assert(pfds_daemon[index] == daemon); + // This function is relied on to not mess with any pollfds prior to the + // index, so it doesn't break a forward iteration on the pollfds. + size_t last_index = pfds_used - 1; + if ( index != last_index ) + { + memcpy(pfds + index, pfds + last_index, sizeof(*pfds)); + pfds_daemon[index] = pfds_daemon[last_index]; + if ( 0 <= pfds_daemon[index]->readyfd && + pfds_daemon[index]->pfd_readyfd_index == last_index ) + pfds_daemon[index]->pfd_readyfd_index = index; + if ( 0 <= pfds_daemon[index]->outputfd && + pfds_daemon[index]->pfd_outputfd_index == last_index ) + pfds_daemon[index]->pfd_outputfd_index = index; + } + pfds_used--; + memset(pfds + last_index, 0, sizeof(*pfds)); + pfds_daemon[last_index] = NULL; +} + +static void daemon_wait(struct daemon* daemon) +{ + assert(daemon->state == DAEMON_STATE_SCHEDULED); + if ( !daemon->configured ) + { + struct daemon_config* daemon_config = daemon_config_load(daemon->name); + if ( !daemon_config ) + { + log_status("failed", "Failed to load configuration for %s.\n", + daemon->name); + daemon->exit_code = WCONSTRUCT(WNATURE_EXITED, 3, 0); + daemon_on_startup_error(daemon); + return NULL; + } + daemon_configure(daemon, daemon_config); + daemon_config_free(daemon_config); + } + if ( daemon->dependencies_ready == daemon->dependencies_used ) + daemon_change_state_list(daemon, DAEMON_STATE_SATISFIED); + else + daemon_change_state_list(daemon, DAEMON_STATE_WAITING); + for ( size_t i = 0; i < daemon->dependencies_used; i++ ) + { + // TODO: Require the dependency graph to be an directed acylic graph. + struct dependency* dependency = daemon->dependencies[i]; + assert(dependency->source == daemon); + assert(dependency->target); + + switch ( dependency->target->state ) + { + case DAEMON_STATE_TERMINATED: + schedule_daemon(dependency->target); + if ( !(dependency->flags & DEPENDENCY_FLAG_AWAIT) ) + daemon_on_dependency_ready(dependency); + break; + case DAEMON_STATE_SCHEDULED: + case DAEMON_STATE_WAITING: + case DAEMON_STATE_SATISFIED: + case DAEMON_STATE_STARTING: + if ( !(dependency->flags & DEPENDENCY_FLAG_AWAIT) ) + daemon_on_dependency_ready(dependency); + break; + case DAEMON_STATE_RUNNING: + case DAEMON_STATE_TERMINATING: + case DAEMON_STATE_FINISHING: + daemon_on_dependency_ready(dependency); + break; + case DAEMON_STATE_FINISHED: + daemon_on_dependency_ready(dependency); + daemon_on_dependency_finished(dependency); + break; + } + if ( daemon->state != DAEMON_STATE_WAITING ) + break; + } +} + +noreturn static void exit_errfd(int errfd, const char* action) +{ + int errnum = errno; + write(errfd, &errnum, sizeof(errnum)); + write(errfd, action, strlen(action)); + _exit(127); +} + +static void daemon_start(struct daemon* daemon) +{ + assert(daemon->state == DAEMON_STATE_SATISFIED); + if ( !daemon->argv ) + { + daemon_on_ready(daemon); + if ( daemon->exit_code_from ) + { + struct daemon* target = daemon->exit_code_from->target; + if ( target->state == DAEMON_STATE_FINISHED ) + { + daemon->exit_code = target->exit_code; + daemon->exit_code_meaning = target->exit_code_meaning; + daemon_on_finished(daemon); + } + } + else if ( daemon->dependencies_finished == daemon->dependencies_used ) + { + daemon_on_finished(daemon); + } + return; + } + if ( 0 < daemon->dependencies_failed ) + { + log_status("failed", "Failed to start %s due to failed dependencies.\n", + daemon->name); + daemon->exit_code = WCONSTRUCT(WNATURE_EXITED, 3, 0); + daemon_on_startup_error(daemon); + return; + } + log_status("starting", "Starting %s...\n", daemon->name); + uid_t uid = getuid(); + pid_t ppid = getpid(); + struct passwd* pwd = getpwuid(uid); + if ( !pwd ) + fatal("looking up user by uid %" PRIuUID ": %m", uid); + const char* home = pwd->pw_dir[0] ? pwd->pw_dir : "/"; + const char* shell = pwd->pw_shell[0] ? pwd->pw_shell : "sh"; + const char* cd = daemon->cd ? daemon->cd : "/"; + // TODO: This is a hack. + if ( !strcmp(cd, "$HOME") ) + cd = home; + int outputfds[2]; + int readyfds[2]; + if ( !daemon->need_tty ) + { + size_t required_fds = 2; + if ( pfds_length - pfds_used < required_fds ) + { + size_t old_length = pfds_length ? pfds_length : required_fds; + struct pollfd* new_pfds = + reallocarray(pfds, old_length, 2 * sizeof(struct pollfd)); + if ( !new_pfds ) + fatal("malloc"); + pfds = new_pfds; + pfds_length = old_length * 2; + } + if ( pfds_daemon_length - pfds_used < required_fds ) + { + size_t old_length = + pfds_daemon_length ? pfds_daemon_length : required_fds; + struct daemon** new_pfds_daemon = + reallocarray(pfds_daemon, old_length, + 2 * sizeof(struct daemon*)); + if ( !new_pfds_daemon ) + fatal("malloc"); + pfds_daemon = new_pfds_daemon; + pfds_daemon_length = old_length * 2; + } + if ( !log_begin(&daemon->log) ) + { + // TODO: Mode where daemons are stopped if logging fails. + } + if ( pipe(outputfds) < 0 ) + fatal("pipe"); + daemon->outputfd = outputfds[0]; + fcntl(daemon->outputfd, F_SETFL, O_NONBLOCK); + // Setup the pollfd for the outputfd. + daemon_register_pollfd(daemon, daemon->outputfd, + &daemon->pfd_outputfd_index, POLLIN); + // Create the readyfd. + if ( pipe(readyfds) < 0 ) + fatal("pipe"); + daemon->readyfd = readyfds[0]; + fcntl(daemon->readyfd, F_SETFL, O_NONBLOCK); + // Setup the pollfd for the readyfd. + daemon_register_pollfd(daemon, daemon->readyfd, + &daemon->pfd_readyfd_index, POLLIN); + } + // TODO: This is not concurrency safe, build a environment array just for + // this daemon. + char ppid_str[sizeof(pid_t) * 3]; + snprintf(ppid_str, sizeof(ppid_str), "%" PRIiPID, ppid); + if ( (!daemon->need_tty && setenv("READYFD", "3", 1)) < 0 || + setenv("INIT_PID", ppid_str, 1) < 0 || + setenv("LOGNAME", pwd->pw_name, 1) < 0 || + setenv("USER", pwd->pw_name, 1) < 0 || + setenv("HOME", home, 1) < 0 || + setenv("SHELL", shell, 1) < 0 ) + fatal("setenv"); + int errfds[2]; + if ( pipe2(errfds, O_CLOEXEC) < 0 ) + fatal("pipe"); + daemon->pid = daemon->log.pid = fork(); + if ( daemon->pid < 0 ) + fatal("fork: %m"); + if ( daemon->need_tty ) + { + if ( tcgetattr(0, &daemon->oldtio) ) + fatal("tcgetattr: %m"); + } + if ( daemon->pid == 0 ) + { + uninstall_signal_handler(); + close(errfds[0]); + if ( chdir(cd) < 0 ) + exit_errfd(errfds[1], "chdir"); + if ( daemon->need_tty ) + { + pid_t pid = getpid(); + // TODO: Support for setsid(2). + if ( setpgid(0, 0) < 0 ) + exit_errfd(errfds[1], "setpgid"); + sigset_t oldset, sigttou; + sigemptyset(&sigttou); + sigaddset(&sigttou, SIGTTOU); + sigprocmask(SIG_BLOCK, &sigttou, &oldset); + if ( tcsetpgrp(0, pid) < 0 ) + exit_errfd(errfds[1], "tcsetpgrp"); + daemon->oldtio.c_cflag |= CREAD; + if ( tcsetattr(0, TCSANOW, &daemon->oldtio) < 0 ) + exit_errfd(errfds[1], "tcsetattr"); + sigprocmask(SIG_SETMASK, &oldset, NULL); + dup3(errfds[1], 3, O_CLOEXEC); + closefrom(4); + } + else + { + close(0); + close(1); + close(2); + if ( open("/dev/null", O_RDONLY) < 0 ) + exit_errfd(errfds[1], "open"); + // The lowest available file descriptor is always allocated, and + // because outputfds is allocated first, then readyfds, and finally + // errfds, then it's safe to move them downwards in that order. + dup2(outputfds[1], 1); + dup2(outputfds[1], 2); + dup2(readyfds[1], 3); + dup3(errfds[1], 4, O_CLOEXEC); + closefrom(5); + } + // TODO: This is a hack. + if ( !strcmp(daemon->argv[0], "$SHELL") ) + daemon->argv[0] = (char*) shell; + execvp(daemon->argv[0], daemon->argv); + exit_errfd(4, "execve"); + } + if ( !daemon->need_tty ) + { + close(outputfds[1]); + close(readyfds[1]); + } + close(errfds[1]); + int errnum; + if ( read(errfds[0], &errnum, sizeof(errnum)) == sizeof(errnum) ) + { + char action[16] = ""; + ssize_t amount = read(errfds[0], action, sizeof(action) - 1); + if ( 0 <= amount ) + action[amount] = '\0'; + errno = errnum; + // TODO: Write control messages to the daemon log. + if ( !strcmp(action, "chdir") ) + warning("Failed to start %s: %s: %s: %m", + daemon->name, action, cd); + else if ( !strcmp(action, "open") ) + warning("Failed to start %s: %s: %s: %m", + daemon->name, action, "/dev/null"); + else if ( !strcmp(action, "execve") ) + warning("Failed to start %s: %s: %s: %m", + daemon->name, action, daemon->argv[0]); + else + warning("Failed to start %s: %s: %m", daemon->name, action); + } + close(errfds[0]); + // TODO: Not thread safe. + // TODO: Also unset other things. + if ( !daemon->need_tty ) + unsetenv("READYFD"); + unsetenv("INIT_PID"); + unsetenv("LOGNAME"); + unsetenv("USER"); + unsetenv("HOME"); + unsetenv("SHELL"); + if ( daemon->need_tty ) + daemon_on_ready(daemon); + else + daemon_change_state_list(daemon, DAEMON_STATE_STARTING); +} + +static bool daemon_process_ready(struct daemon* daemon) +{ + char c; + ssize_t amount = read(daemon->readyfd, &c, sizeof(c)); + if ( amount < 0 && (errno == EAGAIN || errno == EWOULDBLOCK) ) + return true; + if ( amount < 0 ) + return false; + else if ( amount == 0 ) + return false; + if ( c == '\n' ) + { + daemon_on_ready(daemon); + return false; + } + return true; +} + +static bool daemon_process_output(struct daemon* daemon) +{ + char data[4096]; + ssize_t amount = read(daemon->outputfd, data, sizeof(data)); + if ( amount < 0 && (errno == EAGAIN || errno == EWOULDBLOCK) ) + return true; + if ( amount < 0 ) + return false; + else if ( amount == 0 ) + return false; + log_formatted(&daemon->log, data, amount); + return true; +} + +static void daemon_on_exit(struct daemon* daemon, int exit_code) +{ + assert(daemon->state != DAEMON_STATE_FINISHING); + assert(daemon->state != DAEMON_STATE_FINISHED); + daemon->exit_code = exit_code; + if ( 0 <= daemon->readyfd ) + { + daemon_unregister_pollfd(daemon, daemon->pfd_readyfd_index); + close(daemon->readyfd); + daemon->readyfd = -1; + } + if ( 0 <= daemon->outputfd ) + { + daemon_process_output(daemon); + daemon_unregister_pollfd(daemon, daemon->pfd_outputfd_index); + close(daemon->outputfd); + daemon->outputfd = -1; + } + if ( 0 <= daemon->log.fd ) + log_close(&daemon->log); + if ( daemon->need_tty ) + { + // TODO: There is a race condition between getting the exit code from + // waitpid and us reclaiming it here where some other process that + // happened to get the right pid may own the tty. + sigset_t oldset, sigttou; + sigemptyset(&sigttou); + sigaddset(&sigttou, SIGTTOU); + sigprocmask(SIG_BLOCK, &sigttou, &oldset); + if ( tcsetattr(0, TCSAFLUSH, &daemon->oldtio) ) + fatal("tcsetattr: %m"); + if ( tcsetpgrp(0, getpgid(0)) < 0 ) + fatal("tcsetpgrp: %m"); + sigprocmask(SIG_SETMASK, &oldset, NULL); + } + daemon_on_finished(daemon); +} + +static void init(void) +{ + int default_daemon_exit_code = -1; + + while ( true ) + { + if ( caught_exit_signal != -1 && default_daemon_exit_code == -1) + { + struct daemon* default_daemon = daemon_find_by_name("default"); + if ( caught_exit_signal == 0 ) + log_status("stopped", "Powering off...\n"); + else if ( caught_exit_signal == 1 ) + log_status("stopped", "Rebooting...\n"); + else if ( caught_exit_signal == 2 ) + log_status("stopped", "Halting...\n"); + else + log_status("stopped", "Exiting %i...\n", caught_exit_signal); + if ( default_daemon->state != DAEMON_STATE_FINISHING && + default_daemon->state != DAEMON_STATE_FINISHED ) + daemon_change_state_list(default_daemon, + DAEMON_STATE_FINISHING); + default_daemon_exit_code = + WCONSTRUCT(WNATURE_EXITED, caught_exit_signal, 0); + } + caught_exit_signal = -1; + + while ( first_daemon_by_state[DAEMON_STATE_SCHEDULED] || + first_daemon_by_state[DAEMON_STATE_SATISFIED] || + first_daemon_by_state[DAEMON_STATE_FINISHING] ) + { + struct daemon* daemon; + while ( (daemon = first_daemon_by_state[DAEMON_STATE_SCHEDULED]) ) + daemon_wait(daemon); + while ( (daemon = first_daemon_by_state[DAEMON_STATE_SATISFIED]) ) + daemon_start(daemon); + while ( (daemon = first_daemon_by_state[DAEMON_STATE_FINISHING]) ) + daemon_finish(daemon); + } + + if ( !first_daemon_by_state[DAEMON_STATE_STARTING] && + !first_daemon_by_state[DAEMON_STATE_RUNNING] && + !first_daemon_by_state[DAEMON_STATE_TERMINATING] ) + break; + + struct timespec timeout = timespec_make(-1, 0); + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + + for ( struct daemon* daemon = + first_daemon_by_state[DAEMON_STATE_TERMINATING]; + daemon; + daemon = daemon->next_by_state ) + { + if ( !daemon->timeout_set ) + continue; + if ( timespec_le(daemon->timeout, now) ) + { + log_status("timeout", + "Stopping %s timed out, sending SIGKILL.\n", + daemon->name); + kill(daemon->pid, SIGKILL); + daemon->timeout_set = false; + } + else + { + struct timespec left = timespec_sub(daemon->timeout, now); + if ( timeout.tv_sec < 0 || timespec_lt(left, timeout) ) + timeout = left; + } + } + + // Block SIGCHLD so the signal will be delivered during poll(2). + sigset_t mask; + sigemptyset(&mask); + sigaddset(&mask, SIGCHLD); + sigset_t oldset; + sigprocmask(SIG_BLOCK, &mask, &oldset); + sigset_t unhandled_signals; + signotset(&unhandled_signals, &handled_signals); + sigset_t pollset; + sigandset(&pollset, &oldset, &unhandled_signals); + int exit_code; + pid_t pid; + while ( 0 < (pid = waitpid(-1, &exit_code, WNOHANG)) ) + { + struct daemon* daemon = daemon_find_by_pid(pid); + if ( daemon ) + daemon_on_exit(daemon, exit_code); + timeout = timespec_make(0, 0); + } + // Set a dummy SIGCHLD handler to ensure we get EINTR during ppoll(2). + struct sigaction sa = { 0 }; + sa.sa_handler = signal_handler; + sa.sa_flags = 0; + struct sigaction old_sa; + sigaction(SIGCHLD, &sa, &old_sa); + // Await either an event, a timeout, or SIGCHLD. + int nevents = ppoll(pfds, pfds_used, &timeout, &pollset); + sigaction(SIGCHLD, &old_sa, NULL); + sigprocmask(SIG_SETMASK, &oldset, NULL); + if ( nevents < 0 && errno != EINTR ) + fatal("ppoll: %m"); + for ( size_t i = 0; i < pfds_used; i++ ) + { + if ( nevents <= 0 ) + break; + struct pollfd* pfd = pfds + i; + if ( !pfd->revents ) + continue; + nevents--; + struct daemon* daemon = pfds_daemon[i]; + if ( 0 <= daemon->readyfd && pfd->fd == daemon->readyfd ) + { + if ( pfd->revents & (POLLIN | POLLHUP) ) + { + if ( !daemon_process_ready(daemon) ) + { + daemon_unregister_pollfd(daemon, + daemon->pfd_readyfd_index); + close(daemon->readyfd); + daemon->readyfd = -1; + i--; // Process this index again (something new there). + } + } + } + else if ( 0 <= daemon->outputfd && pfd->fd == daemon->outputfd ) + { + if ( pfd->revents & (POLLIN | POLLHUP) ) + { + if ( !daemon_process_output(daemon) ) + { + daemon_unregister_pollfd(daemon, + daemon->pfd_outputfd_index); + close(daemon->outputfd); + daemon->outputfd = -1; + i--; // Process this index again (something new there). + } + } + } + else + { + assert(false); + } + } + } + + // Collect child processes reparented to us that we don't know about and + // attempt to politely shut them down with SIGTERM and SIGKILL after a + // timeout. + sigset_t saved_mask, sigchld_mask; + sigemptyset(&sigchld_mask); + sigaddset(&sigchld_mask, SIGCHLD); + sigprocmask(SIG_BLOCK, &sigchld_mask, &saved_mask); + struct sigaction sa = { .sa_handler = signal_handler }; + struct sigaction old_sa; + sigaction(SIGCHLD, &sa, &old_sa); + struct timespec timeout = timespec_make(30, 0); + struct timespec begun; + clock_gettime(CLOCK_MONOTONIC, &begun); + bool sent_sigterm = false; + while ( true ) + { + int exit_code; + for ( pid_t pid = 1; 0 < pid; pid = waitpid(-1, &exit_code, WNOHANG) ); + + struct timespec now; + clock_gettime(CLOCK_MONOTONIC, &now); + struct timespec elapsed = timespec_sub(now, begun); + + struct psctl_stat psst; + psctl(getpid(), PSCTL_STAT, &psst); + bool any_unknown = false; + for ( pid_t pid = psst.ppid_first; pid != -1; pid = psst.ppid_next ) + { + if ( psctl(pid, PSCTL_STAT, &psst) < 0 ) + { + warning("psctl: %ji", (intmax_t) pid); + continue; + } + bool known = false; + for ( size_t i = 0; !known && i < mountpoints_used; i++ ) + if ( mountpoints[i].pid == pid ) + known = true; + if ( !known ) + { + any_unknown = true; + if ( !sent_sigterm ) + kill(pid, SIGTERM); + // TODO: Hostile processes can try to escape by spawning more + // processes, a kernel feature is needed to recursively + // send a signal to all descendants atomically, although + // want to avoid known safe processes (mountpoints) and + // still catch processes reparented to us. Otherwise + // retrying until we succeed is the best we can do. + else if ( timespec_le(timeout, elapsed) ) + kill(pid, SIGKILL); + } + } + + sent_sigterm = true; + + if ( !any_unknown ) + break; + + // Wait for the timeout to happen, or for another process to exit by + // the poll failing with EINTR because a pending SIGCHLD was delivered + // when the saved signal mask is restored. + struct timespec left = timespec_sub(timeout, elapsed); + if ( left.tv_sec < 0 || (left.tv_sec == 0 && left.tv_nsec == 0) ) + left = timespec_make(1, 0); + struct pollfd pfd = { .fd = -1 }; + ppoll(&pfd, 1, &left, &saved_mask); + } + sigaction(SIGCHLD, &old_sa, NULL); + sigprocmask(SIG_SETMASK, &saved_mask, NULL); + + if ( default_daemon_exit_code != -1 ) + daemon_find_by_name("default")->exit_code = default_daemon_exit_code; } static void write_random_seed(void) @@ -428,7 +2773,7 @@ static void set_hostname(void) fclose(fp); if ( !hostname ) return warning("unable to set hostname: /etc/hostname: %m"); - int ret = sethostname(hostname, strlen(hostname) + 1); + int ret = sethostname(hostname, strlen(hostname)); if ( ret < 0 ) warning("unable to set hostname: `%s': %m", hostname); free(hostname); @@ -462,6 +2807,7 @@ static void set_kblayout(void) } if ( !child_pid ) { + uninstall_signal_handler(); execlp("chkblayout", "chkblayout", "--", kblayout, (const char*) NULL); warning("setting keyboard layout: chkblayout: %m"); _exit(127); @@ -669,30 +3015,80 @@ static void clean_tmp(const char* tmp_path) } } -static void init_early(void) +static bool fsck(struct filesystem* fs) { - static bool done = false; - if ( done ) - return; - done = true; - - // Make sure that we have a /tmp directory. - umask(0000); - mkdir("/tmp", 01777); - clean_tmp("/tmp"); - - // Make sure that we have a /var/run directory. - umask(0000); - mkdir("/var", 0755); - mkdir("/var/run", 0755); - clean_tmp("/var/run"); - - // Set the default file creation mask. - umask(0022); - - // Set up the PATH variable. - if ( setenv("PATH", "/bin:/sbin", 1) < 0 ) - fatal("setenv: %m"); + struct blockdevice* bdev = fs->bdev; + const char* bdev_path = bdev->p ? bdev->p->path : bdev->hd->path; + assert(bdev_path); + assert(fs->fsck); + if ( fs->flags & FILESYSTEM_FLAG_FSCK_MUST ) + note("%s: Repairing filesystem due to inconsistency...", bdev_path); + else + note("%s: Checking filesystem consistency...", bdev_path); + pid_t pid = fork(); + if ( pid < 0 ) + { + if ( fs->flags & FILESYSTEM_FLAG_FSCK_MUST ) + warning("%s: Mandatory repair failed: fork: %m", bdev_path); + else + warning("%s: Skipping filesystem check: fork: %m:", bdev_path); + return false; + } + if ( pid == 0 ) + { + uninstall_signal_handler(); + execlp(fs->fsck, fs->fsck, "-fp", "--", bdev_path, (const char*) NULL); + warning("%s: Failed to load filesystem checker: %s: %m", + bdev_path, fs->fsck); + _exit(127); + } + int code; + if ( waitpid(pid, &code, 0) < 0 ) + warning("%s: Filesystem check: waitpid: %m", bdev_path); + else if ( WIFEXITED(code) && + (WEXITSTATUS(code) == 0 || WEXITSTATUS(code) == 1) ) + { + // Successfully checked filesystem. + fs->flags &= ~(FILESYSTEM_FLAG_FSCK_SHOULD | FILESYSTEM_FLAG_FSCK_MUST); + return true; + } + else if ( fs->flags & FILESYSTEM_FLAG_FSCK_MUST ) + { + if ( WIFSIGNALED(code) ) + warning("%s: Mandatory repair failed: %s: %s", bdev_path, fs->fsck, + strsignal(WTERMSIG(code))); + else if ( !WIFEXITED(code) ) + warning("%s: Mandatory repair failed: %s: %s", bdev_path, fs->fsck, + "Unexpected unusual termination"); + else if ( WEXITSTATUS(code) == 127 ) + warning("%s: Mandatory repair failed: %s: %s", bdev_path, fs->fsck, + "Filesystem checker is absent"); + else if ( WEXITSTATUS(code) & 2 ) + warning("%s: Mandatory repair: %s: %s", bdev_path, fs->fsck, + "System reboot is necessary"); + else + warning("%s: Mandatory repair failed: %s: %s", bdev_path, fs->fsck, + "Filesystem checker was unsuccessful"); + } + else + { + if ( WIFSIGNALED(code) ) + warning("%s: Filesystem check failed: %s: %s", bdev_path, fs->fsck, + strsignal(WTERMSIG(code))); + else if ( !WIFEXITED(code) ) + warning("%s: Filesystem check failed: %s: %s", bdev_path, fs->fsck, + "Unexpected unusual termination"); + else if ( WEXITSTATUS(code) == 127 ) + warning("%s: Skipping filesystem check: %s: %s", bdev_path, + fs->fsck, "Filesystem checker is absent"); + else if ( WEXITSTATUS(code) & 2 ) + warning("%s: Filesystem check: %s: %s", bdev_path, fs->fsck, + "System reboot is necessary"); + else + warning("%s: Filesystem check failed: %s: %s", bdev_path, fs->fsck, + "Filesystem checker was unsuccessful"); + } + return false; } static bool is_chain_init_mountpoint(const struct mountpoint* mountpoint) @@ -715,22 +3111,15 @@ static struct filesystem* mountpoint_lookup(const struct mountpoint* mountpoint) struct device_match match; memset(&match, 0, sizeof(match)); search_by_uuid(uuid, ensure_single_device_match, &match); - if ( !match.path ) + if ( !match.path || !match.bdev ) { warning("%s: No devices matching uuid %s were found", path, uuid); return NULL; } - if ( !match.bdev ) - { - warning("%s: Don't know which particular device to boot with uuid " - "%s", path, uuid); - return NULL; - } assert(match.bdev->fs); return match.bdev->fs; } // TODO: Lookup by device name. - // TODO: Use this function in the chain init case too. warning("%s: Don't know how to resolve `%s' to a filesystem", path, spec); return NULL; } @@ -745,155 +3134,118 @@ static bool mountpoint_mount(struct mountpoint* mountpoint) struct blockdevice* bdev = fs->bdev; const char* bdev_path = bdev->p ? bdev->p->path : bdev->hd->path; assert(bdev_path); - do if ( fs->flags & (FILESYSTEM_FLAG_FSCK_SHOULD | FILESYSTEM_FLAG_FSCK_MUST) ) - { - assert(fs->fsck); - if ( fs->flags & FILESYSTEM_FLAG_FSCK_MUST ) - note("%s: Repairing filesystem due to inconsistency...", bdev_path); - else - note("%s: Checking filesystem consistency...", bdev_path); - pid_t child_pid = fork(); - if ( child_pid < 0 ) - { - if ( fs->flags & FILESYSTEM_FLAG_FSCK_MUST ) - { - warning("%s: Mandatory repair failed: fork: %m", bdev_path); - return false; - } - warning("%s: Skipping filesystem check: fork: %m:", bdev_path); - break; - } - if ( child_pid == 0 ) - { - execlp(fs->fsck, fs->fsck, "-fp", "--", bdev_path, (const char*) NULL); - note("%s: Failed to load filesystem checker: %s: %m", bdev_path, fs->fsck); - _exit(127); - } - int code; - if ( waitpid(child_pid, &code, 0) < 0 ) - fatal("waitpid: %m"); - if ( WIFEXITED(code) && - (WEXITSTATUS(code) == 0 || WEXITSTATUS(code) == 1) ) - { - // Successfully checked filesystem. - } - else if ( fs->flags & FILESYSTEM_FLAG_FSCK_MUST ) - { - if ( WIFSIGNALED(code) ) - warning("%s: Mandatory repair failed: %s: %s", bdev_path, - fs->fsck, strsignal(WTERMSIG(code))); - else if ( !WIFEXITED(code) ) - warning("%s: Mandatory repair failed: %s: %s", bdev_path, - fs->fsck, "Unexpected unusual termination"); - else if ( WEXITSTATUS(code) == 127 ) - warning("%s: Mandatory repair failed: %s: %s", bdev_path, - fs->fsck, "Filesystem checker is absent"); - else if ( WEXITSTATUS(code) & 2 ) - warning("%s: Mandatory repair: %s: %s", bdev_path, - fs->fsck, "System reboot is necessary"); - else - warning("%s: Mandatory repair failed: %s: %s", bdev_path, - fs->fsck, "Filesystem checker was unsuccessful"); - return false; - } - else - { - bool ignore = false; - if ( WIFSIGNALED(code) ) - warning("%s: Filesystem check failed: %s: %s", bdev_path, - fs->fsck, strsignal(WTERMSIG(code))); - else if ( !WIFEXITED(code) ) - warning("%s: Filesystem check failed: %s: %s", bdev_path, - fs->fsck, "Unexpected unusual termination"); - else if ( WEXITSTATUS(code) == 127 ) - { - warning("%s: Skipping filesystem check: %s: %s", bdev_path, - fs->fsck, "Filesystem checker is absent"); - ignore = true; - } - else if ( WEXITSTATUS(code) & 2 ) - warning("%s: Filesystem check: %s: %s", bdev_path, - fs->fsck, "System reboot is necessary"); - else - warning("%s: Filesystem check failed: %s: %s", bdev_path, - fs->fsck, "Filesystem checker was unsuccessful"); - if ( !ignore ) - return false; - } - } while ( 0 ); - if ( !fs->driver ) - { - warning("%s: Don't know how to mount a %s filesystem", - bdev_path, fs->fstype_name); - return false; - } const char* pretend_where = mountpoint->entry.fs_file; const char* where = mountpoint->absolute; + const char* read_only = NULL; + if ( fs->flags & (FILESYSTEM_FLAG_FSCK_SHOULD | FILESYSTEM_FLAG_FSCK_MUST) ) + { + if ( !fsck(fs) && (fs->flags & FILESYSTEM_FLAG_FSCK_MUST) ) + { + warning("Mounting inconsistent filesystem %s read-only on %s", + bdev_path, pretend_where); + read_only = "-r"; + } + } + if ( !fs->driver ) + { + warning("Failed mounting %s on %s: " + "Don't know how to mount a %s filesystem", + bdev_path, pretend_where, fs->fstype_name); + return false; + } struct stat st; if ( stat(where, &st) < 0 ) { - warning("stat: %s: %m", where); + warning("Failed mounting %s on %s: stat: %s: %m", + bdev_path, pretend_where, where); + return false; + } + int readyfds[2]; + if ( pipe(readyfds) < 0 ) + { + warning("Failed mounting %s on %s: pipe: %m", bdev_path, pretend_where); return false; } if ( (mountpoint->pid = fork()) < 0 ) { - warning("%s: Unable to mount: fork: %m", bdev_path); + warning("Failed mounting %s on %s: fork: %m", bdev_path, pretend_where); + close(readyfds[0]); + close(readyfds[1]); return false; } - // TODO: This design is broken. The filesystem should tell us when it is - // ready instead of having to poll like this. if ( mountpoint->pid == 0 ) { + uninstall_signal_handler(); + close(readyfds[0]); + char readyfdstr[sizeof(int) * 3]; + snprintf(readyfdstr, sizeof(readyfdstr), "%d", readyfds[1]); + if ( setenv("READYFD", readyfdstr, 1) < 0 ) + { + warning("Failed mounting %s on %s: setenv: %m", + bdev_path, pretend_where); + _exit(127); + } execlp(fs->driver, fs->driver, "--foreground", bdev_path, where, - "--pretend-mount-path", pretend_where, (const char*) NULL); - warning("%s: Failed to load filesystem driver: %s: %m", bdev_path, fs->driver); + "--pretend-mount-path", pretend_where, read_only, + (const char*) NULL); + warning("Failed mount %s on %s: execvp: %s: %m", + bdev_path, pretend_where, fs->driver); _exit(127); } - while ( true ) + close(readyfds[1]); + char c; + struct stat newst; + ssize_t amount = read(readyfds[0], &c, 1); + close(readyfds[0]); + if ( 0 <= amount ) { - struct stat newst; - if ( stat(where, &newst) < 0 ) + if ( !stat(where, &newst) ) { - warning("stat: %s: %m", where); - if ( unmount(where, 0) < 0 && errno != ENOMOUNT ) - warning("unmount: %s: %m", where); - else if ( errno == ENOMOUNT ) - kill(mountpoint->pid, SIGQUIT); - int code; - waitpid(mountpoint->pid, &code, 0); - mountpoint->pid = -1; - return false; - } - if ( newst.st_dev != st.st_dev || newst.st_ino != st.st_ino ) - break; - int code; - pid_t child = waitpid(mountpoint->pid, &code, WNOHANG); - if ( child < 0 ) - fatal("waitpid: %m"); - if ( child != 0 ) - { - mountpoint->pid = -1; - if ( WIFSIGNALED(code) ) - warning("%s: Mount failed: %s: %s", bdev_path, fs->driver, - strsignal(WTERMSIG(code))); - else if ( !WIFEXITED(code) ) - warning("%s: Mount failed: %s: %s", bdev_path, fs->driver, - "Unexpected unusual termination"); - else if ( WEXITSTATUS(code) == 127 ) - warning("%s: Mount failed: %s: %s", bdev_path, fs->driver, - "Filesystem driver is absent"); - else if ( WEXITSTATUS(code) == 0 ) - warning("%s: Mount failed: %s: Unexpected successful exit", - bdev_path, fs->driver); + if ( newst.st_dev != st.st_dev || newst.st_ino != st.st_ino ) + return true; else - warning("%s: Mount failed: %s: Exited with status %i", bdev_path, - fs->driver, WEXITSTATUS(code)); - return false; + warning("Failed mount %s on %s: %s: " + "No mounted filesystem appeared: %s", + bdev_path, pretend_where, fs->driver, where); } - struct timespec delay = timespec_make(0, 50L * 1000L * 1000L); - nanosleep(&delay, NULL); + else + warning("Failed mounting %s on %s: %s, stat: %s: %m", + bdev_path, pretend_where, fs->driver, where); } - return true; + else + warning("Failed mounting %s on %s: %s, Failed to read readiness: %m", + bdev_path, pretend_where, fs->driver); + if ( unmount(where, 0) < 0 ) + { + if ( errno != ENOMOUNT ) + warning("Failed mounting %s on %s: unmount: %s: %m", + bdev_path, pretend_where, where); + kill(mountpoint->pid, SIGQUIT); + } + int code; + pid_t child = waitpid(mountpoint->pid, &code, 0); + mountpoint->pid = -1; + if ( child < 0 ) + warning("Failed mounting %s on %s: %s: waitpid: %m", + bdev_path, pretend_where, fs->driver); + else if ( WIFSIGNALED(code) ) + warning("Failed mounting %s on %s: %s: %s", + bdev_path, pretend_where, fs->driver, + strsignal(WTERMSIG(code))); + else if ( !WIFEXITED(code) ) + warning("Failed mounting %s on %s: %s: Unexpected unusual termination", + bdev_path, pretend_where, fs->driver); + else if ( WEXITSTATUS(code) == 127 ) + warning("Failed mounting %s on %s: %s: " + "Filesystem driver could not be executed", + bdev_path, pretend_where, fs->driver); + else if ( WEXITSTATUS(code) == 0 ) + warning("Failed mounting %s on %s: %s: Unexpected successful exit", + bdev_path, pretend_where, fs->driver); + else + warning("Failed mounting %s on %s: %s: Exited with status %i", + bdev_path, pretend_where, fs->driver, WEXITSTATUS(code)); + return false; } static void mountpoints_mount(bool is_chain_init) @@ -918,7 +3270,7 @@ static void mountpoints_unmount(void) if ( unmount(mountpoint->absolute, 0) < 0 && errno != ENOMOUNT ) warning("unmount: %s: %m", mountpoint->entry.fs_file); else if ( errno == ENOMOUNT ) - kill(mountpoint->pid, SIGQUIT); + kill(mountpoint->pid, SIGTERM); int code; if ( waitpid(mountpoint->pid, &code, 0) < 0 ) note("waitpid: %m"); @@ -934,8 +3286,17 @@ static void niht(void) if ( getpid() != main_pid ) return; + // TODO: Unify with new daemon system? At least it needs to recursively kill + // all processes. Ideally fatal wouldn't be called for daemons. + + // TODO: Don't do this unless all the mountpoints were mounted (not for + // chain init). write_random_seed(); + // Stop logging when unmounting the filesystems. + cbprintf(&init_log, log_callback, "Finished operating system.\n"); + log_close(&init_log); + if ( chain_location_dev_made ) { unmount(chain_location_dev, 0); @@ -951,32 +3312,291 @@ static void niht(void) } } -static int init(int argc, char** argv, const char* target) +int main(int argc, char* argv[]) { - if ( 1 < argc ) - fatal("unexpected extra operand: %s", argv[1]); - init_early(); + main_pid = getpid(); + + setlocale(LC_ALL, ""); + + const char* target_name = "default"; + + const struct option longopts[] = + { + {"target", required_argument, NULL, 't'}, + {"quiet", no_argument, NULL, 'q'}, + {"silent", no_argument, NULL, 's'}, + {"verbose", no_argument, NULL, 'v'}, + {0, 0, 0, 0} + }; + const char* opts = "t:qsv"; + int opt; + while ( (opt = getopt_long(argc, argv, opts, longopts, NULL)) != -1 ) + { + switch ( opt ) + { + case 't': target_name = optarg; break; + case 'q': verbosity = VERBOSITY_QUIET; break; + case 's': verbosity = VERBOSITY_SILENT; break; + case 'v': verbosity = VERBOSITY_VERBOSE; break; + default: return 2; + } + } + + // Prevent recursive init without care. + if ( getenv("INIT_PID") ) + fatal("System is already managed by an init process"); + + // Register handler that shuts down the system when init exits. + if ( atexit(niht) != 0 ) + fatal("atexit: %m"); + + // Handle signals but block them until the safe points where we handle them. + // All child processes have to uninstall the signal handler and unblock the + // signals or they keep blocking the signals. + install_signal_handler(); + + // The default daemon brings up the operating system and supplies default + // values for the other daemon configuration files. + struct daemon_config* default_daemon_config = NULL; + if ( !access("/etc/init/default", F_OK) || errno != ENOENT ) + { + if ( !(default_daemon_config = daemon_config_load("default")) ) + fatal("Failed to load default daemon configuration"); + } + + // The default daemon must be configured if no target is specified. + if ( !default_daemon_config && !strcmp(target_name, "default") ) + fatal("No default daemon configuration was found"); + + // Daemons inherit their default settings from the default daemon. Load its + // configuration (if it exists) even if another default target has been set. + if ( default_daemon_config ) + { + default_config.log_method = default_daemon_config->log_method; + default_config.log_format = default_daemon_config->log_format; + default_config.log_control_messages = + default_daemon_config->log_control_messages; + default_config.log_rotate_on_start = + default_daemon_config->log_rotate_on_start; + default_config.log_rotations = default_daemon_config->log_rotations; + default_config.log_line_size = default_daemon_config->log_line_size; + default_config.log_size = default_daemon_config->log_size; + } + + // If another daemon has been specified as the boot target, create a fake + // default daemon that depends on the specified boot target daemon. + if ( strcmp(target_name, "default") != 0 ) + { + if ( default_daemon_config ) + daemon_config_free(default_daemon_config); + default_daemon_config = malloc(sizeof(struct daemon_config)); + if ( !default_daemon_config ) + fatal("malloc: %m"); + daemon_config_initialize(default_daemon_config); + if ( !(default_daemon_config->name = strdup("default")) ) + fatal("malloc: %m"); + struct dependency_config* dependency_config = + calloc(1, sizeof(struct dependency_config)); + if ( !dependency_config ) + fatal("malloc: %m"); + if ( !(dependency_config->target = strdup(target_name)) ) + fatal("malloc: %m"); + dependency_config->flags = DEPENDENCY_FLAG_REQUIRE | + DEPENDENCY_FLAG_AWAIT | + DEPENDENCY_FLAG_EXIT_CODE; + if ( !array_add((void***) &default_daemon_config->dependencies, + &default_daemon_config->dependencies_used, + &default_daemon_config->dependencies_length, + dependency_config) ) + fatal("malloc: %m"); + } + else if ( !default_daemon_config ) + fatal("Failed to load /etc/init/default: %m"); + + // Instantiate the default daemon from its configuration. + struct daemon* default_daemon = daemon_create(default_daemon_config); + daemon_config_free(default_daemon_config); + + // The default daemon should depend on exactly one top level daemon. + const char* first_requirement = + 1 <= default_daemon->dependencies_used ? + default_daemon->dependencies[0]->target->name : + ""; + + // Log to memory until the log directory has been mounted. + if ( !log_initialize(&init_log, "init", &default_config) ) + fatal("malloc: %m"); + if ( !log_begin_buffer(&init_log) ) + fatal("malloc: %m"); + init_log.pid = getpid(); + cbprintf(&init_log, log_callback, "Initializing operating system...\n"); + + // Make sure that we have a /tmp directory. + umask(0000); + mkdir("/tmp", 01777); + clean_tmp("/tmp"); + + // Make sure that we have a /var/run directory. + umask(0000); + mkdir("/var", 0755); + mkdir("/var/run", 0755); + clean_tmp("/var/run"); + + // Set the default file creation mask. + umask(0022); + + // Set up the PATH variable. + if ( setenv("PATH", "/bin:/sbin", 1) < 0 ) + fatal("setenv: %m"); + + // Load partition tables and create all the block devices. + prepare_block_devices(); + + // Load the filesystem table. + load_fstab(); + + // If the default daemon's top level dependency is a chain boot target, then + // chain boot the actual root filesystem. + if ( !strcmp(first_requirement, "chain") || + !strcmp(first_requirement, "chain-merge") ) + { + int next_argc = argc - optind; + char** next_argv = argv + optind; + // Create a temporary directory where the real root filesystem will be + // mounted. + if ( !mkdtemp(chain_location) ) + fatal("mkdtemp: /tmp/fs.XXXXXX: %m"); + chain_location_made = true; + // Rewrite the filesystem table to mount inside the temporary directory. + bool found_root = false; + for ( size_t i = 0; i < mountpoints_used; i++ ) + { + struct mountpoint* mountpoint = &mountpoints[i]; + if ( !strcmp(mountpoint->entry.fs_file, "/") ) + found_root = true; + char* absolute = join_paths(chain_location, mountpoint->absolute); + if ( !absolute ) + fatal("malloc: %m"); + free(mountpoint->absolute); + mountpoint->absolute = absolute; + } + if ( !found_root ) + fatal("/etc/fstab: Root filesystem not found in filesystem table"); + // Mount the filesystem table entries marked for chain boot. + mountpoints_mount(true); + // Additionally bind the /dev filesystem inside the root filesystem. + snprintf(chain_location_dev, sizeof(chain_location_dev), "%s/dev", + chain_location); + if ( mkdir(chain_location_dev, 0755) < 0 && + errno != EEXIST && errno != EROFS ) + fatal("mkdir: %s: %m", chain_location_dev); + int old_dev_fd = open("/dev", O_DIRECTORY | O_RDONLY); + if ( old_dev_fd < 0 ) + fatal("%s: %m", "/dev"); + int new_dev_fd = open(chain_location_dev, O_DIRECTORY | O_RDONLY); + if ( new_dev_fd < 0 ) + fatal("%s: %m", chain_location_dev); + if ( fsm_fsbind(old_dev_fd, new_dev_fd, 0) < 0 ) + fatal("mount: `%s' onto `%s': %m", "/dev", chain_location_dev); + chain_location_dev_made = true; + close(new_dev_fd); + close(old_dev_fd); + // TODO: Forward the early init log to the chain init. + // Run the chain booted operating system. + pid_t child_pid = fork(); + if ( child_pid < 0 ) + fatal("fork: %m"); + if ( !child_pid ) + { + uninstall_signal_handler(); + if ( chroot(chain_location) < 0 ) + fatal("chroot: %s: %m", chain_location); + if ( chdir("/") < 0 ) + fatal("chdir: %s: %m", chain_location); + const char* program = next_argv[0]; + char verbose_opt[] = {'-', "sqv"[verbosity], '\0'}; + // Chain boot the operating system upgrade if needed. + if ( !strcmp(first_requirement, "chain-merge") ) + { + program = "/sysmerge/sbin/init"; + // TODO: Concat next_argv onto this argv_next, so the arguments + // can be passed to the final init. + next_argv = + (char*[]) { (char*) program, "--target=merge", + verbose_opt, NULL }; + } + else if ( next_argc < 1 ) + { + program = "/sbin/init"; + next_argv = (char*[]) { "init", verbose_opt, NULL }; + } + execvp(program, (char* const*) next_argv); + fatal("Failed to chain load init: %s: %m", next_argv[0]); + } + forward_signal_pid = child_pid; + sigprocmask(SIG_UNBLOCK, &handled_signals, NULL); + int status; + while ( waitpid(child_pid, &status, 0) < 0 ) + { + if ( errno != EINTR ) + fatal("waitpid: %m"); + } + sigprocmask(SIG_BLOCK, &handled_signals, NULL); + forward_signal_pid = -1; // Racy with waitpid. + if ( WIFEXITED(status) ) + return WEXITSTATUS(status); + else if ( WIFSIGNALED(status) ) + fatal("Chain booted init failed with signal: %s", + strsignal(WTERMSIG(status))); + else + fatal("Chain booted init failed unusually"); + } + + // Mount the filesystems, except for the filesystems that would have been + // mounted by the chain init. + mountpoints_mount(false); + + // TODO: After releasing Sortix 1.1, remove this compatibility since a + // sysmerge from 1.0 will not have a /var/log directory. + if ( !strcmp(first_requirement, "merge") && + access("/var/log", F_OK) < 0 ) + mkdir("/var/log", 0755); + + // Logging works now that the filesystems have been mounted. Reopen the init + // log and write the contents buffered up in memory. + log_begin(&init_log); + + // Update the random seed in case the system fails before it can be written + // out during the system shutdown. + write_random_seed(); + set_hostname(); set_kblayout(); set_videomode(); - prepare_block_devices(); - load_fstab(); - mountpoints_mount(false); - write_random_seed(); - if ( !strcmp(target, "merge") ) + + // Run the operating system upgrade if requested. + if ( !strcmp(first_requirement, "merge") ) { pid_t child_pid = fork(); if ( child_pid < 0 ) fatal("fork: %m"); if ( !child_pid ) { + uninstall_signal_handler(); const char* argv[] = { "sysmerge", "--booting", NULL }; execv("/sysmerge/sbin/sysmerge", (char* const*) argv); - fatal("Failed to run automatic update: %s: %m", argv[0]); + fatal("Failed to load system upgrade: %s: %m", argv[0]); } + forward_signal_pid = child_pid; + sigprocmask(SIG_UNBLOCK, &handled_signals, NULL); int status; - if ( waitpid(child_pid, &status, 0) < 0 ) - fatal("waitpid"); + while ( waitpid(child_pid, &status, 0) < 0 ) + { + if ( errno != EINTR ) + fatal("waitpid: %m"); + } + sigprocmask(SIG_BLOCK, &handled_signals, NULL); + forward_signal_pid = -1; // Racy with waitpid. if ( WIFEXITED(status) && WEXITSTATUS(status) != 0 ) fatal("Automatic upgrade failed: Exit status %i", WEXITSTATUS(status)); @@ -984,281 +3604,25 @@ static int init(int argc, char** argv, const char* target) fatal("Automatic upgrade failed: %s", strsignal(WTERMSIG(status))); else if ( !WIFEXITED(status) ) fatal("Automatic upgrade failed: Unexpected unusual termination"); + // Soft reinit into the freshly upgraded operating system. niht(); - unsetenv("INIT_PID"); + // TODO: Use next_argv here. const char* argv[] = { "init", NULL }; execv("/sbin/init", (char* const*) argv); - fatal("Failed to load chain init: %s: %m", argv[0]); + fatal("Failed to load init during reinit: %s: %m", argv[0]); } - sigset_t oldset, sigttou; - sigemptyset(&sigttou); - sigaddset(&sigttou, SIGTTOU); - int result; - while ( true ) - { - struct termios tio; - if ( tcgetattr(0, &tio) ) - fatal("tcgetattr: %m"); - pid_t child_pid = fork(); - if ( child_pid < 0 ) - fatal("fork: %m"); - if ( !child_pid ) - { - uid_t uid = getuid(); - pid_t pid = getpid(); - pid_t ppid = getppid(); - if ( setpgid(0, 0) < 0 ) - fatal("setpgid: %m"); - sigprocmask(SIG_BLOCK, &sigttou, &oldset); - if ( tcsetpgrp(0, pid) < 0 ) - fatal("tcsetpgrp: %m"); - sigprocmask(SIG_SETMASK, &oldset, NULL); - struct passwd* pwd = getpwuid(uid); - if ( !pwd ) - fatal("looking up user by uid %" PRIuUID ": %m", uid); - const char* home = pwd->pw_dir[0] ? pwd->pw_dir : "/"; - const char* shell = pwd->pw_shell[0] ? pwd->pw_shell : "sh"; - char ppid_str[sizeof(pid_t) * 3]; - snprintf(ppid_str, sizeof(ppid_str), "%" PRIiPID, ppid); - if ( setenv("INIT_PID", ppid_str, 1) < 0 || - setenv("LOGNAME", pwd->pw_name, 1) < 0 || - setenv("USER", pwd->pw_name, 1) < 0 || - setenv("HOME", home, 1) < 0 || - setenv("SHELL", shell, 1) < 0 ) - fatal("setenv: %m"); - if ( chdir(home) < 0 ) - warning("chdir: %s: %m", home); - const char* program = "login"; - bool activate_terminal = false; - if ( !strcmp(target, "single-user") ) - { - activate_terminal = true; - program = shell; - } - if ( !strcmp(target, "sysinstall") ) - { - activate_terminal = true; - program = "sysinstall"; - } - if ( !strcmp(target, "sysupgrade") ) - { - program = "sysupgrade"; - activate_terminal = true; - } - if ( activate_terminal ) - { - tio.c_cflag |= CREAD; - if ( tcsetattr(0, TCSANOW, &tio) ) - fatal("tcgetattr: %m"); - } - const char* argv[] = { program, NULL }; - execvp(program, (char* const*) argv); - fatal("%s: %m", program); - } - int status; - if ( waitpid(child_pid, &status, 0) < 0 ) - fatal("waitpid"); - sigprocmask(SIG_BLOCK, &sigttou, &oldset); - if ( tcsetattr(0, TCSAFLUSH, &tio) ) - fatal("tcgetattr: %m"); - if ( tcsetpgrp(0, getpgid(0)) < 0 ) - fatal("tcsetpgrp: %m"); - sigprocmask(SIG_SETMASK, &oldset, NULL); - const char* back = ": Trying to bring it back up again"; - if ( WIFEXITED(status) ) - { - result = WEXITSTATUS(status); - break; - } - else if ( WIFSIGNALED(status) ) - note("session: %s%s", strsignal(WTERMSIG(status)), back); - else - note("session: Unexpected unusual termination%s", back); - } - return result; -} - -static int init_chain(int argc, char** argv, const char* target) -{ - int next_argc = argc - 1; - char** next_argv = argv + 1; - init_early(); - prepare_block_devices(); - load_fstab(); - if ( !mkdtemp(chain_location) ) - fatal("mkdtemp: /tmp/fs.XXXXXX: %m"); - chain_location_made = true; - bool found_root = false; - for ( size_t i = 0; i < mountpoints_used; i++ ) - { - struct mountpoint* mountpoint = &mountpoints[i]; - if ( !strcmp(mountpoint->entry.fs_file, "/") ) - found_root = true; - char* absolute = join_paths(chain_location, mountpoint->absolute); - free(mountpoint->absolute); - mountpoint->absolute = absolute; - } - if ( !found_root ) - fatal("/etc/fstab: Root filesystem not found in filesystem table"); - mountpoints_mount(true); - snprintf(chain_location_dev, sizeof(chain_location_dev), "%s/dev", - chain_location); - if ( mkdir(chain_location_dev, 0755) < 0 && errno != EEXIST ) - fatal("mkdir: %s: %m", chain_location_dev); - int old_dev_fd = open("/dev", O_DIRECTORY | O_RDONLY); - if ( old_dev_fd < 0 ) - fatal("%s: %m", "/dev"); - int new_dev_fd = open(chain_location_dev, O_DIRECTORY | O_RDONLY); - if ( new_dev_fd < 0 ) - fatal("%s: %m", chain_location_dev); - if ( fsm_fsbind(old_dev_fd, new_dev_fd, 0) < 0 ) - fatal("mount: `%s' onto `%s': %m", "/dev", chain_location_dev); - close(new_dev_fd); - close(old_dev_fd); - int result; - while ( true ) - { - pid_t child_pid = fork(); - if ( child_pid < 0 ) - fatal("fork: %m"); - if ( !child_pid ) - { - if ( chroot(chain_location) < 0 ) - fatal("chroot: %s: %m", chain_location); - if ( chdir("/") < 0 ) - fatal("chdir: %s: %m", chain_location); - unsetenv("INIT_PID"); - const char* program = next_argv[0]; - if ( !strcmp(target, "chain-merge") ) - { - if ( next_argc < 1 ) - { - program = "/sysmerge/sbin/init"; - next_argv = (char*[]) { "init", "--target=merge", NULL }; - } - execvp(program, (char* const*) next_argv); - fatal("Failed to load automatic update chain init: %s: %m", - next_argv[0]); - } - else - { - if ( next_argc < 1 ) - { - program = "/sbin/init"; - next_argv = (char*[]) { "init", NULL }; - } - execvp(program, (char* const*) next_argv); - fatal("Failed to load chain init: %s: %m", next_argv[0]); - } - } - int status; - if ( waitpid(child_pid, &status, 0) < 0 ) - fatal("waitpid"); - // Only run an automatic update once. - if ( !strcmp(target, "chain-merge") ) - target = "chain"; - const char* back = ": Trying to bring it back up again"; - if ( WIFEXITED(status) ) - { - result = WEXITSTATUS(status); - break; - } - else if ( WIFSIGNALED(status) ) - note("chain init: %s%s", strsignal(WTERMSIG(status)), back); - else - note("chain init: Unexpected unusual termination%s", back); - } - return result; -} - -static void compact_arguments(int* argc, char*** argv) -{ - for ( int i = 0; i < *argc; i++ ) - { - while ( i < *argc && !(*argv)[i] ) - { - for ( int n = i; n < *argc; n++ ) - (*argv)[n] = (*argv)[n+1]; - (*argc)--; - } - } -} - -int main(int argc, char* argv[]) -{ - main_pid = getpid(); - - setlocale(LC_ALL, ""); - - const char* target = NULL; - - for ( int i = 1; i < argc; i++ ) - { - const char* arg = argv[i]; - if ( arg[0] != '-' || !arg[1] ) - continue; - argv[i] = NULL; - if ( !strcmp(arg, "--") ) - break; - if ( arg[1] != '-' ) - { - char c; - while ( (c = *++arg) ) switch ( c ) - { - default: - errx(2, "unknown option -- '%c'", c); - } - } - else if ( !strncmp(arg, "--target=", strlen("--target=")) ) - target = arg + strlen("--target="); - else if ( !strcmp(arg, "--target") ) - { - if ( i + 1 == argc ) - errx(2, "option '--target' requires an argument"); - target = argv[i+1]; - argv[++i] = NULL; - } - else - errx(2, "unknown option: %s", arg); - } - - compact_arguments(&argc, &argv); - - char* target_string = NULL; - if ( !target ) - { - const char* target_path = "/etc/init/target"; - if ( access(target_path, F_OK) == 0 ) - { - FILE* target_fp = fopen(target_path, "r"); - if ( !target_fp ) - fatal("%s: %m", target_path); - target_string = read_single_line(target_fp); - if ( !target_string ) - fatal("read: %s: %m", target_path); - target = target_string; - fclose(target_fp); - } - else - fatal("Refusing to initialize because %s doesn't exist", target_path); - } - - if ( getenv("INIT_PID") ) - fatal("System is already managed by an init process"); - - if ( atexit(niht) != 0 ) - fatal("atexit: %m"); - - if ( !strcmp(target, "single-user") || - !strcmp(target, "multi-user") || - !strcmp(target, "sysinstall") || - !strcmp(target, "sysupgrade") || - !strcmp(target, "merge") ) - return init(argc, argv, target); - - if ( !strcmp(target, "chain") || - !strcmp(target, "chain-merge") ) - return init_chain(argc, argv, target); - - fatal("Unknown initialization target `%s'", target); + + // 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); + + // Initialize the operating system. + init(); + + // Finish with the exit code of the default daemon. + return exit_code_to_exit_status(default_daemon->exit_code); } diff --git a/libmount/util.h b/libmount/util.h index 0b3a4ee4..e7e054a8 100644 --- a/libmount/util.h +++ b/libmount/util.h @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015 Jonas 'Sortie' Termansen. + * Copyright (c) 2015, 2022 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 @@ -47,18 +47,15 @@ static bool array_add(void*** array_ptr, if ( *used_ptr == *length_ptr ) { - // TODO: Avoid overflow. - size_t new_length = 2 * *length_ptr; - if ( !new_length ) - new_length = 16; - // TODO: Avoid overflow and use reallocarray. - size_t new_size = new_length * sizeof(void*); - void** new_array = (void**) realloc(array, new_size); + 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 = new_length; + *length_ptr = length * 2; } memcpy(array + (*used_ptr)++, &value, sizeof(value)); // Strict aliasing. diff --git a/sh/util.c b/sh/util.c index 20262078..fe954b47 100644 --- a/sh/util.c +++ b/sh/util.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011, 2012, 2013, 2014, 2015 Jonas 'Sortie' Termansen. + * Copyright (c) 2011, 2012, 2013, 2014, 2015, 2022 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 @@ -49,18 +49,15 @@ bool array_add(void*** array_ptr, if ( *used_ptr == *length_ptr ) { - // TODO: Avoid overflow. - size_t new_length = 2 * *length_ptr; - if ( !new_length ) - new_length = 16; - // TODO: Avoid overflow and use reallocarray. - size_t new_size = new_length * sizeof(void*); - void** new_array = (void**) realloc(array, new_size); + 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 = new_length; + *length_ptr = length * 2; } memcpy(array + (*used_ptr)++, &value, sizeof(value)); // Strict aliasing. diff --git a/share/init/base b/share/init/base new file mode 100644 index 00000000..1fbf5d63 --- /dev/null +++ b/share/init/base @@ -0,0 +1 @@ +require time optional diff --git a/share/init/local b/share/init/local new file mode 100644 index 00000000..e69de29b diff --git a/share/init/multi-user b/share/init/multi-user new file mode 100644 index 00000000..2f30b03a --- /dev/null +++ b/share/init/multi-user @@ -0,0 +1,8 @@ +require base no-await +require local no-await + +tty tty1 +need tty + +exit-code-meaning poweroff-reboot +exec login diff --git a/share/init/no-user b/share/init/no-user new file mode 100644 index 00000000..ad273433 --- /dev/null +++ b/share/init/no-user @@ -0,0 +1,2 @@ +require base no-await +require local no-await exit-code diff --git a/share/init/single-user b/share/init/single-user new file mode 100644 index 00000000..1c420b48 --- /dev/null +++ b/share/init/single-user @@ -0,0 +1,9 @@ +require base no-await +require local no-await + +tty tty1 +need tty + +cd "$HOME" +exit-code-meaning poweroff-reboot +exec "$SHELL" diff --git a/share/init/sysinstall b/share/init/sysinstall new file mode 100644 index 00000000..61dda839 --- /dev/null +++ b/share/init/sysinstall @@ -0,0 +1,8 @@ +require base no-await +require local no-await + +tty tty1 +need tty + +exec sysinstall +exit-code-meaning poweroff-reboot diff --git a/share/init/sysupgrade b/share/init/sysupgrade new file mode 100644 index 00000000..aa3962d1 --- /dev/null +++ b/share/init/sysupgrade @@ -0,0 +1,8 @@ +require base no-await +require local no-await + +tty tty1 +need tty + +exec sysupgrade +exit-code-meaning poweroff-reboot diff --git a/share/init/time b/share/init/time new file mode 100644 index 00000000..e69de29b diff --git a/share/man/man5/init.5 b/share/man/man5/init.5 new file mode 100644 index 00000000..f30991f0 --- /dev/null +++ b/share/man/man5/init.5 @@ -0,0 +1,587 @@ +.Dd July 29, 2018 +.Dt INIT 5 +.Os +.Sh NAME +.Nm init +.Nd system initialization configuration +.Sh SYNOPSIS +.Nm /etc/init/ +.Nm /share/init/ +.Sh DESCRIPTION +.Xr init 8 +starts each +.Xr daemon 7 +(system background process) according to the daemon's +configuration file, which specifies the daemon's dependencies and how to run the +daemon. +.Pp +Configuration files are searched for in the +.Pa /etc/init/ +directory (local initialization configuration owned by the system administrator, +which may be modified) and the +.Pa /share/init/ +directory (default initialization configuration owned by the operating system, +which should not be modified). +The file name of each configuration file is that of the daemon without any file +extension. +For instance, the daemon +.Sy exampled +might come with the default configuration file +.Pa /share/init/exampled +that the system administrator can override in +.Pa /etc/init/exampled . +.Pp +.Xr init 8 +initially starts the +.Sy default +daemon which is configured in +.Pa /etc/init/default , +which then depends on the daemons constituting the operating system (which in +turn depend on the +.Sy local +daemon). +The +.Pa /etc/init/default +file also defines default settings such as logging that are implicitly inherited +by all other deamons, as well as +.Xr init 8 Ns 's +own +.Pa /var/log/init.log +file. +.Pp +Local system daemons should be started by overriding the +.Sy local +daemon in +.Pa /etc/init/local , +which then depends on the locally required daemons. +System provided daemons can be customized by making +.Pa /etc/init/exampled +which starts with the +.Sy furthermore +statement to include the default +.Pa /etc/share/exampled +configuration and then change the desired properties. +.Sh DAEMONS +The +.Sy default +daemon should +.Sy require +exactly one top level daemon with +.Sy exit-code +and nothing else. +.Pp +The following daemons are top level daemons that start the operating system. +They are mutually exclusive and only a single one should be depended on: +.Bl -tag -width "12345678" +.It Sy multi-user +Starts the operating system in the multi-user mode. +It starts the +.Sy login +foreground daemon that provides a login screen and exits with login's exit code +when login exits. +This is a secure operating system mode where only authorized users have access. +It depends on the +.Sy base +and +.Sy local +daemons. +.It Sy no-user +Starts the operating system in the no-user mode. +This is a secure operating system mode where no user is granted access. +Additional daemons can be started by configuring the +.Sy local +daemon. +It depends on the +.Sy base +and +.Sy local +daemons. +The dependency on +.Sy local +is marked +.Sy exit-code , +letting the system administrator fully control the +.Sy default +daemon's exit code and when the system completes. +.It Sy single-user +Starts the operating system in the single user mode. +This foreground daemon starts the +.Sy sh +program that directly provides a root shell and exits with the shell's exit code +when the shell exits. +This operating system mode is insecure because it boots straight to root access +without a password. +It depends on the +.Sy base +and +.Sy local +daemons. +.It Sy sysinstall +Starts the operating system installer. +This foreground daemon starts the +.Sy sysinstall +program that provides the operating system installer and exits with the +installer's exit code when the installer exits. +This operating system mode is insecure because it boots straight to root access +without a password. +It depends on the +.Sy base +and +.Sy local +daemons. +.It Sy sysupgrade +Starts the operating system upgrader. +This foreground daemon starts the +.Sy sysupgrade +program that provides the operating system upgrader and exits with the +upgrader's exit code when the upgrader exits. +This operating system mode is insecure because it boots straight to root access +without a password. +It depends on the +.Sy base +and +.Sy local +daemons. +.El +.Pp +The following daemons are provided by the system: +.Bl -tag -width "12345678" +.It Sy base +Virtual daemon that depends on the core operating system daemons. +It depends on the +.Sy time +daemon. +.It Sy local +Virtual daemon that starts daemons pertinent to the local system. +The system provides a default implementation that does nothing. +The system administrator is meant to override the daemon in +.Pa /etc/init/local +by depending on daemons outside of the base system that should run on the local +system. +.It Sy time +Virtual daemon that becomes ready when the current date and time has been +established. +The system provides a default implementation that does nothing, as the base +system does not contain a daemon that obtains the current date and time. +The system administrator is meant to override the daemon in +.Pa /etc/init/time +by depending on a daemon that obtains the current date and time and sets the +system time. +Daemons can depend on this daemon if they need the current date and time to have +been established before they start. +.El +.Sh FORMAT +Daemon configuration files are processed line by line. +Each line specifies a property of the daemon. +Lines are tokenized like shell commands on white space with support for single +qoutes, double quotes, and backslash escape sequences (\\\\, \\', \\", \\a, \\b, +\\e, \\f, \\n, \\r, \\t, \\v). +The # character starts a comment and the rest of the line is ignored. +.Bl -tag -width "12345678" +.It Sy cd Ar directory +The working directory to run the deamon inside. +(Default is +.Pa / ) +.It Sy exec Ar command +The command line that starts the daemon. +The daemon becomes ready when it writes +a newline to the file descriptor mentioned in the +.Ev READYFD +environment variable as described in +.Xr daemon 7 . +.Pp +If this property isn't specified, then the daemon is a virtual daemon. +Virtual deamons become ready when all their dependencies are ready and finish +when all their dependencies are finished. +Virtual daemons exit 0 (success) if every dependency finished successfully, +otherwise they exit 3 (failed). +.It Sy exit-code-meaning Oo Sy default "|" poweroff-reboot Oc +This property specifies how to interpret the exit code. +.Pp +The +.Sy default +meaning is that exiting 0 is successful. +Any other exit means the daemon failed. +.Pp +The +.Sy poweroff-reboot +meaning is that exiting 0 means the system should power off, exiting 1 means the +system should reboot, exiting 2 means the system should halt, and any other exit +means the daemon failed. +.Pp +Daemons are considered successful if they exit by +.Sy SIGTERM +if +.Xr init 8 +stopped the daemon by sending +.Sy SIGTERM. +.It Sy furthermore +The current daemon configuration file extends an existing daemon that is defined +in a configuration file by the same name later in the search path. +The later configuration file is included into the current configuration file. +This statement can only be used once per configuration file, any subsequent uses +are silently ignored, but it can be used recursively. +Customizing an existing daemon should be done by adding a new daemon file +earlier in the search path that starts with the +.Sy furthermore +statement, followed by additional configuration. +.Pp +This is not a property and cannot be +.Sy unset . +.It Sy log-control-messages Oo Sy false "|" true Oc +Includes control messages such as the start and stop of the daemon and loss of +log data. +Control messages are inserted as entries from the daemon +.Sy init . +.Pp +The default is +.Sy true +and is +inherited from the +.Sy default +deamon. +.It Sy log-file-mode Ar octal +Sets the log file permissions to the +.Ar octal +mode with +.Xr chmod 2 . +.Pp +The default value is +.Sy 644 +and is inherited from the +.Sy default +deamon. +.It Sy log-format Ar format +Selects the +.Ar format +of the log: +.Bl -tag -width "nanoseconds" +.It Sy none +The log is exactly as written by the daemon with no additional formatting. +.It Sy seconds +"YYYY-dd-mm HH:MM:SS +0000: " +.Pp +Each line is prefixed with a timestamp with second precision and the timezone +offset. +.It Sy nanoseconds +"YYYY-dd-mm HH:MM:SS.nnnnnnnnn +0000: " +.Pp +Each line is prefixed with a timestamp with nanosecond precision and the +timezone offset. +.It Sy basic +"YYYY-dd-mm HH:MM:SS.nnnnnnnnn +0000 daemon: " +.Pp +Each line is prefixed with a timestamp with nanosecond precision and the +timezone offset followed by the name of the daemon. +.It Sy full +"YYYY-dd-mm HH:MM:SS.nnnnnnnnn +0000 hostname daemon: " +.Pp +Each line is prefixed with a timestamp with nanosecond precision and the +timezone offset followed +by the hostname and name of the daemon. +.It Sy syslog +"1 YYYY-dd-mmTHH:MM:SS.uuuuuuZ hostname daemon pid - - " +.Pp +Each line is prefixed in the RFC 5424 syslog version 1 format with the priority, +the timestamp with microsecond precision and the timezone offset, the hostname, +the daemon name, and the process id. +.El +.Pp +The default format is +.Sy nanoseconds +and is inherited from the +.Sy default +deamon. +.It Sy log-line-size Ar line-size +When using the +.Sy rotate +log method, log files are cut at newlines if the lines don't exceed +.Ar line-size +bytes. +.Pp +The default value is 4096 bytes and is inherited from the +.Sy default +deamon. +.It Sy log-method Oo Sy none "|" append "|" rotate Oc +Selects the method for logging: +.Bl -tag -width "12345678" +.It Sy none +Disable logging. +.It Sy append +Always append the log data to the log file without any rotation. +For instance, +.Pa exampled.log +will contain all the log entries ever produced by the +.Sy exampled +daemon. +.Pp +This method does not lose log data but it will fail when filesystem space is +exhausted. +.It Sy rotate +Append lines to the log file until it becomes too large, in which case the +daemon's logs are rotated. +.Pp +Rotation is done by deleting the oldest log (if there are too many), each of the +remaining log files are renamed with the subsequent number, and a new log file +is begun. +The logs are cut on a newline boundary if the lines doesn't exceed +.Sy log-line-size . +.Pp +For instance, +.Pa exampled.log.2 +is deleted, +.Pa exampled.log.1 +becomes +.Pa exampled.log.2 , +.Pa exampled.log.1 +becomes +.Pa exampled.log.2 , +and a new +.Pa exampled.log +is begun. +.Pp +This method will lose old log data. +.El +.Pp +The default format is +.Sy rotate +and is inherited from the +.Sy default +deamon. +.It Sy log-rotate-on-start Oo Sy false "|" true Oc +When starting the daemon, rotate the logs (when using the +.Sy rotate +log method) or empty the log (when using the +.Sy append +log method), such that the daemon starts out with a new log. +.Pp +The default value is +.Sy false +and is inherited from the +.Sy default +deamon. +.It Sy log-size Ar size +When using the +.Sy rotate +log method, keep each log file below +.Ar size +bytes. +.Pp +The default value is 1048576 bytes and is inherited from the +.Sy default +deamon. +.It Sy need tty +Specifies that the daemon is not a background daemon, but instead is the +foreground daemon controlling the terminal in the +.Sy tty +property. +The daemon is made a process group leader. +The terminal's foreground process group is set to that of the daemon. +The terminal is enabled by setting +.Sy CREAD . +The daemon is not logged, and the standard input, output, and error are instead +connected to the terminal +Foreground daemons are automatically considered ready and don't participate in +the +.Ev READYFD +daemon readiness protocol. +Upon exit, the original terminal settings are restored and +.Xr init 8 +reclaims ownership of the terminal. +.It Sy require Ar dependency Oo Ar flag ... Oc +When the daemon is needed, start the +.Ar dependency +first. +The daemon starts when all its dependencies have become ready or have finished. +Dependencies are started in parallel whenever possible. +If the daemon hasn't started yet, and any non-optional dependency finishes +unsuccessfully, then the daemon doesn't start and instead directly finishes +unsuccessfully. +If the daemon has started, it is the daemon's responsibility to detect failures +in its dependencies. +.Pp +The dependency can be customized with zero or more flags: +.Bl -tag -width "12345678" +.It Sy exit-code +If the daemon is a virtual daemon, then the daemon's exit code is that of the +specific +.Ar dependency +rather than whether all dependencies succeeded. +The daemon exits as soon as the +.Ar dependency +exits, rather than waiting for all dependencies to exit. +The +.Sy exit-code-meaning +field is set to that of the dependency. +.Sy exit-code +can at most be used on a single dependency for a daemon. +.It Sy no-await +Don't wait for the +.Ar dependency +to become ready before starting this daemon. +This flag is meant for dependencies that the daemon can make use of, but isn't +essential to the daemon itself becoming ready. +It shouldn't be used if the daemon polls for the the dependency to come online, +as it is more efficient to only start the daemon once the dependency is ready. +.It Sy optional +Start the daemon even if the +.Ar dependency +fails. +The dependency is assumed to exist and a warning occurs if it doesn't exist. +.El +.Pp +Dependencies can be forgotten using +.Sy unset require Ar dependency . +Flags on a dependency can be be unset using +.Sy unset require Ar dependency flag ... . +.It Sy unset Ar property +Reset the given property to its default value. +.It Sy tty Ar device +If the daemon is a foreground daemon +.Sy ( need tty +is set), then connect the daemon to the terminal named +.Ar device . +.Pp +The default value is the terminal +.Xr init 8 +is attached to, usually +.Pa tty1 . +.El +.Sh ENVIRONMENT +Daemons inherit their environment from +.Xr init 8 +with this additional environment: +.Bl -tag -width "READYFD" +.It Ev READYFD +Daemons signal they are ready by writing a newline to the file descriptor +mentioned in the +.Ev READYFD +environment variable as described in +.Xr daemon 7 . +.El +.Sh FILES +.Bl -tag -width /share/init/default -compact +.It Pa /etc/init/ +Daemon configuration for the local system (first in search path). +.It Pa /etc/init/default +The configuration file for the +.Sy default +daemon. +.It Pa /etc/init/local +The configuration file for the +.Sy local +daemon which depends on the installation's local daemons. +.It Pa /share/init/ +Default daemon configuration provided by the operating system (second in search +path). +.It Pa /var/log/ +Daemon log files. +.El +.Sh EXAMPLES +.Ss Configuring a daemon to start on boot +The local system can be configured to start the +.Sy exampled +daemon by creating +.Pa /etc/init/local +with the following contents: +.Bd -literal +require exampled optional +.Ed +.Pp +Additional lines can be included for any daemon you wish to start. +The +.Sy optional +flag means the +.Sy local +daemon doesn't fail if the daemon fails. +The top level daemons +.Sy ( multi-user , single-user , ... ) +fails if the +.Sy local +daemon fails, which will shut down the operating system. +The +.Sy optional +flag should only be omitted if a local daemon is critical and the boot should +fail if the daemon fails. +.Ss Creating a new virtual daemon +The +.Sy exampled +daemon, which depends on the +.Sy food , bard , +and +.Sy quxd +daemons and whose program file is called +.Pa exampled , +can then be configured by creating +.Pa /etc/init/exampled +with the following contents: +.Bd -literal +require food +require bard +require quxd +exec exampled +.Ed +.Ss Changing the log format +The default log format of daemons and +.Xr init 8 Ns 's +own can be set by setting the properties in +.Pa /etc/init/default . +A few examples: +.Bd -literal +log-format full +log-method append +.Ed +.Pp +Uses the +.Sy full +log format and grows the log without limit, never losing data unless the +filesystem space is exhausted. +.Bd -literal +log-control-messages false +log-format none +log-method rotate +log-rotate-on-start true +.Ed +.Pp +Provides plain rotated log files, by disabling control messages from +.Xr init 8 +about starting/stopping the daemon, turning off log metadata, and also rotates +the log when the deamon is started. +.Ss Configuring a multi-user system +The system can be configured to boot into multi-user mode by creating +.Pa /etc/init/default +with the following contents: +.Bd -literal +require multi-user exit-code +.Ed +.Ss Configuring an unattended system +A fully unattended system that only starts the base system and the +.Sy exampled +daemon, shutting down when the +.Sy exampled +daemon finishes, can be done by first creating +.Pa /etc/init/default +with the following contents: +.Bd -literal +require no-user exit-code +.Ed +.Pp +And then secondly creating +.Pa /etc/init/local +with the following contents: +.Bd -literal +require exampled exit-code +.Ed +.Sh SEE ALSO +.Xr daemon 7 , +.Xr init 8 +.Sh BUGS +The control messages mentioned in +.Sy log-control-messages +aren't implemented yet. +.Pp +The +.Sy tty +property isn't implemented yet and must be +.Pa tty1 +if set. diff --git a/share/man/man7/daemon.7 b/share/man/man7/daemon.7 new file mode 100644 index 00000000..276c6670 --- /dev/null +++ b/share/man/man7/daemon.7 @@ -0,0 +1,113 @@ +.Dd September 19, 2022 +.Dt DAEMON 7 +.Os +.Sh NAME +.Nm daemon +.Nd system background process +.Sh DESCRIPTION +A daemon is a system background process that performs a task or continuously +provides a service. +.Xr init 8 +starts daemons on system startup per the +.Xr init 5 +configuration and stops them on system shutdown. +.Pp +Conventions for daemons have varied between traditional init systems and this +document describes the modern design of daemons suitable for this operating +system. +Daemons should default to the behavior described below, or offer the behavior +through options if they need to be compatible with historic default behavior. +.Pp +A daemon is implemented as a system program, usually in +.Pa /sbin +inside the appropriate prefix, +whose name conventionally is the name of the service it implements plus the +letter d (as opposed to a client program). +Its runtime dependencies on other daemons are declared ahead of time in the +init system's configuration, so the daemons can be started in the right order. +.Pp +The process will be started per the init system's configuration with the +appropriate command line arguments, environment variables, working directory, +user, group, and so on. +.Pp +The process must remain in the foreground such that +.Xr init 8 +can manage its lifetime. +It must not +.Xr fork 2 +to become a background process and escape +the init system. +The process should have no need to escape the controlling terminal by starting a +new session using +.Xr setsid 2 . +Daemons should not write a pid file but instead be administered through the init +system. +.Pp +Logs should be written to the standard error as it is non-buffered and is meant +to contain messages that are not process output. +Alternatively logs may be written to the standard output. +The standard output may be the same file description as the standard error. +The standard input should not be used and will be +.Pa /dev/null . +The log entries should be formatted as plain line; or if the program wants to +supply additional meta data, one of the log formats described in +.Xr init 5 +or the syslog format. +.Xr syslog 3 +is discouraged but may be used if the program has additional meta data. +On this operating system, +.Xr syslog 3 +will write the log entry to the standard error instead of sending it to a +centralized log service, which is unreliable on other operating systems. +Daemons should prefer letting the init system manage the log files but may +provide their own logging as appropriate. +.Pp +The process may be executed as root per the init system configuration. +Privileges should be dropped after acquiring the needed protected resources. +The main loop should run with the least privileges required, ideally as another +user, potentially in a +.Xr chroot 2 +or sandboxed environment. +.Pp +Continuous daemons providing a service should signal their readiness once the +main loop is serving requests, such that the init system will start dependent +daemons. +Unfortunately there is no portable convention and this operating system uses the +.Ev READYFD +environment variable containing a file descriptor pointing to a writing pipe, +where the daemon must write a newline upon readiness. +Alternatively closing the pipe is considered readiness as a discouraged +fallback. +.Pp +The process must exit 0 if the daemon has concluded its work and exit non-zero +in the case of errors. +The daemon may be restarted by the init system +upon error per the configuration. +.Pp +The process must exit unconditionally when sent +.Dv SIGTERM +and should gracefully conclude its work immediately and recursively terminate +any child processes. +In this case, dying by the +.Dv SIGTERM +signal is considered a successful exit. +The process is killed with +.Dv SIGKILL +if it does not gracefully terminate within a high system-specific timeout. +.Sh EXAMPLES +A daemon can signal readiness using this utility function: +.Bd -literal -offset indent +static void ready(void) { + const char *readyfd_env = getenv("READYFD"); + if ( !readyfd_env ) + return; + int readyfd = atoi(readyfd_env); + char c = '\n'; + write(readyfd, &c, 1); + close(readyfd); + unsetenv("READYFD"); +} +.Ed +.Sh SEE ALSO +.Xr init 5 , +.Xr init 8 diff --git a/share/man/man7/following-development.7 b/share/man/man7/following-development.7 index 7568d817..11053f6b 100644 --- a/share/man/man7/following-development.7 +++ b/share/man/man7/following-development.7 @@ -69,6 +69,21 @@ releasing Sortix x.y, foo." to allow the maintainer to easily .Xr grep 1 for it after a release. .Sh CHANGES +.Ss Add daemon support to init(8) +.Xr init 8 +has gained +.Xr daemon 7 +support with the new +.Xr init 5 +configuration format. +.Pp +The old +.Pa /etc/init/target +configuration file is replaced by the +.Sy default +daemon in +.Pa /etc/init/default . +An upgrade hook will migrate the configuration. .Ss Add ports to the Sortix repository The ports have been moved from the porttix/srctix repositories into the .Pa ports/ diff --git a/share/man/man7/portability.7 b/share/man/man7/portability.7 index 091263ed..323d09e0 100644 --- a/share/man/man7/portability.7 +++ b/share/man/man7/portability.7 @@ -46,7 +46,9 @@ is the modern replacement with nanosecond precision. is the standard replacement. .Ss daemon Daemons should not background by double forking but rather stay in the -foreground and be managed by +foreground as described in +.Xr daemon 7 +and be managed by .Xr init 8 . .Ss __dead .Dv noreturn diff --git a/share/sysinstall/hooks/sortix-1.1-init b/share/sysinstall/hooks/sortix-1.1-init new file mode 100644 index 00000000..e69de29b diff --git a/sysinstall/Makefile b/sysinstall/Makefile index ff30baa6..d6ea8e42 100644 --- a/sysinstall/Makefile +++ b/sysinstall/Makefile @@ -54,6 +54,7 @@ install: all touch $(DESTDIR)$(DATADIR)/sysinstall/hooks/sortix-1.1-random-seed touch $(DESTDIR)$(DATADIR)/sysinstall/hooks/sortix-1.1-tix-manifest-mode touch $(DESTDIR)$(DATADIR)/sysinstall/hooks/sortix-1.1-leaked-files + touch $(DESTDIR)$(DATADIR)/sysinstall/hooks/sortix-1.1-init sysinstall: $(SYSINSTALL_OBJS) $(CC) $(SYSINSTALL_OBJS) -o $@ -lmount diff --git a/sysinstall/devices.c b/sysinstall/devices.c index bd12364d..ce3a2a7b 100644 --- a/sysinstall/devices.c +++ b/sysinstall/devices.c @@ -1,5 +1,5 @@ /* - * Copyright (c) 2015, 2016, 2021 Jonas 'Sortie' Termansen. + * Copyright (c) 2015, 2016, 2021, 2022 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 @@ -198,25 +198,29 @@ bool fsck(struct filesystem* fs) const char* bdev_path = path_of_blockdevice(fs->bdev); printf("%s: Repairing filesystem due to inconsistency...\n", bdev_path); assert(fs->fsck); - pid_t child_pid = fork(); - if ( child_pid < 0 ) + pid_t pid = fork(); + if ( pid < 0 ) { warn("%s: Mandatory repair failed: fork", bdev_path); return false; } - if ( child_pid == 0 ) + if ( pid == 0 ) { execlp(fs->fsck, fs->fsck, "-fp", "--", bdev_path, (const char*) NULL); warn("%s: Failed to load filesystem checker: %s", bdev_path, fs->fsck); _Exit(127); } int code; - if ( waitpid(child_pid, &code, 0) < 0 ) - { + if ( waitpid(pid, &code, 0) < 0 ) warn("waitpid"); - return false; + else if ( WIFEXITED(code) && + (WEXITSTATUS(code) == 0 || WEXITSTATUS(code) == 1) ) + { + // Successfully checked filesystem. + fs->flags &= ~(FILESYSTEM_FLAG_FSCK_SHOULD | FILESYSTEM_FLAG_FSCK_MUST); + return true; } - if ( WIFSIGNALED(code) ) + else if ( WIFSIGNALED(code) ) warnx("%s: Mandatory repair failed: %s: %s", bdev_path, fs->fsck, strsignal(WTERMSIG(code))); else if ( !WIFEXITED(code) ) @@ -228,14 +232,9 @@ bool fsck(struct filesystem* fs) else if ( WEXITSTATUS(code) & 2 ) warnx("%s: Mandatory repair: %s: %s", bdev_path, fs->fsck, "System reboot is necessary"); - else if ( WEXITSTATUS(code) != 0 && WEXITSTATUS(code) != 1 ) + else warnx("%s: Mandatory repair failed: %s: %s", bdev_path, fs->fsck, "Filesystem checker was unsuccessful"); - else - { - fs->flags &= ~(FILESYSTEM_FLAG_FSCK_SHOULD | FILESYSTEM_FLAG_FSCK_MUST); - return true; - } return false; } @@ -338,82 +337,106 @@ bool mountpoint_mount(struct mountpoint* mountpoint) warnx("Failed to fsck %s", bdev_path); return false; } - if ( !fs->driver ) - { - warnx("%s: Don't know how to mount a %s filesystem", - bdev_path, fs->fstype_name); - return false; - } const char* pretend_where = mountpoint->entry.fs_file; const char* where = mountpoint->absolute; + if ( !fs->driver ) + { + warnx("Failed mounting %s on %s: " + "Don't know how to mount a %s filesystem", + bdev_path, pretend_where, fs->fstype_name); + return false; + } struct stat st; if ( stat(where, &st) < 0 ) { - warn("stat: %s", where); + warn("Failed mounting %s on %s: stat: %s", + bdev_path, pretend_where, where); + return false; + } + int readyfds[2]; + if ( pipe(readyfds) < 0 ) + { + warn("Failed mounting %s on %s: pipe", bdev_path, pretend_where); return false; } if ( (mountpoint->pid = fork()) < 0 ) { - warn("%s: Unable to mount: fork", bdev_path); + warn("Failed mounting %s on %s: fork", bdev_path, pretend_where); + close(readyfds[0]); + close(readyfds[1]); return false; } - // TODO: This design is broken. The filesystem should tell us when it is - // ready instead of having to poll like this. if ( mountpoint->pid == 0 ) { + close(readyfds[0]); + char readyfdstr[sizeof(int) * 3]; + snprintf(readyfdstr, sizeof(readyfdstr), "%d", readyfds[1]); + if ( setenv("READYFD", readyfdstr, 1) < 0 ) + { + warn("Failed mounting %s on %s: setenv", + bdev_path, pretend_where); + _exit(127); + } execlp(fs->driver, fs->driver, "--foreground", bdev_path, where, "--pretend-mount-path", pretend_where, (const char*) NULL); - warn("%s: Failed to load filesystem driver: %s", bdev_path, fs->driver); + warn("Failed mount %s on %s: execvp: %s", + bdev_path, pretend_where, fs->driver); _exit(127); } - while ( true ) + close(readyfds[1]); + char c; + struct stat newst; + ssize_t amount = read(readyfds[0], &c, 1); + close(readyfds[0]); + if ( 0 <= amount ) { - struct stat newst; - if ( stat(where, &newst) < 0 ) + if ( !stat(where, &newst) ) { - warn("stat: %s", where); - if ( unmount(where, 0) < 0 && errno != ENOMOUNT ) - warn("unmount: %s", where); - else if ( errno == ENOMOUNT ) - kill(mountpoint->pid, SIGQUIT); - int code; - waitpid(mountpoint->pid, &code, 0); - mountpoint->pid = -1; - return false; - } - if ( newst.st_dev != st.st_dev || newst.st_ino != st.st_ino ) - break; - int code; - pid_t child = waitpid(mountpoint->pid, &code, WNOHANG); - if ( child < 0 ) - { - warn("waitpid"); - return false; - } - if ( child != 0 ) - { - mountpoint->pid = -1; - if ( WIFSIGNALED(code) ) - warnx("%s: Mount failed: %s: %s", bdev_path, fs->driver, - strsignal(WTERMSIG(code))); - else if ( !WIFEXITED(code) ) - warnx("%s: Mount failed: %s: %s", bdev_path, fs->driver, - "Unexpected unusual termination"); - else if ( WEXITSTATUS(code) == 127 ) - warnx("%s: Mount failed: %s: %s", bdev_path, fs->driver, - "Filesystem driver is absent"); - else if ( WEXITSTATUS(code) == 0 ) - warnx("%s: Mount failed: %s: Unexpected successful exit", - bdev_path, fs->driver); + if ( newst.st_dev != st.st_dev || newst.st_ino != st.st_ino ) + return true; else - warnx("%s: Mount failed: %s: Exited with status %i", bdev_path, - fs->driver, WEXITSTATUS(code)); - return false; + warnx("Failed mount %s on %s: %s: " + "No mounted filesystem appeared: %s", + bdev_path, pretend_where, fs->driver, where); } - struct timespec delay = timespec_make(0, 50L * 1000L * 1000L); - nanosleep(&delay, NULL); + else + warn("Failed mounting %s on %s: %s, stat: %s", + bdev_path, pretend_where, fs->driver, where); } - return true; + else + warn("Failed mounting %s on %s: %s, Failed to read readiness", + bdev_path, pretend_where, fs->driver); + if ( unmount(where, 0) < 0 ) + { + if ( errno != ENOMOUNT ) + warn("Failed mounting %s on %s: unmount: %s", + bdev_path, pretend_where, where); + kill(mountpoint->pid, SIGQUIT); + } + int code; + pid_t child = waitpid(mountpoint->pid, &code, 0); + mountpoint->pid = -1; + if ( child < 0 ) + warn("Failed mounting %s on %s: %s: waitpid", + bdev_path, pretend_where, fs->driver); + else if ( WIFSIGNALED(code) ) + warnx("Failed mounting %s on %s: %s: %s", + bdev_path, pretend_where, fs->driver, + strsignal(WTERMSIG(code))); + else if ( !WIFEXITED(code) ) + warnx("Failed mounting %s on %s: %s: Unexpected unusual termination", + bdev_path, pretend_where, fs->driver); + else if ( WEXITSTATUS(code) == 127 ) + warnx("Failed mounting %s on %s: %s: " + "Filesystem driver could not be executed", + bdev_path, pretend_where, fs->driver); + else if ( WEXITSTATUS(code) == 0 ) + warnx("Failed mounting %s on %s: %s: Unexpected successful exit", + bdev_path, pretend_where, fs->driver); + else + warnx("Failed mounting %s on %s: %s: Exited with status %i", + bdev_path, pretend_where, fs->driver, WEXITSTATUS(code)); + return false; } void mountpoint_unmount(struct mountpoint* mountpoint) diff --git a/sysinstall/hooks.c b/sysinstall/hooks.c index 6d7e7dfc..2603c712 100644 --- a/sysinstall/hooks.c +++ b/sysinstall/hooks.c @@ -257,6 +257,45 @@ void upgrade_prepare(const struct release* old_release, free(installed); } } + + // TODO: After releasing Sortix 1.1, remove this compatibility. + if ( hook_needs_to_be_run(source_prefix, target_prefix, "sortix-1.1-init") ) + { + char* init_target_path = join_paths(target_prefix, "/etc/init/target"); + char* init_default_path = + join_paths(target_prefix, "/etc/init/default"); + if ( !init_target_path || !init_default_path ) + { + warn("malloc"); + _exit(2); + } + char* line = read_string_file(init_target_path); + if ( line ) + { + printf(" - Converting /etc/init/target to /etc/init/default...\n"); + FILE* init_default_fp = fopen(init_default_path, "w"); + if ( !init_default_fp || + fprintf(init_default_fp, "require %s exit-code\n", line) < 0 || + fclose(init_default_fp) == EOF ) + { + warn("%s", init_default_path); + _exit(2); + } + free(line); + if ( unlink(init_target_path) < 0 ) + { + warn("unlink: %s", init_target_path); + _exit(2); + } + } + else if ( errno != ENOENT ) + { + warn("%s", init_target_path); + _exit(2); + } + free(init_target_path); + free(init_default_path); + } } void upgrade_finalize(const struct release* old_release, diff --git a/sysinstall/sysinstall.c b/sysinstall/sysinstall.c index 08833770..69f025e5 100644 --- a/sysinstall/sysinstall.c +++ b/sysinstall/sysinstall.c @@ -1033,7 +1033,8 @@ int main(void) else warn("mkdir: etc/init"); } - install_configurationf("etc/init/target", "w", "multi-user\n"); + install_configurationf("etc/init/default", "w", + "require multi-user exit-code\n"); text("Congratulations, the system is now functional! This is a good time " "to do further customization of the system.\n\n"); diff --git a/tix/tix-iso-bootconfig b/tix/tix-iso-bootconfig index 42354ff5..fb66f47d 100755 --- a/tix/tix-iso-bootconfig +++ b/tix/tix-iso-bootconfig @@ -23,6 +23,7 @@ default= directory= enable_append_title=true enable_src= +init_target= liveconfig= operand=1 random_seed=false @@ -53,6 +54,8 @@ for argument do --disable-src) enable_src=false ;; --enable-append-title) enable_append_title=true ;; --enable-src) enable_src=true ;; + --init-target=*) init_target=$parameter ;; + --init-target) previous_option=init_target ;; --liveconfig=*) liveconfig=$parameter ;; --liveconfig) previous_option=liveconfig ;; --random-seed) random_seed=true ;; @@ -137,6 +140,13 @@ mkdir -p -- "$directory/boot/grub" printf "base_menu_title=\"\$base_menu_title - \"'%s'\n" \ "$(printf '%s\n' "$append_title" | sed "s/'/'\\\\''/g")" fi + if [ -n "$init_target" ]; then + printf 'function hook_menu_pre {\n' + printf ' menuentry "Sortix (%s)" {\n' "$init_target" + printf ' load_sortix -- /sbin/init --target=%s\n' "$init_target" + printf ' }\n' + printf '}\n' + fi if [ -e "$directory/boot/liveconfig.tar.xz" ]; then printf 'function hook_initrd_post {\n' printf ' echo -n "Loading /boot/liveconfig.tar.xz (%s) ... "\n' \ diff --git a/tix/tix-iso-bootconfig.8 b/tix/tix-iso-bootconfig.8 index b223d8ee..a0b1fe10 100644 --- a/tix/tix-iso-bootconfig.8 +++ b/tix/tix-iso-bootconfig.8 @@ -12,6 +12,7 @@ .Op Fl \-disable-src .Op Fl \-enable-append-title .Op Fl \-enable-src +.Op Fl \-init-target Ns = Ns Ar target .Op Fl \-liveconfig Ns = Ns Ar liveconfig-directory .Op Fl \-random-seed .Op Fl \-timeout Ns = Ns Ar boot-menu-timeout @@ -110,6 +111,12 @@ by setting .Sy enable_src GRUB variable to .Sy true . +.It Fl \-init-target Ns = Ns Ar target +Add a new first menu entry that boots the +.Ar target +daemon as the +.Xr init 8 +target. .It Fl \-liveconfig Ns = Ns Ar liveconfig-directory Overlay the .Ar liveconfig-directory @@ -222,6 +229,16 @@ bootloader menu timeout to 2 seconds: tix-iso-bootconfig --default=1 --timeout=2 bootconfig tix-iso-add sortix.iso bootconfig .Ed +.Ss Non-interactive Live Environment +The interactive user environment can be disabled by setting the default +.Xr init 8 +.Fl \-target +to +.Sy no-user : +.Bd -literal +tix-iso-bootconfig --init-target=no-user bootconfig +tix-iso-add sortix.iso bootconfig +.Ed .Ss Add to Bootloader Menu Title To customize a release so the bootloader menu title is appended with a message of your choice: @@ -234,5 +251,6 @@ tix-iso-add sortix.iso bootconfig .Xr kernel 7 , .Xr release-iso-bootconfig 7 , .Xr release-iso-modification 7 , +.Xr init 8 , .Xr tix-iso-add 8 , .Xr tix-iso-liveconfig 8 diff --git a/tix/tix-iso-liveconfig b/tix/tix-iso-liveconfig index 79921bd4..46305647 100755 --- a/tix/tix-iso-liveconfig +++ b/tix/tix-iso-liveconfig @@ -1,5 +1,5 @@ #!/bin/sh -# Copyright (c) 2017 Jonas 'Sortie' Termansen. +# Copyright (c) 2017, 2022 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 @@ -18,6 +18,7 @@ set -e +daemons= directory= hostname= kblayout= @@ -41,6 +42,8 @@ for argument do case $dashdash$argument in --) dashdash=yes ;; + --daemons=*) daemons=$parameter ;; + --daemons) previous_option=daemons ;; --hostname=*) hostname=$parameter ;; --hostname) previous_option=hostname ;; --kblayout=*) kblayout=$parameter ;; @@ -73,6 +76,15 @@ fi mkdir -p "$directory" + +if [ -n "$daemons" ]; then + mkdir -p -- "$directory/etc/init" + true > "$directory/etc/init/local" + for daemon in $daemons; do + printf "require %s optional\n" "$daemon" >> "$directory/etc/init/local" + done +fi + if [ -n "$hostname" ]; then mkdir -p -- "$directory/etc" printf "%s\n" "$hostname" > "$directory/etc/hostname" diff --git a/tix/tix-iso-liveconfig.8 b/tix/tix-iso-liveconfig.8 index 3f233dfe..05d2035f 100644 --- a/tix/tix-iso-liveconfig.8 +++ b/tix/tix-iso-liveconfig.8 @@ -6,6 +6,7 @@ .Nd generate additional live environment configuration for Sortix .iso releases .Sh SYNOPSIS .Nm +.Op Fl \-daemons Ns = Ns Ar daemons .Op Fl \-hostname Ns = Ns Ar hostname .Op Fl \-kblayout Ns = Ns Ar kblayout .Op Fl \-videomode Ns = Ns Ar videomode @@ -43,6 +44,15 @@ installations made from inside it. .Pp The options are as follows: .Bl -tag -width "12345678" +.It Fl \-daemons Ns = Ns Ar daemons +Configures the +.Sy local +daemon to optionally depend on each of the +.Ar daemons +in +.Pa output-directory/etc/init/local . +(See +.Xr init 5 ) .It Fl \-hostname Ns = Ns Ar hostname Set the live environment's hostname by writing .Ar hostname diff --git a/update-initrd/update-initrd b/update-initrd/update-initrd index 3dc5eaff..9823b884 100755 --- a/update-initrd/update-initrd +++ b/update-initrd/update-initrd @@ -77,9 +77,13 @@ mkdir "$tmp/etc" cp "$sysroot/etc/fstab" "$tmp/etc/fstab" mkdir "$tmp/etc/init" if $sysmerge; then - echo chain-merge > "$tmp/etc/init/target" + cat > "$tmp/etc/init/default" << EOF +require chain-merge exit-code +EOF else - echo chain > "$tmp/etc/init/target" + cat > "$tmp/etc/init/default" << EOF +require chain exit-code +EOF fi mkdir -p "$sysroot/boot" mkinitrd --format=sortix-initrd-2 "$tmp" -o "$sysroot/boot/sortix.initrd" > /dev/null