mirror of
https://github.com/nestriness/nestri.git
synced 2026-11-03 22:22:39 +02:00
feat(nesinit): the box starts one daemon for its whole life, beside the compositor
This commit is contained in:
@@ -13,7 +13,7 @@ use std::os::unix::ffi::OsStringExt as _;
|
||||
use std::pin::Pin;
|
||||
|
||||
use nesprotocol::lifecycle::{
|
||||
AttachId, Compositor, Exec, Exit, Image, LAUNCH_ID_ENV, LaunchId, Mount, Overlay, Winsize,
|
||||
AttachId, Compositor, Daemon, Exec, Exit, Image, LAUNCH_ID_ENV, LaunchId, Mount, Overlay, Winsize,
|
||||
};
|
||||
|
||||
use std::os::unix::process::CommandExt;
|
||||
@@ -99,6 +99,20 @@ pub trait Workload {
|
||||
None
|
||||
}
|
||||
|
||||
/// Start the box's daemon, and return once it is spawned.
|
||||
///
|
||||
/// Owned by the box like the compositor, and for the same reason: every
|
||||
/// launch shares what it holds (the box's one Steam sign-in), so no launch's
|
||||
/// stop may reach it. Whether it is *working* is its own to say; this is
|
||||
/// only that it started. ref(d-0085)
|
||||
fn start_daemon(&mut self, daemon: &Daemon) -> Result<(), Failure>;
|
||||
|
||||
/// The daemon's exit, once, after [`Workload::start_daemon`] has returned.
|
||||
/// `None` when there is no daemon or it was already taken.
|
||||
fn daemon_exit(&mut self) -> Option<Exited> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Ask one launch to stop: its whole process group, not its top process.
|
||||
/// Idempotent, and never ends the session by itself. A launch that is not
|
||||
/// running is not signalled: its pid may already belong to something else.
|
||||
@@ -129,8 +143,25 @@ pub struct Process {
|
||||
/// The compositor's exit, until the session takes it. See
|
||||
/// [`Workload::compositor_exit`].
|
||||
compositor_exit: Option<Exited>,
|
||||
/// The box's daemon, if it has one. Like the compositor, not a launch.
|
||||
daemon: Option<Watched>,
|
||||
/// The daemon's exit, until the session takes it.
|
||||
daemon_exit: Option<Exited>,
|
||||
/// Where the daemon is announced to the box's launches: [`DAEMON_MARKER`],
|
||||
/// except in a test.
|
||||
daemon_marker: String,
|
||||
}
|
||||
|
||||
/// The file that says this box has a daemon, holding its name.
|
||||
///
|
||||
/// Written before the daemon is spawned and so before the box is ready, which
|
||||
/// is before any launch can exist: a launch that finds it knows the daemon is
|
||||
/// there to be waited for, and one that does not find it knows there is none to
|
||||
/// wait for. Anything else a launch could look at (the daemon's own state) is
|
||||
/// not there yet in the first moments of a box, and a launch that read its
|
||||
/// absence as "no daemon" would do what the daemon is for by itself.
|
||||
pub const DAEMON_MARKER: &str = "/run/nestri/daemon";
|
||||
|
||||
/// How long a launch's group has to leave after being asked before it is
|
||||
/// killed.
|
||||
pub(crate) const STOP_GRACE: std::time::Duration = std::time::Duration::from_secs(5);
|
||||
@@ -419,6 +450,9 @@ impl Process {
|
||||
cgroup_root: CGROUP_ROOT.to_string(),
|
||||
compositor: None,
|
||||
compositor_exit: None,
|
||||
daemon: None,
|
||||
daemon_exit: None,
|
||||
daemon_marker: DAEMON_MARKER.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -808,6 +842,70 @@ impl Workload for Process {
|
||||
self.compositor_exit.take()
|
||||
}
|
||||
|
||||
fn start_daemon(&mut self, daemon: &Daemon) -> Result<(), Failure> {
|
||||
let exec = &daemon.exec;
|
||||
let Some((program, args)) = exec.argv.split_first() else {
|
||||
return Err(Failure::new(format!("the {} daemon has no command", daemon.name)));
|
||||
};
|
||||
let (uid, gid) = (exec.uid, exec.gid);
|
||||
let runtime = crate::system::runtime_dir(uid, gid).map_err(|error| {
|
||||
Failure::new(format!(
|
||||
"no runtime directory for the {} daemon (uid {uid}): {error}",
|
||||
daemon.name
|
||||
))
|
||||
})?;
|
||||
// Before the spawn: see [`DAEMON_MARKER`]. A failure here refuses the
|
||||
// box, because a launch that could not tell would start its own.
|
||||
if let Some(directory) = std::path::Path::new(&self.daemon_marker).parent() {
|
||||
std::fs::create_dir_all(directory).map_err(|error| {
|
||||
Failure::new(format!("could not make {}: {error}", directory.display()))
|
||||
})?;
|
||||
}
|
||||
std::fs::write(&self.daemon_marker, &daemon.name).map_err(|error| {
|
||||
Failure::new(format!("could not write {}: {error}", self.daemon_marker))
|
||||
})?;
|
||||
|
||||
let mut command = std::process::Command::new(program);
|
||||
command.args(args);
|
||||
command.env_clear();
|
||||
if let Some(cwd) = &exec.cwd {
|
||||
command.current_dir(cwd);
|
||||
}
|
||||
command.envs(environment(exec, Some(&runtime)));
|
||||
// SAFETY: async-signal-safe calls only, nothing allocated.
|
||||
unsafe {
|
||||
command.pre_exec(move || {
|
||||
if libc::setsid() == -1 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
if libc::setgid(gid) != 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
if libc::setuid(uid) != 0 {
|
||||
return Err(io::Error::last_os_error());
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
}
|
||||
let mut watched = self
|
||||
.waiters
|
||||
.watch(|| Ok(command.spawn()?.id() as i32))
|
||||
.map_err(|error| Failure::new(format!("{program} as {uid}:{gid}: {error}")))?;
|
||||
let exit = watched.take_exit().expect("a new watch has its exit");
|
||||
let name = daemon.name.clone();
|
||||
tracing::info!(pid = watched.pid, %name, "the box's daemon is started");
|
||||
self.daemon_exit = Some(Box::pin(async move {
|
||||
exit.await
|
||||
.map_err(|_| io::Error::other(format!("the {name} daemon's exit was not delivered")))
|
||||
}));
|
||||
self.daemon = Some(watched);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn daemon_exit(&mut self) -> Option<Exited> {
|
||||
self.daemon_exit.take()
|
||||
}
|
||||
|
||||
fn stop_launch(&mut self, id: &LaunchId) {
|
||||
if let Some(group) = self.groups.get(id) {
|
||||
// Whether or not the top process is still there: its children may
|
||||
@@ -2082,6 +2180,11 @@ pub mod double {
|
||||
pub compositor_failure: Option<Failure>,
|
||||
/// When the compositor "exits": the session takes this after it starts.
|
||||
pub compositor_exit: Option<oneshot::Receiver<Exit>>,
|
||||
/// Every daemon asked for.
|
||||
pub daemons: Vec<Daemon>,
|
||||
pub daemon_failure: Option<Failure>,
|
||||
/// When the daemon "exits": the session takes this after it starts.
|
||||
pub daemon_exit: Option<oneshot::Receiver<Exit>>,
|
||||
pub mount_failure: Option<Failure>,
|
||||
pub image_failure: Option<Failure>,
|
||||
/// Like `image_failure`, but only for a call that names an image, so a
|
||||
@@ -2227,6 +2330,22 @@ pub mod double {
|
||||
}))
|
||||
}
|
||||
|
||||
fn start_daemon(&mut self, daemon: &Daemon) -> Result<(), Failure> {
|
||||
self.order.push("daemon");
|
||||
self.daemons.push(daemon.clone());
|
||||
match &self.daemon_failure {
|
||||
Some(failure) => Err(failure.clone()),
|
||||
None => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
fn daemon_exit(&mut self) -> Option<Exited> {
|
||||
let rx = self.daemon_exit.take()?;
|
||||
Some(Box::pin(async move {
|
||||
rx.await.map_err(|_| io::Error::other("the daemon was dropped"))
|
||||
}))
|
||||
}
|
||||
|
||||
fn stop_launch(&mut self, id: &LaunchId) {
|
||||
self.stops += 1;
|
||||
self.stopped.push(Some(id.clone()));
|
||||
|
||||
Reference in New Issue
Block a user