Add service(8).

This commit is contained in:
Jonas 'Sortie' Termansen 2024-05-07 02:25:22 +02:00
parent 6f70708ff5
commit b676a5f2da
2 changed files with 434 additions and 19 deletions

View File

@ -8,26 +8,27 @@ CFLAGS?=$(OPTLEVEL)
CFLAGS:=$(CFLAGS) -Wall -Wextra
BINARY=init
BINARIES=\
init \
service \
OBJS=\
init.o \
MANPAGES8=\
init.8 \
all: $(BINARY)
# TODO: service.8
all: $(BINARIES)
.PHONY: all install clean
$(BINARY): $(OBJS)
$(CC) $(CFLAGS) $(OBJS) -o $(BINARY) -lmount $(LIBS)
%.o: %.c
$(CC) -std=gnu11 $(CFLAGS) $(CPPFLAGS) -c $< -o $@
%: %.c
$(CC) -std=gnu11 $(CFLAGS) $(CPPFLAGS) $< -o $@ -lmount $(LIBS)
install: all
mkdir -p $(DESTDIR)$(SBINDIR)
install $(BINARY) $(DESTDIR)$(SBINDIR)
install $(BINARIES) $(DESTDIR)$(SBINDIR)
mkdir -p $(DESTDIR)$(MANDIR)/man8
cp init.8 $(DESTDIR)$(MANDIR)/man8/init.8
install $(MANPAGES8) $(DESTDIR)$(MANDIR)/man8
clean:
rm -f $(BINARY) $(OBJS) *.o
rm -f $(BINARY) service $(OBJS) *.o

View File

