Track running -r/--run sessions with a locked PID file
Each bwrap session is now recorded under $XDG_STATE_HOME/slocker-lite/run/<container-name>-<pid> (falling back to $HOME/.local/state/...), holding an exclusive advisory flock() for as long as it's running -- so any tool can tell a stale leftover file apart from a live session by attempting the same non-blocking flock(). The file is removed once the run ends, on every exit path including a forwarded Ctrl-C. run_process_foreground() gained an optional on_start(pid) callback, fired right after fork() succeeds -- the only point the real bwrap pid is knowable, since exec() (including nsenter handing off to bwrap) never changes it. run_bwrap() uses this to create/release the session lock. The container name comes from read_image_ref(), promoted from a list_oci_images()-only helper to public API in oci_image.h so run_container() can reuse the same name/tag derivation for a single image tar. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
@@ -50,13 +50,22 @@ Source layout (all under `src/`):
|
||||
path first, and passes the resolved list to `run_bwrap()`. `--hostname <name>`
|
||||
(long-option only, no short form) is likewise threaded straight through
|
||||
`run_container()` into `run_bwrap()`/`build_bwrap_args()` (`bwrap.{h,cpp}`) —
|
||||
see there for how/when it actually takes effect.
|
||||
see there for how/when it actually takes effect. `run_container()` also derives
|
||||
a `container_name` for the session-tracking pid file (see `pid_file.{h,cpp}`
|
||||
below): `read_image_ref()` (`oci_image.{h,cpp}`) applied to the single image
|
||||
tar being run, formatted as `name:tag`, falling back to the tar's own filename
|
||||
stem if `read_image_ref()` can't determine one — passed through to
|
||||
`run_bwrap()` alongside everything else.
|
||||
- `oci_image.{h,cpp}` — validates/parses the OCI Image Layout tar (libarchive +
|
||||
nlohmann_json) and extracts layer blobs. `list_oci_images()` scans a directory
|
||||
(non-recursively) for `*.tar`/`*.tar.*` files and, for each valid OCI archive,
|
||||
derives an image name/tag from its `index.json` manifest annotations
|
||||
(`io.containerd.image.name` preferred, else `org.opencontainers.image.ref.name`),
|
||||
falling back to the archive's filename and `"latest"` respectively.
|
||||
derives an image name/tag via `read_image_ref()` from its `index.json` manifest
|
||||
annotations (`io.containerd.image.name` preferred, else
|
||||
`org.opencontainers.image.ref.name`), falling back to the archive's filename and
|
||||
`"latest"` respectively. `read_image_ref()` is public (not just an internal
|
||||
helper of `list_oci_images()`) precisely so `run_container()` (`main.cpp`) can
|
||||
reuse the exact same logic to name a *single* image tar's session pid file (see
|
||||
`pid_file.{h,cpp}` below) instead of duplicating it.
|
||||
`read_oci_image_config()` reads the image config blob referenced by the manifest
|
||||
and extracts `User` (split on `:` into `OciImageConfig::user`/`group`),
|
||||
`ExposedPorts`, `Env`, `Volumes`, and the effective default command
|
||||
@@ -104,7 +113,14 @@ Source layout (all under `src/`):
|
||||
one valid entry, so the helper's own `setuid()` fails cleanly there instead of
|
||||
silently doing nothing. `run_bwrap()` fails fast (returns -1) if the helper can't
|
||||
be found next to this binary when `--user` was requested, rather than silently
|
||||
running the command as root.
|
||||
running the command as root. `run_bwrap()` also takes a `container_name` and
|
||||
tracks the running session with it: it passes a lambda as
|
||||
`run_process_foreground()`'s new `on_start` callback (see `process.{h,cpp}`
|
||||
below) that calls `create_session_lock(container_name, pid)` (`pid_file.{h,cpp}`,
|
||||
see below) the instant the real `bwrap` pid is known, then calls
|
||||
`release_session_lock()` once `run_process_foreground()` returns (covering
|
||||
every exit path — normal, nonzero, or a forwarded-signal exit — since that call
|
||||
always blocks until the child has actually exited).
|
||||
- `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
|
||||
@@ -143,7 +159,36 @@ Source layout (all under `src/`):
|
||||
to the running child and keeps waiting instead of letting the default disposition
|
||||
kill `slocker-lite` itself — without this, Ctrl-C (or `kill`) during `-r`'s `bwrap`
|
||||
run would skip `run_container()`'s unmount/cleanup entirely, leaving the layer
|
||||
imported and/or mounted.
|
||||
imported and/or mounted. `run_process_foreground()` also takes an optional
|
||||
`on_start` callback, invoked with the child's real pid right after `fork()`
|
||||
succeeds (before the signal handlers go up and it blocks in `waitpid()`) — the
|
||||
only point where that pid is knowable, and still accurate even when `argv`
|
||||
itself execs into something else first (e.g. `nsenter` handing off to the final
|
||||
command via its own in-place `execvp()` — a pid never changes across `exec()`).
|
||||
`run_bwrap()` (`bwrap.cpp`) is the one caller that uses it, for session pid-file
|
||||
tracking (see `pid_file.{h,cpp}` below).
|
||||
- `pid_file.{h,cpp}` — tracks one running `-r/--run` session (a live `bwrap`
|
||||
process) as a locked pid file, so an outside process (or a later
|
||||
`slocker-lite` invocation) can tell whether it's still running.
|
||||
`session_pid_file_path()` resolves
|
||||
`$XDG_STATE_HOME/slocker-lite/run/<container_name>-<pid>` (falling back to
|
||||
`$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()`
|
||||
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 —
|
||||
held only by that fd, so its lifetime tracks `slocker-lite`'s own process
|
||||
lifetime (released automatically on any exit, including a crash), which lines
|
||||
up with `bwrap` itself being invoked with `--die-with-parent`. Any external
|
||||
tool can check liveness the same way: attempt the same exclusive non-blocking
|
||||
`flock()` on the file — success means nothing holds it anymore (stale, safe to
|
||||
remove), `EWOULDBLOCK` means a live process still does. `release_session_lock()`
|
||||
closes the fd (releasing the flock immediately) and removes the file. Every
|
||||
failure path here (can't create the directory/file, can't lock, can't remove)
|
||||
is a `spdlog::warn`, never fatal — session tracking is best-effort and must
|
||||
never block or fail `-r/--run` itself.
|
||||
- `config_file.{h,cpp}` — `load_config_file()` reads and parses (via libyaml's
|
||||
document API, `<yaml.h>`) the `global` and `volumes` sections of the local YAML
|
||||
config file located by `config_file_path()` (`$XDG_CONFIG_HOME/slocker-lite/config.yaml`,
|
||||
|
||||
Reference in New Issue
Block a user