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().
This commit is contained in:
apio 2024-07-31 19:43:09 +02:00
parent 2aefbdc4ee
commit 0ab8efd405
Signed by: apio
GPG Key ID: B8A7D06E42258954
3 changed files with 162 additions and 0 deletions

View File

@ -320,5 +320,78 @@ namespace os
bool m_ipc_in_progress { false }; bool m_ipc_in_progress { false };
u8 m_ipc_saved_id { 0 }; 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();
} }
} }

View File

@ -7,7 +7,13 @@
* *
*/ */
#include <errno.h>
#include <fcntl.h>
#include <luna/String.h>
#include <os/IPC.h> #include <os/IPC.h>
#include <stdlib.h>
#include <sys/poll.h>
#include <unistd.h>
namespace os::IPC namespace os::IPC
{ {
@ -84,4 +90,60 @@ namespace os::IPC
Client::Client(OwnedPtr<LocalClient>&& connection) : m_connection(move(connection)) Client::Client(OwnedPtr<LocalClient>&& 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");
}
} }

View File

@ -8,6 +8,7 @@
#include <os/ArgumentParser.h> #include <os/ArgumentParser.h>
#include <os/Directory.h> #include <os/Directory.h>
#include <os/File.h> #include <os/File.h>
#include <os/IPC.h>
#include <os/Process.h> #include <os/Process.h>
#include <os/Security.h> #include <os/Security.h>
#include <pwd.h> #include <pwd.h>
@ -38,6 +39,7 @@ struct Service
Option<uid_t> user {}; Option<uid_t> user {};
Option<gid_t> group {}; Option<gid_t> group {};
bool wait { false }; bool wait { false };
bool wait_notify { false };
Option<pid_t> pid {}; Option<pid_t> pid {};
}; };
@ -132,9 +134,13 @@ static Result<void> try_start_service(Service& service)
new_stdin->set_close_on_exec(); 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()); pid_t pid = TRY(os::Process::fork());
if (pid == 0) if (pid == 0)
{ {
if (service.wait_notify) { notifier.hook(); }
auto rc = service_child(service, new_stdout, new_stderr, new_stdin); auto rc = service_child(service, new_stdout, new_stderr, new_stdin);
if (rc.has_error()) if (rc.has_error())
{ {
@ -157,7 +163,17 @@ static Result<void> try_start_service(Service& service)
do_log("[init] child process %d exited with code %d\n", pid, WEXITSTATUS(status)); do_log("[init] child process %d exited with code %d\n", pid, WEXITSTATUS(status));
} }
else 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; service.pid = pid;
}
return {}; return {};
} }
@ -273,6 +289,17 @@ static Result<void> load_service(const os::Path& path)
continue; 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()); do_log("[init] skipping unknown entry name %s\n", parts[0].chars());
} }