From 0ab8efd405e657d824746d94d2f47e907100e651 Mon Sep 17 00:00:00 2001 From: apio Date: Wed, 31 Jul 2024 19:43:09 +0200 Subject: [PATCH] libos+init: Add a Notifier API to know when child processes have finished initialization init now supports the "WaitUntilReady" key, which will wait until the child calls os::IPC::notify_parent(). --- libos/include/os/IPC.h | 73 ++++++++++++++++++++++++++++++++++++++++++ libos/src/IPC.cpp | 62 +++++++++++++++++++++++++++++++++++ system/init.cpp | 27 ++++++++++++++++ 3 files changed, 162 insertions(+) diff --git a/libos/include/os/IPC.h b/libos/include/os/IPC.h index 7dd56fa5..5385a4a6 100644 --- a/libos/include/os/IPC.h +++ b/libos/include/os/IPC.h @@ -320,5 +320,78 @@ namespace os bool m_ipc_in_progress { false }; u8 m_ipc_saved_id { 0 }; }; + + /** + * @brief API used to notify a parent process when a child process finishes initialization. The Notifier struct + * is the parent part of the API. + * + */ + struct Notifier + { + int pfds[2]; + + /** + * @brief Create a new Notifier. + * + * This function will create a pipe for the parent and child to communicate. + * + * @return Notifier The new Notifier object. + */ + static Notifier create(); + + /** + * @brief Hook the Notifier into any child process started afterwards. + * + * This will set an environment variable, which if detected by a child process, will use it to notify the + * parent whenever it's ready. + * + * The recommended order to call this API is: + * hook() + * fork+exec + * unhook() + */ + void hook(); + + /** + * @brief Remove the previously created environment variable, so that any future child processes will not + * notify this Notifier. + * + */ + void unhook(); + + /** + * @brief Wait for a child process to be ready. If several child processes are hooked by the hook() method, + * this method will only catch the first one that notifies the parent. + * + * @param timeout If positive, specifies the timeout after which the function fails if no notification is + * received. + * @return true The child is ready. + * @return false The method timed out. + */ + bool wait(int timeout = -1); + + /** + * @brief Combines hook(), unhook() and wait() into one single method. This method takes a function, and + * executes it in a "hooked" context, so that any child process started by this function will automatically + * detect the parent when it's ready (if it supports the notification API). Then, the function waits for the + * child to be ready. + * + * @param action The function to run. + * @param timeout If positive, specifies the timeout after which the function fails if no notification is + * received. + * @return true The child is ready. + * @return false The method timed out. + */ + static bool run_and_wait(os::Action&& action, int timeout = -1); + }; + + /** + * @brief Use this function to notify a parent process whenever your program finishes initialization. + * + * If the parent has not used the Notifier API to request a notification from the child process, this function + * does nothing. + * + */ + void notify_parent(); } } diff --git a/libos/src/IPC.cpp b/libos/src/IPC.cpp index 7fd6a1ae..bba6602e 100644 --- a/libos/src/IPC.cpp +++ b/libos/src/IPC.cpp @@ -7,7 +7,13 @@ * */ +#include +#include +#include #include +#include +#include +#include namespace os::IPC { @@ -84,4 +90,60 @@ namespace os::IPC Client::Client(OwnedPtr&& connection) : m_connection(move(connection)) { } + + Notifier Notifier::create() + { + Notifier result; + pipe(result.pfds); + fcntl(result.pfds[0], F_SETFD, FD_CLOEXEC); + return result; + } + + void Notifier::hook() + { + auto value = String::format("%d"_sv, pfds[1]).release_value(); + setenv("LUNA_NOTIFY_FD", value.chars(), 1); + } + + void Notifier::unhook() + { + unsetenv("LUNA_NOTIFY_FD"); + } + + bool Notifier::wait(int timeout) + { + close(pfds[1]); + struct pollfd fds[] = { { .fd = pfds[0], .events = POLLIN, .revents = 0 } }; + + poll: + int result = poll(fds, 1, timeout); + if (result < 0 && errno == EINTR) goto poll; + + close(pfds[0]); + + return result > 0; + } + + bool Notifier::run_and_wait(os::Action&& action, int timeout) + { + auto notifier = create(); + notifier.hook(); + action(); + notifier.unhook(); + return notifier.wait(timeout); + } + + void notify_parent() + { + char* fd_string = getenv("LUNA_NOTIFY_FD"); + if (!fd_string) return; + + int fd = atoi(fd_string); + + u8 data = 0; + write(fd, &data, 1); + + close(fd); + unsetenv("LUNA_NOTIFY_FD"); + } } diff --git a/system/init.cpp b/system/init.cpp index 19c9ccef..cf238661 100644 --- a/system/init.cpp +++ b/system/init.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,7 @@ struct Service Option user {}; Option group {}; bool wait { false }; + bool wait_notify { false }; Option pid {}; }; @@ -132,9 +134,13 @@ static Result try_start_service(Service& service) new_stdin->set_close_on_exec(); } + os::IPC::Notifier notifier; + if (service.wait_notify) { notifier = os::IPC::Notifier::create(); } + pid_t pid = TRY(os::Process::fork()); if (pid == 0) { + if (service.wait_notify) { notifier.hook(); } auto rc = service_child(service, new_stdout, new_stderr, new_stdin); if (rc.has_error()) { @@ -157,7 +163,17 @@ static Result try_start_service(Service& service) do_log("[init] child process %d exited with code %d\n", pid, WEXITSTATUS(status)); } else + { + if (service.wait_notify) + { + bool success = notifier.wait(1000); + if (!success) + { + do_log("[init] service %s with pid %d failed to start successfully\n", service.name.chars(), pid); + } + } service.pid = pid; + } return {}; } @@ -273,6 +289,17 @@ static Result load_service(const os::Path& path) continue; } + if (parts[0].view() == "WaitUntilReady") + { + if (parts[1].view() == "true" || parts[1].view().to_uint().value_or(0) == 1) + { + service.wait_notify = true; + continue; + } + service.wait_notify = false; + continue; + } + do_log("[init] skipping unknown entry name %s\n", parts[0].chars()); }