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:
2026-08-23 16:41:07 +00:00
parent e7dac86eee
commit f904d33a11
10 changed files with 407 additions and 19 deletions
+77 -3
View File
@@ -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`
+18
View File
@@ -77,6 +77,7 @@ slocker-lite -V|--version
| `-u, --umount <layer-id>` | Unmount a previously mounted layer (the ID printed by `--mount`/`--run`, or from `containers-storage layers`). |
| `-c, --cleanup <layer-id>` | Delete a layer and its ancestor chain from local storage (unmount it first). |
| `-n, --no-nsenter` | With `--run`, bind the mount directly instead of `nsenter`-ing into `fuse-overlayfs`'s namespace. Automatic when running as root; use this to force it off otherwise. |
| `-D, --daemonize` | With `--run`, fork into the background: detaches from the controlling terminal (`setsid()`), ignores `SIGHUP`, and redirects stdin from `/dev/null` and stdout/stderr to a log file under `$XDG_STATE_HOME/slocker-lite/logs/`. Prints the session's pid and log path, then returns — the same pid `--list-processes`/`-e/--exec` use. |
| `--user <user>` | With `--run`, run the command as this user (name or numeric uid) instead of the image's own declared user (or root, if it declares none). Resolved against the image's own `/etc/passwd`. Only takes effect when `--run` executes as root. |
| `--group <group>` | With `--user`, use this group (name or numeric gid) instead of the user's primary group. |
| `--hostname <name>` | With `--run`, set the sandbox's hostname. Only takes effect if the running kernel supports `--unshare-uts`; ignored with a warning otherwise. |
@@ -117,6 +118,9 @@ sudo ./buildDir/slocker-lite -r myimage.tar --user git
# Run with extra environment variables, from flags and/or a file
./buildDir/slocker-lite -r myimage.tar --env FOO=bar --env-file ./app.env
# Run in the background; prints its pid and log file, then returns
./buildDir/slocker-lite -r myimage.tar -D
# List every OCI image tar in a directory
./buildDir/slocker-lite -l ./images
@@ -208,6 +212,20 @@ rather than joining the outer `bwrap` process's own namespaces, so the joined
command sees the container's process tree and hostname too, not just its
filesystem.
`-D/--daemonize` forks and detaches into the background by calling `setsid()`
itself, rather than re-enabling `bwrap`'s own `--new-session` — that flag only
detaches the deeply-nested sandboxed command, leaving `bwrap`/`nsenter` still
attached to the original session. Calling `setsid()` in `slocker-lite`'s own
forked child, before it execs into `nsenter`/`bwrap`, detaches the whole chain
at once (`exec()` never changes session membership), and correctly scopes
`bwrap`'s own `--die-with-parent` to that child. The child ignores `SIGHUP` and
redirects output to a log file before doing anything else; the original,
still-foreground process waits only long enough to learn the real session pid
(the same one `--list-processes`/`-e/--exec` use) before printing it and
returning — the detached child is what runs the entire session afterward,
including the same unmount/cleanup that always ran once the sandboxed command
exits.
See `CLAUDE.md` for the full architecture writeup (file-by-file breakdown, the
reasoning behind each of the above, and known gaps).
+1 -1
View File
@@ -19,7 +19,7 @@ configure_file(output : 'config.h', configuration : conf_data)
slocker_lite = executable('slocker-lite',
['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/pid_file.cpp', 'src/exec_session.cpp', 'src/env_spec.cpp'],
'src/pid_file.cpp', 'src/exec_session.cpp', 'src/env_spec.cpp', 'src/daemonize.cpp'],
include_directories : include_directories('.'),
dependencies : [fmt_dep, catch2_dep, yaml_dep, archive_dep, json_dep, spdlog_dep],
install : true)
+9 -2
View File
@@ -333,7 +333,8 @@ 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,
const std::vector<ResolvedVolumeMount>& volumes, std::optional<ResolvedUser> user,
const std::optional<std::string>& hostname, const std::string& container_name,
const std::vector<std::pair<std::string, std::string>>& extra_env) {
const std::vector<std::pair<std::string, std::string>>& extra_env,
const std::function<void(pid_t)>& on_bwrap_pid_known) {
if (user && !find_priv_drop_helper()) {
spdlog::error("could not find the {} helper next to this binary; --user/--group requires it",
kPrivDropHelperName);
@@ -348,7 +349,13 @@ int run_bwrap(const std::string& root, const std::vector<std::string>& command,
std::optional<SessionLock> session_lock;
int exit_code = run_process_foreground(
*argv, [&](pid_t pid) { session_lock = create_session_lock(container_name, pid); },
*argv,
[&](pid_t pid) {
session_lock = create_session_lock(container_name, pid);
if (on_bwrap_pid_known) {
on_bwrap_pid_known(pid);
}
},
build_sandbox_env(user, extra_env));
if (session_lock) {
+10 -3
View File
@@ -16,11 +16,14 @@
#pragma once
#include <functional>
#include <optional>
#include <string>
#include <utility>
#include <vector>
#include <sys/types.h>
#include "volume_mount.h"
// Probes the running kernel for which Linux namespace types can actually be
@@ -83,9 +86,13 @@ std::vector<std::string> build_bwrap_args(const std::string& root,
// with `extra_env` (from --env/--env-file, see env_spec.h) appended after the
// built-in PATH/HOME/PWD/TERM, and passed to run_process_foreground()'s own env
// override, rather than relying on bwrap's own --clearenv/--setenv (which
// build_bwrap_args() no longer uses). Returns bwrap's exit code, or -1 on
// failure to launch.
// build_bwrap_args() no longer uses). If `on_bwrap_pid_known` is set, it's
// called (alongside, not instead of, the session-lock creation above) at the
// same instant the real pid becomes known -- e.g. -D/--daemonize uses this to
// report that pid back to the still-waiting original process (see
// daemonize.h). 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,
const std::vector<ResolvedVolumeMount>& volumes, std::optional<ResolvedUser> user,
const std::optional<std::string>& hostname, const std::string& container_name,
const std::vector<std::pair<std::string, std::string>>& extra_env);
const std::vector<std::pair<std::string, std::string>>& extra_env,
const std::function<void(pid_t)>& on_bwrap_pid_known = nullptr);
+167
View File
@@ -0,0 +1,167 @@
// 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 "daemonize.h"
#include <fcntl.h>
#include <unistd.h>
#include <cerrno>
#include <csignal>
#include <cstdlib>
#include <cstring>
#include <filesystem>
#include <system_error>
#include <fmt/core.h>
#include <spdlog/spdlog.h>
#include "pid_file.h"
namespace {
// Only ever one daemonize() call per process, same "singleton runtime state"
// pattern as process.cpp's own g_foreground_child_pid.
int g_report_fd = -1;
std::filesystem::path g_log_path;
// Reads whatever the child reports over the pipe until EOF, extracting the
// "LOG <path>" and (if present) "PID <pid>" lines. Parent-side only.
DaemonizeResult read_daemonize_report(int fd) {
std::string buffer;
char chunk[256];
ssize_t n;
while ((n = read(fd, chunk, sizeof(chunk))) > 0) {
buffer.append(chunk, static_cast<size_t>(n));
}
DaemonizeResult result;
size_t pos = 0;
while (pos < buffer.size()) {
size_t eol = buffer.find('\n', pos);
if (eol == std::string::npos) {
break;
}
std::string line = buffer.substr(pos, eol - pos);
pos = eol + 1;
if (line.rfind("LOG ", 0) == 0) {
result.log_path = line.substr(4);
} else if (line.rfind("PID ", 0) == 0) {
result.pid = static_cast<pid_t>(std::atoi(line.c_str() + 4));
}
}
return result;
}
void report_line(const std::string& line) {
if (g_report_fd >= 0) {
write(g_report_fd, line.data(), line.size());
}
}
} // namespace
std::optional<DaemonizeResult> daemonize(const std::string& container_name) {
int fds[2];
if (pipe2(fds, O_CLOEXEC) != 0) {
spdlog::error("failed to set up daemonize pipe: {}", strerror(errno));
return DaemonizeResult{std::nullopt, ""};
}
pid_t pid = fork();
if (pid < 0) {
spdlog::error("failed to fork for --daemonize: {}", strerror(errno));
close(fds[0]);
close(fds[1]);
return DaemonizeResult{std::nullopt, ""};
}
if (pid > 0) {
// Parent: block until the child either reports the real pid or exits
// without ever doing so (a hard failure before bwrap ever started).
close(fds[1]);
auto result = read_daemonize_report(fds[0]);
close(fds[0]);
return result;
}
// Child: detach from the controlling terminal and everything it implies
// (see daemonize.h for why this, not bwrap's own --new-session).
close(fds[0]);
g_report_fd = fds[1];
setsid();
struct sigaction ignore_sighup = {};
ignore_sighup.sa_handler = SIG_IGN;
sigemptyset(&ignore_sighup.sa_mask);
sigaction(SIGHUP, &ignore_sighup, nullptr);
g_log_path = session_log_file_path(container_name, getpid());
std::error_code ec;
std::filesystem::create_directories(g_log_path.parent_path(), ec);
if (ec) {
spdlog::error("failed to create log directory {}: {}", g_log_path.parent_path().string(), ec.message());
_exit(1);
}
int log_fd = open(g_log_path.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0644);
if (log_fd < 0) {
spdlog::error("failed to open log file {}: {}", g_log_path.string(), strerror(errno));
_exit(1);
}
int null_fd = open("/dev/null", O_RDONLY);
if (null_fd >= 0) {
dup2(null_fd, STDIN_FILENO);
close(null_fd);
}
dup2(log_fd, STDOUT_FILENO);
dup2(log_fd, STDERR_FILENO);
close(log_fd);
report_line(fmt::format("LOG {}\n", g_log_path.string()));
return std::nullopt;
}
void report_daemon_started(const std::string& container_name, pid_t pid) {
if (g_report_fd < 0) {
return;
}
auto new_path = session_log_file_path(container_name, pid);
if (new_path != g_log_path) {
std::error_code ec;
std::filesystem::rename(g_log_path, new_path, ec);
if (ec) {
spdlog::warn("failed to rename log file {} to {}: {}", g_log_path.string(), new_path.string(),
ec.message());
} else {
g_log_path = new_path;
// The parent already got an earlier "LOG" line naming the pre-rename
// path (sent before the real pid was known) -- report the updated one
// so it prints the path the file actually ends up at, not a stale one.
report_line(fmt::format("LOG {}\n", g_log_path.string()));
}
}
report_line(fmt::format("PID {}\n", pid));
close(g_report_fd);
g_report_fd = -1;
}
+61
View File
@@ -0,0 +1,61 @@
// 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 <optional>
#include <string>
#include <sys/types.h>
struct DaemonizeResult {
std::optional<pid_t> pid; // the real session pid, if the child got that far
std::string log_path; // where the daemon's output ended up
};
// Forks and detaches the calling process into a background daemon: the child
// calls setsid() -- becoming a new session leader, detaching the *entire*
// nsenter/bwrap/sandboxed-command chain from the controlling terminal at once,
// since exec() never changes session membership. This is deliberately done
// here rather than via bwrap's own --new-session, which only calls setsid()
// for the deeply-nested sandboxed command itself, leaving bwrap/nsenter/this
// process still attached to the original session (and still receiving its
// signals, e.g. a SIGHUP when the controlling terminal closes) -- not real
// daemonization. The child also ignores SIGHUP (inherited across the later
// exec() into nsenter/bwrap, since SIG_IGN -- unlike a real handler -- survives
// exec()) and redirects stdin to /dev/null and stdout/stderr to a log file
// under $XDG_STATE_HOME/slocker-lite/logs/ (session_log_file_path(), see
// pid_file.h), initially named after its own pid since the real session pid
// isn't known yet (see report_daemon_started() below). If the log
// file/directory can't be set up at all, that's a hard failure, not
// best-effort -- silently losing the very output --daemonize was asked to
// capture would defeat the point of the flag.
//
// Returns nullopt in the child -- the caller should continue on to do the
// real work. In the parent, blocks until the child either reports the real
// session pid (via report_daemon_started()) or exits without ever doing so,
// then returns a result describing what happened -- the caller should print
// it and exit immediately, without running any further session logic.
std::optional<DaemonizeResult> daemonize(const std::string& container_name);
// Called from the daemonized child once the real session pid is known (from
// run_bwrap()'s on_bwrap_pid_known callback): renames the pid-named log file
// to <container_name>-<pid>.log and reports success back to the parent still
// blocked in daemonize(), which then returns and can exit. A failed rename is
// only a warning -- the daemon keeps running under the old filename, matching
// this project's established best-effort-on-auxiliary-state-tracking
// convention (e.g. pid_file.cpp's own lock/removal warnings).
void report_daemon_started(const std::string& container_name, pid_t pid);
+48 -8
View File
@@ -18,6 +18,7 @@
#include <array>
#include <cstdlib>
#include <filesystem>
#include <functional>
#include <getopt.h>
#include <optional>
#include <string>
@@ -34,6 +35,7 @@
#include "config.h"
#include "config_file.h"
#include "containers_storage.h"
#include "daemonize.h"
#include "env_spec.h"
#include "exec_session.h"
#include "oci_image.h"
@@ -81,7 +83,7 @@ constexpr int kCleanProcessesOpt = 264;
constexpr int kEnvOpt = 265;
constexpr int kEnvFileOpt = 266;
constexpr std::array<struct option, 24> kLongOptions = {{
constexpr std::array<struct option, 25> kLongOptions = {{
{"help", no_argument, nullptr, 'h'},
{"version", no_argument, nullptr, 'V'},
{"test", no_argument, nullptr, 't'},
@@ -91,6 +93,7 @@ constexpr std::array<struct option, 24> kLongOptions = {{
{"run", required_argument, nullptr, 'r'},
{"cleanup", required_argument, nullptr, 'c'},
{"no-nsenter", no_argument, nullptr, 'n'},
{"daemonize", no_argument, nullptr, 'D'},
{"list-images", required_argument, nullptr, 'l'},
{"user", required_argument, nullptr, kUserOpt},
{"group", required_argument, nullptr, kGroupOpt},
@@ -144,6 +147,14 @@ void print_usage(const char* prog) {
" (this is automatic when running as root, where\n"
" the mount is already directly visible; pass\n"
" this to force it off otherwise)\n"
" -D, --daemonize with --run, fork into the background: detaches\n"
" from the controlling terminal (setsid()) and\n"
" ignores SIGHUP, redirecting stdin from /dev/null\n"
" and stdout/stderr to a log file under\n"
" $XDG_STATE_HOME/slocker-lite/logs/. Prints the\n"
" session's pid and log path, then returns\n"
" immediately -- the same pid --list-processes/\n"
" -e/--exec use\n"
" --user <user> with --run, run the command as this user (name\n"
" or numeric uid) instead of the image's own\n"
" declared user (or root, if it declares none),\n"
@@ -540,7 +551,31 @@ int run_container(const std::filesystem::path& image_tar,
const std::optional<std::string>& user, const std::optional<std::string>& group,
const std::optional<std::string>& hostname,
const std::vector<std::pair<std::string, std::string>>& volume_specs,
const std::vector<EnvSpec>& env_specs, const AppConfig& app_config) {
const std::vector<EnvSpec>& env_specs, bool daemonize_flag, const AppConfig& app_config) {
// Only depends on image_tar, so this can run before mount_image() -- moved
// up here (rather than right before the run_bwrap() call, as before) so
// daemonize() below can use the real container name for the log file from
// its very first line, not just after a later rename.
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();
if (daemonize_flag) {
auto result = daemonize(container_name);
if (result) {
// Parent (or a hard daemonize failure before ever forking) -- report
// and return immediately. The child falls through below instead.
if (result->pid) {
fmt::print("started in background: pid {}, log: {}\n", *result->pid, result->log_path);
return 0;
}
spdlog::error("failed to start in background{}", result->log_path.empty()
? ""
: fmt::format(" (see log: {})", result->log_path));
return 1;
}
}
auto mounted = mount_image(image_tar);
if (!mounted) {
return 1;
@@ -606,14 +641,15 @@ int run_container(const std::filesystem::path& image_tar,
: 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();
std::function<void(pid_t)> on_bwrap_pid_known;
if (daemonize_flag) {
on_bwrap_pid_known = [&](pid_t pid) { report_daemon_started(container_name, pid); };
}
int exit_code = -1;
if (ok) {
exit_code = run_bwrap(mounted->merged_path, command, use_nsenter, volume_mounts, resolved_user, hostname,
container_name, *resolved_env);
container_name, *resolved_env, on_bwrap_pid_known);
if (exit_code < 0) {
spdlog::error("failed to run bwrap");
}
@@ -644,6 +680,7 @@ int main(int argc, char* argv[]) {
Mode mode = Mode::kNone;
std::string mode_arg;
bool disable_nsenter = false;
bool daemonize_flag = false;
std::optional<std::string> user_flag;
std::optional<std::string> group_flag;
std::optional<std::string> hostname_flag;
@@ -652,7 +689,7 @@ int main(int argc, char* argv[]) {
opterr = 0;
int opt;
while ((opt = getopt_long(argc, argv, ":hVtm:u:r:c:nl:v:i:e:", kLongOptions.data(), nullptr)) != -1) {
while ((opt = getopt_long(argc, argv, ":hVtm:u:r:c:nl:v:i:e:D", kLongOptions.data(), nullptr)) != -1) {
switch (opt) {
case 'h':
print_usage(argv[0]);
@@ -744,6 +781,9 @@ int main(int argc, char* argv[]) {
case 'n':
disable_nsenter = true;
break;
case 'D':
daemonize_flag = true;
break;
case kLogLevelOpt:
if (!apply_log_level(optarg)) {
return 1;
@@ -861,7 +901,7 @@ int main(int argc, char* argv[]) {
spdlog::debug("running as root; skipping nsenter (the mount is already directly visible)");
}
return run_container(mode_arg, command, use_nsenter, user_flag, group_flag, hostname_flag, volume_specs,
env_specs, *config);
env_specs, daemonize_flag, *config);
}
auto mounted = mount_image(mode_arg);
+10 -2
View File
@@ -44,7 +44,7 @@ std::string sanitize_for_filename(std::string_view name) {
return result.empty() ? "container" : result;
}
std::filesystem::path session_run_dir() {
std::filesystem::path xdg_state_dir() {
const char* xdg_state_home = std::getenv("XDG_STATE_HOME");
std::filesystem::path state_home;
if (xdg_state_home && *xdg_state_home) {
@@ -53,7 +53,11 @@ std::filesystem::path session_run_dir() {
const char* home = std::getenv("HOME");
state_home = std::filesystem::path(home ? home : "") / ".local" / "state";
}
return state_home / "slocker-lite" / "run";
return state_home / "slocker-lite";
}
std::filesystem::path session_run_dir() {
return xdg_state_dir() / "run";
}
struct ParsedSessionFile {
@@ -102,6 +106,10 @@ std::filesystem::path session_pid_file_path(std::string_view container_name, pid
return session_run_dir() / fmt::format("{}-{}", sanitize_for_filename(container_name), pid);
}
std::filesystem::path session_log_file_path(std::string_view container_name, pid_t pid) {
return xdg_state_dir() / "logs" / fmt::format("{}-{}.log", 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);
+6
View File
@@ -45,6 +45,12 @@ struct SessionLock {
// names/tags can contain '/' (registry paths) or ':'.
std::filesystem::path session_pid_file_path(std::string_view container_name, pid_t pid);
// $XDG_STATE_HOME/slocker-lite/logs/<container_name>-<pid>.log (or the
// $HOME/.local/state/... fallback), sibling to session_pid_file_path() above --
// same sanitization, same directory resolution, just a different subdirectory
// and a .log extension. Used by daemonize.{h,cpp} for -D/--daemonize's log file.
std::filesystem::path session_log_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