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>`
|
path first, and passes the resolved list to `run_bwrap()`. `--hostname <name>`
|
||||||
(long-option only, no short form) is likewise threaded straight through
|
(long-option only, no short form) is likewise threaded straight through
|
||||||
`run_container()` into `run_bwrap()`/`build_bwrap_args()` (`bwrap.{h,cpp}`) —
|
`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 +
|
- `oci_image.{h,cpp}` — validates/parses the OCI Image Layout tar (libarchive +
|
||||||
nlohmann_json) and extracts layer blobs. `list_oci_images()` scans a directory
|
nlohmann_json) and extracts layer blobs. `list_oci_images()` scans a directory
|
||||||
(non-recursively) for `*.tar`/`*.tar.*` files and, for each valid OCI archive,
|
(non-recursively) for `*.tar`/`*.tar.*` files and, for each valid OCI archive,
|
||||||
derives an image name/tag from its `index.json` manifest annotations
|
derives an image name/tag via `read_image_ref()` from its `index.json` manifest
|
||||||
(`io.containerd.image.name` preferred, else `org.opencontainers.image.ref.name`),
|
annotations (`io.containerd.image.name` preferred, else
|
||||||
falling back to the archive's filename and `"latest"` respectively.
|
`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
|
`read_oci_image_config()` reads the image config blob referenced by the manifest
|
||||||
and extracts `User` (split on `:` into `OciImageConfig::user`/`group`),
|
and extracts `User` (split on `:` into `OciImageConfig::user`/`group`),
|
||||||
`ExposedPorts`, `Env`, `Volumes`, and the effective default command
|
`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
|
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
|
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
|
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
|
- `priv_drop_helper.cpp` → the separate `slocker-lite-priv-drop` binary (its own
|
||||||
`executable()` target in `meson.build`, **built with `-static`**). Deliberately
|
`executable()` target in `meson.build`, **built with `-static`**). Deliberately
|
||||||
has zero dependencies on the rest of this project (no fmt/spdlog/etc.) and is
|
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
|
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`
|
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
|
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
|
- `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
|
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`,
|
config file located by `config_file_path()` (`$XDG_CONFIG_HOME/slocker-lite/config.yaml`,
|
||||||
|
|||||||
@@ -165,6 +165,13 @@ there, `slocker-lite` instead bind-mounts a separate, statically-linked helper
|
|||||||
(`slocker-lite-priv-drop`) into the sandbox and routes the command through it to drop
|
(`slocker-lite-priv-drop`) into the sandbox and routes the command through it to drop
|
||||||
privileges before exec.
|
privileges before exec.
|
||||||
|
|
||||||
|
While a `-r/--run` session is active, its `bwrap` process is tracked as a locked
|
||||||
|
PID file under `$XDG_STATE_HOME/slocker-lite/run/` (falling back to
|
||||||
|
`$HOME/.local/state/...`), named after the image and its PID so the same image can
|
||||||
|
be run concurrently without collisions. The file is removed automatically once the
|
||||||
|
run ends; any tool can check whether a session is still alive by attempting the
|
||||||
|
same exclusive, non-blocking `flock()` on its file.
|
||||||
|
|
||||||
See `CLAUDE.md` for the full architecture writeup (file-by-file breakdown, the
|
See `CLAUDE.md` for the full architecture writeup (file-by-file breakdown, the
|
||||||
reasoning behind each of the above, and known gaps).
|
reasoning behind each of the above, and known gaps).
|
||||||
|
|
||||||
|
|||||||
+2
-1
@@ -18,7 +18,8 @@ configure_file(output : 'config.h', configuration : conf_data)
|
|||||||
|
|
||||||
slocker_lite = executable('slocker-lite',
|
slocker_lite = executable('slocker-lite',
|
||||||
['src/main.cpp', 'src/process.cpp', 'src/oci_image.cpp', 'src/containers_storage.cpp',
|
['src/main.cpp', 'src/process.cpp', 'src/oci_image.cpp', 'src/containers_storage.cpp',
|
||||||
'src/bwrap.cpp', 'src/user_spec.cpp', 'src/config_file.cpp', 'src/volume_mount.cpp'],
|
'src/bwrap.cpp', 'src/user_spec.cpp', 'src/config_file.cpp', 'src/volume_mount.cpp',
|
||||||
|
'src/pid_file.cpp'],
|
||||||
include_directories : include_directories('.'),
|
include_directories : include_directories('.'),
|
||||||
dependencies : [fmt_dep, catch2_dep, yaml_dep, archive_dep, json_dep, spdlog_dep],
|
dependencies : [fmt_dep, catch2_dep, yaml_dep, archive_dep, json_dep, spdlog_dep],
|
||||||
install : true)
|
install : true)
|
||||||
|
|||||||
+11
-2
@@ -32,6 +32,7 @@
|
|||||||
#include <fmt/core.h>
|
#include <fmt/core.h>
|
||||||
#include <spdlog/spdlog.h>
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
#include "pid_file.h"
|
||||||
#include "process.h"
|
#include "process.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -305,7 +306,7 @@ std::optional<std::vector<std::string>> wrap_for_root_namespace(const std::strin
|
|||||||
|
|
||||||
int run_bwrap(const std::string& root, const std::vector<std::string>& command, bool use_nsenter,
|
int run_bwrap(const std::string& root, const std::vector<std::string>& command, bool use_nsenter,
|
||||||
const std::vector<ResolvedVolumeMount>& volumes, std::optional<ResolvedUser> user,
|
const std::vector<ResolvedVolumeMount>& volumes, std::optional<ResolvedUser> user,
|
||||||
const std::optional<std::string>& hostname) {
|
const std::optional<std::string>& hostname, const std::string& container_name) {
|
||||||
if (user && !find_priv_drop_helper()) {
|
if (user && !find_priv_drop_helper()) {
|
||||||
spdlog::error("could not find the {} helper next to this binary; --user/--group requires it",
|
spdlog::error("could not find the {} helper next to this binary; --user/--group requires it",
|
||||||
kPrivDropHelperName);
|
kPrivDropHelperName);
|
||||||
@@ -318,5 +319,13 @@ int run_bwrap(const std::string& root, const std::vector<std::string>& command,
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
return run_process_foreground(*argv);
|
std::optional<SessionLock> session_lock;
|
||||||
|
int exit_code = run_process_foreground(
|
||||||
|
*argv, [&](pid_t pid) { session_lock = create_session_lock(container_name, pid); });
|
||||||
|
|
||||||
|
if (session_lock) {
|
||||||
|
release_session_lock(*session_lock);
|
||||||
|
}
|
||||||
|
|
||||||
|
return exit_code;
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-3
@@ -75,8 +75,10 @@ std::vector<std::string> build_bwrap_args(const std::string& root,
|
|||||||
// If `user` is set, this process's own binary is bind-mounted into the sandbox and
|
// If `user` is set, this process's own binary is bind-mounted into the sandbox and
|
||||||
// used to drop privileges to that uid/gid before running `command` -- see
|
// used to drop privileges to that uid/gid before running `command` -- see
|
||||||
// build_bwrap_args(). `hostname`, if set, is forwarded to build_bwrap_args() --
|
// build_bwrap_args(). `hostname`, if set, is forwarded to build_bwrap_args() --
|
||||||
// see there for when it does/doesn't take effect. Returns bwrap's exit code, or -1
|
// see there for when it does/doesn't take effect. While bwrap is running,
|
||||||
// on failure to launch.
|
// `container_name` (paired with its actual pid) is recorded as a locked session
|
||||||
|
// pid file under $XDG_STATE_HOME (see pid_file.h) -- removed again once it exits.
|
||||||
|
// Returns bwrap's exit code, or -1 on failure to launch.
|
||||||
int run_bwrap(const std::string& root, const std::vector<std::string>& command, bool use_nsenter,
|
int run_bwrap(const std::string& root, const std::vector<std::string>& command, bool use_nsenter,
|
||||||
const std::vector<ResolvedVolumeMount>& volumes, std::optional<ResolvedUser> user,
|
const std::vector<ResolvedVolumeMount>& volumes, std::optional<ResolvedUser> user,
|
||||||
const std::optional<std::string>& hostname);
|
const std::optional<std::string>& hostname, const std::string& container_name);
|
||||||
|
|||||||
+6
-1
@@ -522,9 +522,14 @@ int run_container(const std::filesystem::path& image_tar,
|
|||||||
: std::vector<std::string>{"/bin/sh"};
|
: std::vector<std::string>{"/bin/sh"};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto image_ref = read_image_ref(image_tar);
|
||||||
|
std::string container_name =
|
||||||
|
image_ref ? fmt::format("{}:{}", image_ref->name, image_ref->tag) : image_tar.stem().string();
|
||||||
|
|
||||||
int exit_code = -1;
|
int exit_code = -1;
|
||||||
if (ok) {
|
if (ok) {
|
||||||
exit_code = run_bwrap(mounted->merged_path, command, use_nsenter, volume_mounts, resolved_user, hostname);
|
exit_code = run_bwrap(mounted->merged_path, command, use_nsenter, volume_mounts, resolved_user, hostname,
|
||||||
|
container_name);
|
||||||
if (exit_code < 0) {
|
if (exit_code < 0) {
|
||||||
spdlog::error("failed to run bwrap");
|
spdlog::error("failed to run bwrap");
|
||||||
}
|
}
|
||||||
|
|||||||
+51
-51
@@ -135,57 +135,6 @@ ParsedRef parse_image_ref(const std::string& ref) {
|
|||||||
return {"", ref};
|
return {"", ref};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::optional<OciImageRef> read_image_ref(const std::filesystem::path& tar_path) {
|
|
||||||
auto layout = read_entry_to_string(tar_path, "oci-layout");
|
|
||||||
if (!layout) {
|
|
||||||
spdlog::debug("{}: not an OCI image tar (missing oci-layout)", tar_path.string());
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto index_content = read_entry_to_string(tar_path, "index.json");
|
|
||||||
if (!index_content) {
|
|
||||||
spdlog::debug("{}: not an OCI image tar (missing index.json)", tar_path.string());
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
json index;
|
|
||||||
try {
|
|
||||||
index = json::parse(*index_content);
|
|
||||||
} catch (const json::parse_error& e) {
|
|
||||||
spdlog::debug("{}: index.json is not valid JSON: {}", tar_path.string(), e.what());
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
json manifest_entry;
|
|
||||||
bool found = false;
|
|
||||||
for (const auto& m : index.value("manifests", json::array())) {
|
|
||||||
if (m.value("mediaType", "") == "application/vnd.oci.image.manifest.v1+json") {
|
|
||||||
manifest_entry = m;
|
|
||||||
found = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!found) {
|
|
||||||
spdlog::debug("{}: index.json has no OCI image manifest entry", tar_path.string());
|
|
||||||
return std::nullopt;
|
|
||||||
}
|
|
||||||
|
|
||||||
OciImageRef ref;
|
|
||||||
ref.path = tar_path;
|
|
||||||
|
|
||||||
auto annotations = manifest_entry.value("annotations", json::object());
|
|
||||||
std::string annotation_ref = annotations.value("io.containerd.image.name", "");
|
|
||||||
if (annotation_ref.empty()) {
|
|
||||||
annotation_ref = annotations.value("org.opencontainers.image.ref.name", "");
|
|
||||||
}
|
|
||||||
|
|
||||||
ParsedRef parsed = annotation_ref.empty() ? ParsedRef{} : parse_image_ref(annotation_ref);
|
|
||||||
ref.name = parsed.name.empty() ? archive_basename(tar_path) : parsed.name;
|
|
||||||
ref.tag = parsed.tag.empty() ? "latest" : parsed.tag;
|
|
||||||
|
|
||||||
return ref;
|
|
||||||
}
|
|
||||||
|
|
||||||
struct OciManifest {
|
struct OciManifest {
|
||||||
std::string digest;
|
std::string digest;
|
||||||
json data;
|
json data;
|
||||||
@@ -284,6 +233,57 @@ std::optional<OciExposedPort> parse_exposed_port(const std::string& key) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
std::optional<OciImageRef> read_image_ref(const std::filesystem::path& tar_path) {
|
||||||
|
auto layout = read_entry_to_string(tar_path, "oci-layout");
|
||||||
|
if (!layout) {
|
||||||
|
spdlog::debug("{}: not an OCI image tar (missing oci-layout)", tar_path.string());
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto index_content = read_entry_to_string(tar_path, "index.json");
|
||||||
|
if (!index_content) {
|
||||||
|
spdlog::debug("{}: not an OCI image tar (missing index.json)", tar_path.string());
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
json index;
|
||||||
|
try {
|
||||||
|
index = json::parse(*index_content);
|
||||||
|
} catch (const json::parse_error& e) {
|
||||||
|
spdlog::debug("{}: index.json is not valid JSON: {}", tar_path.string(), e.what());
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
json manifest_entry;
|
||||||
|
bool found = false;
|
||||||
|
for (const auto& m : index.value("manifests", json::array())) {
|
||||||
|
if (m.value("mediaType", "") == "application/vnd.oci.image.manifest.v1+json") {
|
||||||
|
manifest_entry = m;
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!found) {
|
||||||
|
spdlog::debug("{}: index.json has no OCI image manifest entry", tar_path.string());
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
OciImageRef ref;
|
||||||
|
ref.path = tar_path;
|
||||||
|
|
||||||
|
auto annotations = manifest_entry.value("annotations", json::object());
|
||||||
|
std::string annotation_ref = annotations.value("io.containerd.image.name", "");
|
||||||
|
if (annotation_ref.empty()) {
|
||||||
|
annotation_ref = annotations.value("org.opencontainers.image.ref.name", "");
|
||||||
|
}
|
||||||
|
|
||||||
|
ParsedRef parsed = annotation_ref.empty() ? ParsedRef{} : parse_image_ref(annotation_ref);
|
||||||
|
ref.name = parsed.name.empty() ? archive_basename(tar_path) : parsed.name;
|
||||||
|
ref.tag = parsed.tag.empty() ? "latest" : parsed.tag;
|
||||||
|
|
||||||
|
return ref;
|
||||||
|
}
|
||||||
|
|
||||||
std::string oci_digest_hex(std::string_view digest) {
|
std::string oci_digest_hex(std::string_view digest) {
|
||||||
constexpr std::string_view prefix = "sha256:";
|
constexpr std::string_view prefix = "sha256:";
|
||||||
if (digest.substr(0, prefix.size()) == prefix) {
|
if (digest.substr(0, prefix.size()) == prefix) {
|
||||||
|
|||||||
@@ -49,6 +49,14 @@ struct OciImageRef {
|
|||||||
std::filesystem::path path; // the archive file this was found in
|
std::filesystem::path path; // the archive file this was found in
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Determines a single image tar's own name/tag the same way list_oci_images() does
|
||||||
|
// for each file it scans: from its index.json manifest annotations
|
||||||
|
// (io.containerd.image.name preferred, else org.opencontainers.image.ref.name),
|
||||||
|
// falling back to the archive's filename (name) and "latest" (tag) when unset or
|
||||||
|
// unparseable. Returns nullopt if `tar_path` isn't a valid OCI Image Layout tar
|
||||||
|
// with at least one image manifest entry.
|
||||||
|
std::optional<OciImageRef> read_image_ref(const std::filesystem::path& tar_path);
|
||||||
|
|
||||||
// Scans `dir` (non-recursively) for files matching *.tar or *.tar.*, and for each one
|
// Scans `dir` (non-recursively) for files matching *.tar or *.tar.*, and for each one
|
||||||
// that's a valid OCI Image Layout archive, determines an image name/tag from its
|
// that's a valid OCI Image Layout archive, determines an image name/tag from its
|
||||||
// index.json manifest annotations (io.containerd.image.name or
|
// index.json manifest annotations (io.containerd.image.name or
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
// Copyright (C) 2026 Viorel Munteanu
|
||||||
|
//
|
||||||
|
// This program is free software; you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation; either version 2 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License along
|
||||||
|
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
#include "pid_file.h"
|
||||||
|
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <sys/file.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <cctype>
|
||||||
|
#include <cerrno>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <cstring>
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
|
#include <fmt/core.h>
|
||||||
|
#include <spdlog/spdlog.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
std::string sanitize_for_filename(std::string_view name) {
|
||||||
|
std::string result;
|
||||||
|
result.reserve(name.size());
|
||||||
|
for (char c : name) {
|
||||||
|
if (std::isalnum(static_cast<unsigned char>(c)) || c == '-' || c == '_' || c == '.') {
|
||||||
|
result += c;
|
||||||
|
} else {
|
||||||
|
result += '_';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.empty() ? "container" : result;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path session_run_dir() {
|
||||||
|
const char* xdg_state_home = std::getenv("XDG_STATE_HOME");
|
||||||
|
std::filesystem::path state_home;
|
||||||
|
if (xdg_state_home && *xdg_state_home) {
|
||||||
|
state_home = xdg_state_home;
|
||||||
|
} else {
|
||||||
|
const char* home = std::getenv("HOME");
|
||||||
|
state_home = std::filesystem::path(home ? home : "") / ".local" / "state";
|
||||||
|
}
|
||||||
|
return state_home / "slocker-lite" / "run";
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
std::filesystem::path session_pid_file_path(std::string_view container_name, pid_t pid) {
|
||||||
|
return session_run_dir() / fmt::format("{}-{}", sanitize_for_filename(container_name), pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<SessionLock> create_session_lock(std::string_view container_name, pid_t pid) {
|
||||||
|
auto path = session_pid_file_path(container_name, pid);
|
||||||
|
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::create_directories(path.parent_path(), ec);
|
||||||
|
if (ec) {
|
||||||
|
spdlog::warn("failed to create session state directory {}: {}", path.parent_path().string(),
|
||||||
|
ec.message());
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fd = open(path.c_str(), O_CREAT | O_WRONLY | O_TRUNC | O_CLOEXEC, 0644);
|
||||||
|
if (fd < 0) {
|
||||||
|
spdlog::warn("failed to create session pid file {}: {}", path.string(), strerror(errno));
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flock(fd, LOCK_EX | LOCK_NB) != 0) {
|
||||||
|
spdlog::warn("failed to lock session pid file {}: {}", path.string(), strerror(errno));
|
||||||
|
close(fd);
|
||||||
|
return std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string contents = fmt::format("{}\n", pid);
|
||||||
|
if (write(fd, contents.data(), contents.size()) < 0) {
|
||||||
|
spdlog::warn("failed to write session pid file {}: {}", path.string(), strerror(errno));
|
||||||
|
}
|
||||||
|
|
||||||
|
return SessionLock{path, fd};
|
||||||
|
}
|
||||||
|
|
||||||
|
void release_session_lock(const SessionLock& lock) {
|
||||||
|
if (lock.fd >= 0) {
|
||||||
|
close(lock.fd);
|
||||||
|
}
|
||||||
|
std::error_code ec;
|
||||||
|
std::filesystem::remove(lock.path, ec);
|
||||||
|
if (ec) {
|
||||||
|
spdlog::warn("failed to remove session pid file {}: {}", lock.path.string(), ec.message());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
// Copyright (C) 2026 Viorel Munteanu
|
||||||
|
//
|
||||||
|
// This program is free software; you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU General Public License as published by
|
||||||
|
// the Free Software Foundation; either version 2 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
//
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU General Public License for more details.
|
||||||
|
//
|
||||||
|
// You should have received a copy of the GNU General Public License along
|
||||||
|
// with this program; if not, write to the Free Software Foundation, Inc.,
|
||||||
|
// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
// Tracks one running -r/--run session (a live bwrap process) as a PID file with an
|
||||||
|
// advisory flock() held for as long as this process is running it -- so a *different*
|
||||||
|
// process can tell a stale leftover file apart from a genuinely still-running
|
||||||
|
// session: attempt the same exclusive, non-blocking flock() on the same file --
|
||||||
|
// succeeding means nothing holds it anymore (stale, safe to remove), failing
|
||||||
|
// (EWOULDBLOCK) means a live process still does. The lock is held only by `fd`
|
||||||
|
// (opened O_CLOEXEC), so it's never visible to the sandboxed command itself, and
|
||||||
|
// its lifetime tracks this process's own lifetime -- released automatically if
|
||||||
|
// this process exits for any reason, including a crash.
|
||||||
|
struct SessionLock {
|
||||||
|
std::filesystem::path path;
|
||||||
|
int fd = -1;
|
||||||
|
};
|
||||||
|
|
||||||
|
// $XDG_STATE_HOME/slocker-lite/run/<container_name>-<pid>, or
|
||||||
|
// $HOME/.local/state/slocker-lite/run/<container_name>-<pid> if XDG_STATE_HOME is
|
||||||
|
// unset/empty (mirrors config_file_path()'s XDG_CONFIG_HOME pattern). Characters in
|
||||||
|
// container_name outside [A-Za-z0-9._-] are replaced with '_', since image
|
||||||
|
// names/tags can contain '/' (registry paths) or ':'.
|
||||||
|
std::filesystem::path session_pid_file_path(std::string_view container_name, pid_t pid);
|
||||||
|
|
||||||
|
// Creates the pid file for (container_name, pid) (creating its parent directory if
|
||||||
|
// needed), writes `pid` as text, and takes an exclusive advisory flock() on it (see
|
||||||
|
// SessionLock above). Returns nullopt (logging a warning, never fatal -- session
|
||||||
|
// tracking is best-effort and must never block or fail -r/--run itself) if the
|
||||||
|
// directory/file can't be created or the lock can't be taken.
|
||||||
|
std::optional<SessionLock> create_session_lock(std::string_view container_name, pid_t pid);
|
||||||
|
|
||||||
|
// Closes fd (releasing the flock immediately) and removes the file. Best-effort:
|
||||||
|
// logs a warning on failure, never treated as fatal, mirroring run_container()'s
|
||||||
|
// own unmount/cleanup-failure handling.
|
||||||
|
void release_session_lock(const SessionLock& lock);
|
||||||
+5
-1
@@ -108,7 +108,8 @@ ProcessResult run_process(const std::vector<std::string>& argv) {
|
|||||||
return {exit_code, output};
|
return {exit_code, output};
|
||||||
}
|
}
|
||||||
|
|
||||||
int run_process_foreground(const std::vector<std::string>& argv) {
|
int run_process_foreground(const std::vector<std::string>& argv,
|
||||||
|
const std::function<void(pid_t)>& on_start) {
|
||||||
spdlog::debug("running external command: {}", fmt::join(argv, " "));
|
spdlog::debug("running external command: {}", fmt::join(argv, " "));
|
||||||
|
|
||||||
pid_t pid = fork();
|
pid_t pid = fork();
|
||||||
@@ -125,6 +126,9 @@ int run_process_foreground(const std::vector<std::string>& argv) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
g_foreground_child_pid = pid;
|
g_foreground_child_pid = pid;
|
||||||
|
if (on_start) {
|
||||||
|
on_start(pid);
|
||||||
|
}
|
||||||
|
|
||||||
struct sigaction action = {};
|
struct sigaction action = {};
|
||||||
action.sa_handler = forward_signal_to_foreground_child;
|
action.sa_handler = forward_signal_to_foreground_child;
|
||||||
|
|||||||
+11
-2
@@ -17,11 +17,14 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
|
#include <functional>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
struct ProcessResult {
|
struct ProcessResult {
|
||||||
int exit_code;
|
int exit_code;
|
||||||
std::string stdout_output;
|
std::string stdout_output;
|
||||||
@@ -33,8 +36,14 @@ ProcessResult run_process(const std::vector<std::string>& argv);
|
|||||||
|
|
||||||
// Runs argv[0] with the given arguments via fork/execvp, with stdin/stdout/stderr
|
// Runs argv[0] with the given arguments via fork/execvp, with stdin/stdout/stderr
|
||||||
// all inherited from the caller (no output capture) for interactive/foreground use.
|
// all inherited from the caller (no output capture) for interactive/foreground use.
|
||||||
// Returns the exit code, or -1 if fork or exec failed.
|
// If `on_start` is set, it's called with the child's pid immediately after fork()
|
||||||
int run_process_foreground(const std::vector<std::string>& argv);
|
// succeeds and before this blocks in waitpid() -- e.g. so a caller can record the
|
||||||
|
// real pid of what it just launched. This stays accurate even when argv itself
|
||||||
|
// execs into something else before the real target (e.g. nsenter handing off to
|
||||||
|
// the final command), since exec() never changes the pid. Returns the exit code,
|
||||||
|
// or -1 if fork or exec failed (on_start is not called in that case).
|
||||||
|
int run_process_foreground(const std::vector<std::string>& argv,
|
||||||
|
const std::function<void(pid_t)>& on_start = nullptr);
|
||||||
|
|
||||||
// Searches $PATH for an executable regular file named `name`, in PATH order.
|
// Searches $PATH for an executable regular file named `name`, in PATH order.
|
||||||
// Returns its full path, or nullopt if not found.
|
// Returns its full path, or nullopt if not found.
|
||||||
|
|||||||
Reference in New Issue
Block a user