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