Add -D/--daemonize to run -r/--run sessions in the background
Single-fork daemonize: the child calls setsid() itself rather than
re-enabling bwrap's own --new-session, which was previously removed
(and stays that way) because it only detaches the deeply-nested
sandboxed command, leaving bwrap/nsenter/slocker-lite itself still
attached to the original session -- not real daemonization. Calling
setsid() in slocker-lite's own forked child, before it execs into
nsenter/bwrap, detaches the whole chain at once, since exec() never
changes session membership -- confirmed via ps -o sid,pgid,tty against
a running daemonized session.
The child also ignores SIGHUP (confirmed to survive exec() into bwrap,
unlike a real handler, which exec() resets) and redirects stdin to
/dev/null and stdout/stderr to a log file under
$XDG_STATE_HOME/slocker-lite/logs/ (session_log_file_path(), new
sibling to the existing session_pid_file_path() in pid_file.{h,cpp}).
The original process blocks briefly on a pipe until the child reports
the real bwrap pid (or exits without doing so), then prints it and
exits -- keeping "pid" meaning the same thing everywhere in this
codebase (the same one --list-processes/-e/--exec already use), rather
than introducing a separate daemon-supervisor pid. run_bwrap() gained
an on_bwrap_pid_known callback (bwrap.{h,cpp}) for this, invoked
alongside the existing session-lock creation at the same instant.
The daemonized child is what runs run_container()'s entire existing
body afterward, including the unmount/cleanup that already ran once
bwrap exits -- no separate watcher/reaper process.
Testing caught a real bug before this was correct: the log file gets
renamed from its initial (daemon-pid-named) filename to the final
<container_name>-<bwrap-pid>.log once the real pid is known, but the
parent had already been told the pre-rename path and was never updated
-- fixed by re-reporting the path over the same pipe after the rename.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
@@ -37,6 +37,20 @@ Source layout (all under `src/`):
|
||||
(`argv[optind:]`, required — errors out if empty) is collected the same way
|
||||
`-r`'s own command is, sharing that mode's exemption from the "no leftover
|
||||
positional args" check.
|
||||
`-D/--daemonize` (has a short form; `'D'` was free) is a plain boolean flag
|
||||
(`daemonize_flag`, set in its own `case 'D':`, same pattern as
|
||||
`-n/--no-nsenter`) threaded through to `run_container()`, which computes
|
||||
`container_name` *before* `mount_image()` now (moved up from right before
|
||||
the `run_bwrap()` call — it only ever depended on `image_tar`, a parameter
|
||||
available from the start, so this is a pure reordering) and, if daemonizing,
|
||||
calls `daemonize(container_name)` (`daemonize.{h,cpp}`, see below)
|
||||
immediately after: a returned value means this is the original (parent)
|
||||
process (or a hard daemonize failure) — print it and `return` right away;
|
||||
`nullopt` means this is the now-detached child, which falls through into the
|
||||
rest of `run_container()`'s existing body completely unchanged, including
|
||||
the unmount/cleanup that already runs after `run_bwrap()` returns (no
|
||||
separate watcher/reaper — the daemonized child *is* what runs the whole
|
||||
session, start to finish).
|
||||
`inspect_image_command()` implements `-i/--inspect
|
||||
<image.tar>`: prints every `OciImageConfig` field (user/group, exposed ports, env,
|
||||
volumes, default command) without mounting or running the image — extend it
|
||||
@@ -190,6 +204,12 @@ Source layout (all under `src/`):
|
||||
*sandboxed command's own* lookup is affected by a `--env PATH=...` override
|
||||
(as expected — same as overriding `PATH` in any real shell before running a
|
||||
bare command name), and `bwrap`/`nsenter` are always found regardless.
|
||||
`run_bwrap()` also takes an optional `on_bwrap_pid_known` callback, invoked
|
||||
alongside (not instead of) the session-lock-creation lambda, at the exact
|
||||
same `on_start` timing — `-D/--daemonize` (`daemonize.{h,cpp}`, see below)
|
||||
hooks in here via `report_daemon_started()` to learn the real pid at the
|
||||
same instant everything else that needs it does, rather than needing its own
|
||||
separate pid-discovery mechanism.
|
||||
- `priv_drop_helper.cpp` → the separate `slocker-lite-priv-drop` binary (its own
|
||||
`executable()` target in `meson.build`, **built with `-static`**). Deliberately
|
||||
has zero dependencies on the rest of this project (no fmt/spdlog/etc.) and is
|
||||
@@ -250,7 +270,11 @@ Source layout (all under `src/`):
|
||||
`$HOME/.local/state/...` when `XDG_STATE_HOME` is unset/empty — same
|
||||
resolution pattern as `config_file_path()` below, for state instead of
|
||||
config), sanitizing `container_name` first (anything outside `[A-Za-z0-9._-]`
|
||||
→ `_`, since an image name/tag can contain `/` or `:`). `create_session_lock()`
|
||||
→ `_`, since an image name/tag can contain `/` or `:`). `session_log_file_path()`
|
||||
is a sibling resolving to `$XDG_STATE_HOME/slocker-lite/logs/<container_name>-<pid>.log`
|
||||
instead — same sanitization, same `$XDG_STATE_HOME`/`$HOME` fallback, just a
|
||||
different subdirectory and a `.log` extension — used by `daemonize.{h,cpp}`
|
||||
(see below) for `-D/--daemonize`'s log file. `create_session_lock()`
|
||||
creates the file (`O_CREAT|O_WRONLY|O_TRUNC|O_CLOEXEC`, mode 0644 — `O_CLOEXEC`
|
||||
matters: this fd must never leak into the sandboxed command's own fd table),
|
||||
writes the pid as text, and takes an exclusive, non-blocking `flock()` on it —
|
||||
@@ -285,6 +309,56 @@ Source layout (all under `src/`):
|
||||
a check and a later removal. Only files it actually removes are reported back
|
||||
(as `SessionInfo`s with `running=false`); still-locked (running) files are
|
||||
left untouched and not reported.
|
||||
- `daemonize.{h,cpp}` — implements `-D/--daemonize`'s fork/detach mechanics.
|
||||
`daemonize(container_name)` sets up a `pipe2(..., O_CLOEXEC)` pair (so it
|
||||
never leaks into `bwrap`/the sandboxed command, same reasoning as the pid
|
||||
file's own `O_CLOEXEC`) and `fork()`s. The **child** calls `setsid()` —
|
||||
deliberately here, not via re-adding bwrap's own `--new-session` (removed
|
||||
earlier, see the `build_bwrap_args()` comment): `--new-session` only calls
|
||||
`setsid()` for the deeply-nested sandboxed command *inside* bwrap's own
|
||||
namespace setup, leaving the outer `bwrap`/`nsenter`/`slocker-lite` processes
|
||||
still attached to the *original* session and still receiving its signals
|
||||
(e.g. a `SIGHUP` when the controlling terminal closes) — not real
|
||||
daemonization. Calling `setsid()` in our own forked child, *before* it execs
|
||||
into `nsenter`/`bwrap`, detaches the entire chain at once, since `exec()`
|
||||
never changes session membership — confirmed by direct testing
|
||||
(`ps -o pid,sid,pgid,tty`): the daemon child becomes its own session leader
|
||||
with no controlling tty, and `bwrap` (a later descendant) shares that same
|
||||
session, also with no tty. The child also `sigaction()`s `SIGHUP` to
|
||||
`SIG_IGN` (survives the later `exec()` into `nsenter`/`bwrap`, unlike a real
|
||||
handler, which `exec()` resets to default — confirmed by sending `SIGHUP`
|
||||
directly to a running daemonized `bwrap` pid and it staying alive), then
|
||||
redirects stdin to `/dev/null` and stdout/stderr to a log file at
|
||||
`session_log_file_path(container_name, getpid())` (`pid_file.h`) — named
|
||||
after its *own* pid since the real session pid (`bwrap`'s) isn't known yet.
|
||||
If the log directory/file can't be set up at all, that's a **hard** failure
|
||||
here (`_exit(1)`), not best-effort — silently losing the very output
|
||||
`--daemonize` was asked to capture would defeat the point of the flag. The
|
||||
child reports `"LOG <path>\n"` over the pipe immediately (so the parent can
|
||||
show a useful location even on failure) and returns `nullopt` to its caller
|
||||
(`run_container()`, `main.cpp`), which then falls through into the rest of
|
||||
that function's existing body completely unchanged — **the daemonized child
|
||||
is what runs the whole rest of `run_container()`, including the unmount/
|
||||
cleanup that already existed after `run_bwrap()` returns; no separate
|
||||
watcher/reaper process exists**. The **parent** blocks reading the pipe until
|
||||
EOF, returning the accumulated `"LOG "`/`"PID "` lines as a `DaemonizeResult`
|
||||
— the caller then prints it and exits immediately without running any
|
||||
session logic itself. `report_daemon_started(container_name, pid)` (called
|
||||
from `run_bwrap()`'s new `on_bwrap_pid_known` callback — see `bwrap.{h,cpp}`
|
||||
below — the instant the real `bwrap` pid is known) renames the pid-named log
|
||||
file to `<container_name>-<pid>.log`, re-reports the *updated* `"LOG "` line
|
||||
(a real bug caught by testing: the parent's first `"LOG "` line names the
|
||||
pre-rename, daemon-pid-named path — without a second one, the parent would
|
||||
print a stale filename that doesn't match where the file actually ends up),
|
||||
then `"PID <pid>\n"` and closes its own end of the pipe — must happen here,
|
||||
explicitly, rather than waiting for the pipe to close naturally at the end of
|
||||
the (potentially very long) daemon's lifetime, or the parent would block for
|
||||
as long as the session runs instead of returning promptly. The pipe's write
|
||||
fd and the current log path are tracked as private file-scope state in
|
||||
`daemonize.cpp` (matching `process.cpp`'s own `g_foreground_child_pid`
|
||||
pattern for "there's only ever one of these per process" runtime state),
|
||||
since `report_daemon_started()` is called later, from a different function,
|
||||
not threaded explicitly through every call in between.
|
||||
- `exec_session.{h,cpp}` — implements `-e/--exec <pid>`: joins an already-running
|
||||
`-r/--run` session's namespaces via `nsenter` and runs a command inside it in
|
||||
the foreground. `exec_in_session()` first confirms `pid` is a tracked, running
|
||||
@@ -398,8 +472,8 @@ Build directory is `buildDir/` (already configured).
|
||||
(see `priv_drop_helper.cpp` in "Project state")
|
||||
- Run the executable: `./buildDir/slocker-lite -m <image.tar>` (see `--help` for the
|
||||
full flag list: `-m/--mount`, `-r/--run`, `-u/--umount`, `-c/--cleanup`,
|
||||
`-l/--list-images`, `-i/--inspect`, `-e/--exec`, `-n/--no-nsenter`, `--user`, `--group`,
|
||||
`--hostname`, `--env`, `--env-file`, `-v/--volume`, `--list-volumes`, `--delete-volume`,
|
||||
`-l/--list-images`, `-i/--inspect`, `-e/--exec`, `-n/--no-nsenter`, `-D/--daemonize`,
|
||||
`--user`, `--group`, `--hostname`, `--env`, `--env-file`, `-v/--volume`, `--list-volumes`, `--delete-volume`,
|
||||
`--delete-volume-full`, `--list-processes`, `--clean-processes`, `-t/--test`, `--log-level`,
|
||||
`-h/--help`, `-V/--version`)
|
||||
- Run tests: `meson test -C buildDir`
|
||||
|
||||
Reference in New Issue
Block a user