@ -20,8 +20,10 @@
#include <sys/display.h>
#include <sys/ioctl.h>
#include <sys/mount.h>
#include <sys/socket.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/un.h>
#include <sys/wait.h>
#include <assert.h>
@ -137,6 +139,7 @@ struct dependency
#define DEPENDENCY_FLAG_REQUIRE (1 << 0)
#define DEPENDENCY_FLAG_AWAIT (1 << 1)
#define DEPENDENCY_FLAG_EXIT_CODE (1 << 2)
#define DEPENDENCY_FLAG_REFERENCED (1 << 30)
enum log_method
{
@ -223,7 +226,6 @@ struct daemon
bool need_tty;
bool was_ready;
bool was_terminated;
bool was_dereferenced;
bool timeout_set;
};
@ -255,10 +257,31 @@ struct daemon_config
mode_t log_file_mode;
};
struct server
{
size_t index;
const char* path;
int fd;
};
struct connection
{
size_t index;
char* input;
size_t input_used;
size_t input_size;
char* output;
size_t output_used;
size_t output_size;
int fd;
};
enum communication_type
{
COMMUNICATION_TYPE_OUTPUT,
COMMUNICATION_TYPE_READY,
COMMUNICATION_TYPE_SERVER,
COMMUNICATION_TYPE_CONNECTION,
};
struct communication
@ -1742,6 +1765,27 @@ static struct daemon* daemon_create_unconfigured(const char* name)
return daemon;
}
static struct daemon* daemon_find_or_create(const char* name)
{
// TODO: What happens for virtual daemons?
struct daemon* daemon = daemon_find_by_name(name);
// TODO: non-fatal daemon_create_unconfigured
if ( !daemon )
daemon = daemon_create_unconfigured(name);
return daemon;
}
static struct dependency* daemon_find_dependency(struct daemon* daemon,
const char* target)
{
for ( size_t i = 0; i < daemon->dependencies_used; i++ )
{
if ( !strcmp(daemon->dependencies[i]->target->name, target) )
return daemon->dependencies[i];
}
return NULL;
}
static bool daemon_add_dependency(struct daemon* daemon,
struct daemon* target,
int flags)
@ -1772,6 +1816,7 @@ static bool daemon_add_dependency(struct daemon* daemon,
if ( flags & DEPENDENCY_FLAG_EXIT_CODE )
daemon->exit_code_from = dependency;
target->reference_count++;
dependency->flags |= DEPENDENCY_FLAG_REFERENCED;
return true;
}
@ -1878,12 +1923,39 @@ static struct daemon* daemon_create(struct daemon_config* daemon_config)
return daemon;
}
static void schedule_daemon(struct daemon* daemon)
static void daemon_schedule(struct daemon* daemon)
{
assert(daemon->state == DAEMON_STATE_TERMINATED);
daemon_change_state_list(daemon, DAEMON_STATE_SCHEDULED);
}
static void daemon_request_restart(struct daemon* daemon)
{
// TODO: Force a restart if STARTING, RUNNING.
// TODO: In TERMINATING, FINISHING, schedule the restart.
if ( daemon->state == DAEMON_STATE_FINISHED )
{
daemon_change_state_list(daemon, DAEMON_STATE_SCHEDULED);
// TODO: Update dependencies_ready.
// TODO: Update dependencies_finished.
// TODO: Update dependencies_failed.
daemon->was_ready = false;
daemon->was_terminated = false;
// TODO: was_dereferenced?
daemon->timeout_set = false; // TODO: Reset before going to FINISHING.
}
}
static void daemon_request_start(struct daemon* daemon)
{
if ( daemon->state == DAEMON_STATE_TERMINATED )
daemon_schedule(daemon);
else if ( daemon->state == DAEMON_STATE_TERMINATING ||
daemon->state == DAEMON_STATE_FINISHING ||
daemon->state == DAEMON_STATE_FINISHED )
daemon_request_restart(daemon);
}
static void daemon_on_finished(struct daemon* daemon)
{
assert(daemon->state != DAEMON_STATE_FINISHING);
@ -1947,12 +2019,19 @@ static void daemon_dereference(struct daemon* daemon)
daemon_on_not_referenced(daemon);
}
static void daemon_dereference_dependency(struct dependency* dependency)
{
if ( dependency->flags & DEPENDENCY_FLAG_REFERENCED )
{
daemon_dereference(dependency->target);
dependency->flags &= ~(DEPENDENCY_FLAG_REFERENCED);
}
}
static void daemon_dereference_dependencies(struct daemon* daemon)
{
assert(!daemon->was_dereferenced);
daemon->was_dereferenced = true;
for ( size_t i = 0; i < daemon->dependencies_used; i++ )
daemon_dereference(daemon->dependencies[i]->target);
daemon_dereference_dependency(daemon->dependencies[i]);
}
static void daemon_on_dependency_ready(struct dependency* dependency)
@ -2015,6 +2094,96 @@ static void daemon_on_dependency_finished(struct dependency* dependency)
daemon_on_finished(daemon);
}
static bool daemon_depend(struct daemon* source,
struct daemon* target,
int flags,
bool ensure_start)
{
struct dependency* dependency =
daemon_find_dependency(source, target->name);
if ( dependency )
{
// TODO: Updating the flags could be tricky and unsafe.
return true;
}
else if ( !daemon_add_dependency(source, target, flags) )
return false;
// No need to start the target if the source isn't supposed to be running.
if ( source->state == DAEMON_STATE_TERMINATED )
return true;
// No need to start the target if the source is a non-virtual daemon that is
// terminating, or a virtual daemon with exit-code that is terminating.
if ( (source->argv || source->exit_code_from) &&
(source->state == DAEMON_STATE_TERMINATING ||
source->state == DAEMON_STATE_FINISHING ||
source->state == DAEMON_STATE_FINISHED) )
return true;
// No need to start the target if it is finishing, unless we are asked to
// ensure it is started up again.
// TODO: ensure_start can be always true once there's a proper one shot
// vs persistent daemon concept.
if ( !ensure_start &&
(target->state == DAEMON_STATE_TERMINATING ||
target->state == DAEMON_STATE_FINISHING ||
target->state == DAEMON_STATE_FINISHED) )
return true;
if ( target->state == DAEMON_STATE_TERMINATED ||
target->state == DAEMON_STATE_TERMINATING ||
target->state == DAEMON_STATE_FINISHING ||
target->state == DAEMON_STATE_FINISHED )
daemon_request_start(target);
if ( (target->state == DAEMON_STATE_TERMINATED ||
target->state == DAEMON_STATE_SCHEDULED ||
target->state == DAEMON_STATE_WAITING) )
{
if ( source->state == DAEMON_STATE_SATISFIED )
daemon_change_state_list(target, DAEMON_STATE_WAITING);
else if ( source->state == DAEMON_STATE_TERMINATING ||
source->state == DAEMON_STATE_FINISHING ||
source->state == DAEMON_STATE_FINISHED )
{
// TODO: Propagate recursive dependents to RUNNING.
// TODO: dependencies_finished and such
daemon_change_state_list(source, DAEMON_STATE_RUNNING);
}
}
return true;
}
static void daemon_undepend(struct daemon* source,
struct daemon* target,
bool ensure_stop)
{
for ( size_t i = 0; i < source->dependencies_used; i++ )
{
if ( source->dependencies[i]->target != target )
continue;
struct dependency* dependency = source->dependencies[i];
daemon_dereference_dependency(source->dependencies[i]);
size_t last = source->dependencies_used-- - 1;
if ( i != last )
source->dependencies[i] = source->dependencies[last];
for ( size_t n = 0; i < target->dependents_used; n++ )
{
if ( target->dependents[n] != dependency )
continue;
last = target->dependents_used-- - 1;
if ( n != last )
target->dependents[i] = target->dependents[last];
}
free(dependency);
// TODO: Correct dependencies_ready.
// TODO: Correct dependencies_finished.
// TODO: Correct dependencies_failed.
// TODO: State transition if now SATISFIED or FINISHED.
break;
}
if ( ensure_stop &&
(target->state == DAEMON_STATE_STARTING ||
target->state == DAEMON_STATE_RUNNING) )
daemon_terminate(target);
}
static void daemon_finish(struct daemon* daemon)
{
assert(daemon->state != DAEMON_STATE_FINISHED);
@ -2064,7 +2233,7 @@ static void daemon_wait(struct daemon* daemon)
switch ( dependency->target->state )
{
case DAEMON_STATE_TERMINATED:
schedule_daemon(dependency->target);
daemon_schedule(dependency->target);
if ( !(dependency->flags & DEPENDENCY_FLAG_AWAIT) )
daemon_on_dependency_ready(dependency);
break;
@ -2378,6 +2547,241 @@ static void daemon_on_exit(struct daemon* daemon, int exit_code)
daemon_on_finished(daemon);
}
static void request_require(int argc, char** argv)
{
if ( argc < 3 )
return warning("missing operand");
const char* source_name = argv[1];
const char* target_name = argv[2];
bool start = 4 <= argc && !strcmp(argv[3], "start");
// TODO: Parse flags.
int flags = DEPENDENCY_FLAG_AWAIT;
struct daemon* source = daemon_find_by_name(source_name);
if ( !source )
return warning("no %s", source_name);
if ( source->argv )
return warning("source had argv");
struct daemon* target = daemon_find_or_create(target_name);
if ( !target )
return warning("failed to create %s", target_name);
if ( !daemon_depend(source, target, flags, start) )
return warning("failed to depend %s -> %s", source_name, target_name);
}
static void request_unrequire(int argc, char** argv)
{
if ( argc < 3 )
return warning("missing operand");
const char* source_name = argv[1];
const char* target_name = argv[2];
bool stop = 4 <= argc && !strcmp(argv[3], "stop");
struct daemon* source = daemon_find_by_name(source_name);
if ( !source )
return warning("no %s", source_name);
struct daemon* target = daemon_find_by_name(target_name);
if ( !target )
return warning("no such daemon running: %s", target_name);
daemon_undepend(source, target, stop);
}
static void connection_free(struct connection* conn)
{
free(conn->input);
free(conn->output);
free(conn);
}
static void connection_close(struct connection* conn)
{
communication_unregister(conn->index);
close(conn->fd);
conn->fd = -1;
connection_free(conn);
}
static struct connection* connection_new(int fd)
{
size_t buffer_size = 4096;
struct connection* conn = calloc(1, sizeof(struct connection));
char* input = malloc(buffer_size);
char* output = malloc(buffer_size);
if ( !conn || !input || !output )
{
free(conn);
free(input);
free(output);
return NULL;
}
conn->input = input;
conn->input_used = 0;
conn->input_size = buffer_size;
conn->output = output;
conn->output_used = 0;
conn->output_size = buffer_size;
conn->fd = fd;
if ( !communication_reserve(1) )
return connection_free(conn), NULL;
struct communication comm;
comm.type = COMMUNICATION_TYPE_CONNECTION;
comm.index_ptr = &conn->index;
comm.connection = conn;
communication_register(&comm, fd, POLLIN);
return conn;
}
static void connection_on_message(struct connection* conn, const char* message)
{
(void) conn; // TODO: Reply.
size_t argc = 0;
char** argv = tokenize(&argc, message);
if ( !argv )
{
if ( !errno )
/* TODO: syntax error */{}
else
/* TODO: other error */{}
return false;
}
if ( argc == 0 ) {}
else if ( !strcmp(argv[0], "require") )
request_require(argc, argv);
else if ( !strcmp(argv[0], "unrequire") )
request_unrequire(argc, argv);
else
warning("%s", message);
for ( size_t i = 0; i < argc; i++ )
free(argv[i]);
free(argv);
}
static bool connection_on_event(struct connection* conn, int revents)
{
// TODO: POLLHUP and POLLERR.
if ( (revents & POLLIN) && conn->input_used < conn->input_size )
{
ssize_t amount = recv(conn->fd, conn->input + conn->input_used,
conn->input_size - conn->input_used, 0);
if ( 0 < amount )
{
size_t i = conn->input_used;
conn->input_used += amount;
while ( i < conn->input_used )
{
if ( conn->input[i] == '\n' )
{
conn->input[i] = '\0';
connection_on_message(conn, conn->input);
memmove(conn->input, conn->input + i, conn->input_size - i);
}
else
i++;
}
// Disconnect if the input line is too long.
if ( conn->input_used == conn->input_size )
{
connection_close(conn);
return false;
}
}
else if ( amount == 0 ||
(amount < 0 && errno != EWOULDBLOCK && errno != EAGAIN ) )
{
connection_close(conn);
return false;
}
}
if ( (revents & POLLOUT) && conn->output_used )
{
ssize_t amount = send(conn->fd, conn->output, conn->output_used,
MSG_NOSIGNAL);
if ( 0 < amount )
{
if ( (size_t) amount < conn->output_used )
memmove(conn->output, conn->output + amount,
conn->output_used - amount);
conn->output_used -= amount;
}
else if ( errno != EWOULDBLOCK && errno != EAGAIN )
{
connection_close(conn);
return false;
}
}
if ( (revents & (POLLHUP | POLLERR)) )
{
connection_close(conn);
return false;
}
// TODO: Recompute poll bits.
return true;
}
static int open_local_server_socket(const char* path, int flags)
{
size_t path_length = strlen(path);
size_t addr_size = offsetof(struct sockaddr_un, sun_path) + path_length + 1;
struct sockaddr_un* sockaddr = malloc(addr_size);
if ( !sockaddr )
return -1;
sockaddr->sun_family = AF_LOCAL;
strcpy(sockaddr->sun_path, path);
int fd = socket(AF_LOCAL, SOCK_STREAM | flags, 0);
if ( fd < 0 )
return free(sockaddr), -1;
if ( fchmod(fd, 0600) < 0 )
return close(fd), free(sockaddr), -1;
if ( bind(fd, (const struct sockaddr*) sockaddr, addr_size) < 0 )
return close(fd), free(sockaddr), -1;
if ( listen(fd, SOMAXCONN) < 0 )
return close(fd), free(sockaddr), -1;
free(sockaddr);
return fd;
}
static struct server* server_start(const char* path)
{
if ( !communication_reserve(1) )
return NULL;
struct server* server = malloc(sizeof(struct server));
if ( !server )
return NULL;
server->fd = open_local_server_socket(path, SOCK_NONBLOCK | SOCK_CLOEXEC);
if ( server->fd < 0 )
{
free(server);
return NULL;
}
struct communication comm;
comm.type = COMMUNICATION_TYPE_SERVER;
comm.index_ptr = &server->index;
comm.server = server;
communication_register(&comm, server->fd, POLLIN);
return server;
}
static bool server_on_event(struct server* server, int revents)
{
if ( revents & POLLIN )
{
int fd = accept4(server->fd, NULL, NULL, SOCK_NONBLOCK | SOCK_CLOEXEC);
if ( 0 <= fd )
{
struct connection* conn = connection_new(fd);
if ( !conn )
{
warning("Failed to allocate connection: %s: %m", server->path);
close(fd);
}
}
else if ( errno != EAGAIN && errno != EWOULDBLOCK )
warning("accept: %s: %m", server->path);
}
return true;
}
static void init(void)
{
int default_daemon_exit_code = -1;
@ -2500,6 +2904,12 @@ static void init(void)
case COMMUNICATION_TYPE_READY:
closed = daemon_on_ready_event(comm->daemon, pfd->revents);
break;
case COMMUNICATION_TYPE_SERVER:
closed = server_on_event(comm->server, pfd->revents);
break;
case COMMUNICATION_TYPE_CONNECTION:
closed = connection_on_event(comm->connection, pfd->revents);
break;
}
if ( closed )
i--; // Process this index again (something new there).
@ -3712,13 +4122,17 @@ int main(int argc, char* argv[])
reinit();
}
const char* server_path = "/var/run/init";
if ( !server_start(server_path) )
fatal("Failed to start init server: %s: %m", server_path);
// TODO: Use the arguments to specify additional things the default daemon
// should depend on, as well as a denylist of things not to start
// even if in default's dependencies. The easiest thing is probably to
// be able to inject require and unset require lines into default.
// Request the default daemon be run.
schedule_daemon(default_daemon);
daemon_schedule(default_daemon);
// Initialize the operating system.
init();