Add --list-processes to list running -r/--run sessions

Lists every session pid file found under
$XDG_STATE_HOME/slocker-lite/run/, showing pid, container name, and
status (running or exited). Status is determined the same way any
external tool could check it: a non-blocking exclusive flock() on the
file that succeeds means it's actually stale (nothing holds it), so
the session is reported as exited in that case; the lock is always
released again immediately, never left held by the check itself.

list_sessions() (pid_file.{h,cpp}) reads the real pid from each file's
own contents rather than parsing it out of the filename, which would
be ambiguous for container names that themselves contain '-'.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
2026-08-22 09:05:27 +00:00
parent 23f380e180
commit 8c55e5c288
5 changed files with 158 additions and 12 deletions
+22 -3
View File
@@ -18,7 +18,13 @@ Source layout (all under `src/`):
- `main.cpp` — CLI entry point, dependency checks, orchestration (`mount_image()`, - `main.cpp` — CLI entry point, dependency checks, orchestration (`mount_image()`,
`run_container()`, `cleanup_image()`, `unmount_image()`, `list_images_command()`, `run_container()`, `cleanup_image()`, `unmount_image()`, `list_images_command()`,
`inspect_image_command()`, `create_volume_command()`, `list_volumes_command()`, `inspect_image_command()`, `create_volume_command()`, `list_volumes_command()`,
`delete_volume_command()`). `inspect_image_command()` implements `-i/--inspect `delete_volume_command()`, `list_processes_command()`). `list_processes_command()`
implements `--list-processes` (long-option only): calls `list_sessions()`
(`pid_file.{h,cpp}`, see below) and prints one tab-aligned `pid`, `container
name`, `running`/`exited` row per entry (same two-column tab-alignment scheme
as `list_images_command()`/`list_volumes_command()`, extended to a third
column), no header row, silent success on an empty list.
`inspect_image_command()` implements `-i/--inspect
<image.tar>`: prints every `OciImageConfig` field (user/group, exposed ports, env, <image.tar>`: prints every `OciImageConfig` field (user/group, exposed ports, env,
volumes, default command) without mounting or running the image — extend it volumes, default command) without mounting or running the image — extend it
whenever `OciImageConfig` gains a new field (see `oci_image.{h,cpp}` below). whenever `OciImageConfig` gains a new field (see `oci_image.{h,cpp}` below).
@@ -188,7 +194,19 @@ Source layout (all under `src/`):
closes the fd (releasing the flock immediately) and removes the file. Every 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) 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 is a `spdlog::warn`, never fatal — session tracking is best-effort and must
never block or fail `-r/--run` itself. never block or fail `-r/--run` itself. `list_sessions()` implements
`--list-processes` (`main.cpp`'s `list_processes_command()`): scans the same
`run/` directory and reports one `SessionInfo {pid, container_name, running}`
per readable pid file. `pid` is read from the file's own contents, not parsed
from the filename (ambiguous for names that themselves contain `-`);
`container_name` is then recovered by stripping that exact `-<pid>` suffix
back off the filename. `running` reuses the same liveness check any external
tool would do — a non-blocking exclusive `flock()` that succeeds means the
file is actually stale, so `running` is false in that case; the lock is always
released again immediately either way, never left held by the check itself. A
file that can't be opened or doesn't parse as a pid (e.g. removed mid-scan) is
silently skipped, not reported as an error — scanning a live directory is
inherently racy.
- `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`,
@@ -267,7 +285,8 @@ Build directory is `buildDir/` (already configured).
full flag list: `-m/--mount`, `-r/--run`, `-u/--umount`, `-c/--cleanup`, full flag list: `-m/--mount`, `-r/--run`, `-u/--umount`, `-c/--cleanup`,
`-l/--list-images`, `-i/--inspect`, `-n/--no-nsenter`, `--user`, `--group`, `-l/--list-images`, `-i/--inspect`, `-n/--no-nsenter`, `--user`, `--group`,
`--hostname`, `-v/--volume`, `--list-volumes`, `--delete-volume`, `--hostname`, `-v/--volume`, `--list-volumes`, `--delete-volume`,
`--delete-volume-full`, `-t/--test`, `--log-level`, `-h/--help`, `-V/--version`) `--delete-volume-full`, `--list-processes`, `-t/--test`, `--log-level`,
`-h/--help`, `-V/--version`)
- Run tests: `meson test -C buildDir` - Run tests: `meson test -C buildDir`
## Code style ## Code style
+8 -1
View File
@@ -62,6 +62,7 @@ slocker-lite -v|--volume <name> <directory>
slocker-lite --list-volumes slocker-lite --list-volumes
slocker-lite --delete-volume <name> slocker-lite --delete-volume <name>
slocker-lite --delete-volume-full <name> slocker-lite --delete-volume-full <name>
slocker-lite --list-processes
slocker-lite -t|--test slocker-lite -t|--test
slocker-lite -h|--help slocker-lite -h|--help
slocker-lite -V|--version slocker-lite -V|--version
@@ -83,6 +84,7 @@ slocker-lite -V|--version
| `--list-volumes` | List all named volumes (see `-v/--volume`) with their host directory. | | `--list-volumes` | List all named volumes (see `-v/--volume`) with their host directory. |
| `--delete-volume <name>` | Remove a named volume from the config. The host directory is left untouched. | | `--delete-volume <name>` | Remove a named volume from the config. The host directory is left untouched. |
| `--delete-volume-full <name>` | Like `--delete-volume`, but also recursively deletes the volume's host directory. | | `--delete-volume-full <name>` | Like `--delete-volume`, but also recursively deletes the volume's host directory. |
| `--list-processes` | List running `--run` sessions found by their pid files under `$XDG_STATE_HOME/slocker-lite/run/`, with their pid, container name, and status (`running` or `exited`). |
| `-t, --test` | Print which `bwrap --unshare-xxx` namespaces the running kernel supports. | | `-t, --test` | Print which `bwrap --unshare-xxx` namespaces the running kernel supports. |
| `--log-level <level>` | Set log verbosity (`trace`, `debug`, `info`, `warn`, `error`, `critical`, `off`). | | `--log-level <level>` | Set log verbosity (`trace`, `debug`, `info`, `warn`, `error`, `critical`, `off`). |
| `-h, --help` | Print usage and exit. | | `-h, --help` | Print usage and exit. |
@@ -126,6 +128,9 @@ sudo ./buildDir/slocker-lite -r myimage.tar --user git
# Remove a named volume and delete its host directory too # Remove a named volume and delete its host directory too
./buildDir/slocker-lite --delete-volume-full mydata ./buildDir/slocker-lite --delete-volume-full mydata
# List currently running (and any leftover, exited) --run sessions
./buildDir/slocker-lite --list-processes
``` ```
## Configuration ## Configuration
@@ -170,7 +175,9 @@ 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 `$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 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 run ends; any tool can check whether a session is still alive by attempting the
same exclusive, non-blocking `flock()` on its file. same exclusive, non-blocking `flock()` on its file. `--list-processes` does
exactly that for every pid file it finds, reporting each one's pid, container
name, and `running`/`exited` status.
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).
+58 -8
View File
@@ -35,6 +35,7 @@
#include "config_file.h" #include "config_file.h"
#include "containers_storage.h" #include "containers_storage.h"
#include "oci_image.h" #include "oci_image.h"
#include "pid_file.h"
#include "process.h" #include "process.h"
#include "user_spec.h" #include "user_spec.h"
#include "volume_mount.h" #include "volume_mount.h"
@@ -55,13 +56,15 @@ enum class Mode {
kListVolumes, kListVolumes,
kDeleteVolume, kDeleteVolume,
kDeleteVolumeFull, kDeleteVolumeFull,
kInspect kInspect,
kListProcesses
}; };
// --log-level/--user/--group/--list-volumes/--delete-volume[-full]/--hostname have // --log-level/--user/--group/--list-volumes/--delete-volume[-full]/--hostname/
// no short form (--log-level's was freed up so -l could become --list-images; -u is // --list-processes have no short form (--log-level's was freed up so -l could
// already --umount; the rest have no natural free letter left, or don't need one), // become --list-images; -u is already --umount; the rest have no natural free
// so they need long-option vals outside the printable-char range short options use. // letter left, or don't need one), so they need long-option vals outside the
// printable-char range short options use.
constexpr int kLogLevelOpt = 256; constexpr int kLogLevelOpt = 256;
constexpr int kUserOpt = 257; constexpr int kUserOpt = 257;
constexpr int kGroupOpt = 258; constexpr int kGroupOpt = 258;
@@ -69,8 +72,9 @@ constexpr int kListVolumesOpt = 259;
constexpr int kDeleteVolumeOpt = 260; constexpr int kDeleteVolumeOpt = 260;
constexpr int kDeleteVolumeFullOpt = 261; constexpr int kDeleteVolumeFullOpt = 261;
constexpr int kHostnameOpt = 262; constexpr int kHostnameOpt = 262;
constexpr int kListProcessesOpt = 263;
constexpr std::array<struct option, 19> kLongOptions = {{ constexpr std::array<struct option, 20> kLongOptions = {{
{"help", no_argument, nullptr, 'h'}, {"help", no_argument, nullptr, 'h'},
{"version", no_argument, nullptr, 'V'}, {"version", no_argument, nullptr, 'V'},
{"test", no_argument, nullptr, 't'}, {"test", no_argument, nullptr, 't'},
@@ -89,6 +93,7 @@ constexpr std::array<struct option, 19> kLongOptions = {{
{"delete-volume-full", required_argument, nullptr, kDeleteVolumeFullOpt}, {"delete-volume-full", required_argument, nullptr, kDeleteVolumeFullOpt},
{"inspect", required_argument, nullptr, 'i'}, {"inspect", required_argument, nullptr, 'i'},
{"hostname", required_argument, nullptr, kHostnameOpt}, {"hostname", required_argument, nullptr, kHostnameOpt},
{"list-processes", no_argument, nullptr, kListProcessesOpt},
{nullptr, 0, nullptr, 0}, {nullptr, 0, nullptr, 0},
}}; }};
@@ -104,6 +109,7 @@ void print_usage(const char* prog) {
" {0} --list-volumes\n" " {0} --list-volumes\n"
" {0} --delete-volume <name>\n" " {0} --delete-volume <name>\n"
" {0} --delete-volume-full <name>\n" " {0} --delete-volume-full <name>\n"
" {0} --list-processes\n"
" {0} -t|--test\n" " {0} -t|--test\n"
" {0} -h|--help\n" " {0} -h|--help\n"
" {0} -V|--version\n" " {0} -V|--version\n"
@@ -160,6 +166,10 @@ void print_usage(const char* prog) {
" --delete-volume-full <name>\n" " --delete-volume-full <name>\n"
" like --delete-volume, but also recursively\n" " like --delete-volume, but also recursively\n"
" deletes the volume's host directory\n" " deletes the volume's host directory\n"
" --list-processes list running --run sessions found by their pid\n"
" files under $XDG_STATE_HOME/slocker-lite/run/,\n"
" with their pid, container name, and status\n"
" (running or exited)\n"
" -t, --test run the test suite\n" " -t, --test run the test suite\n"
" --log-level <level> set log verbosity (trace, debug, info, warn,\n" " --log-level <level> set log verbosity (trace, debug, info, warn,\n"
" error, critical, off)\n" " error, critical, off)\n"
@@ -426,6 +436,39 @@ int list_volumes_command(const AppConfig& config) {
return 0; return 0;
} }
int list_processes_command() {
auto sessions = list_sessions();
std::vector<std::string> pids;
std::vector<std::string> names;
pids.reserve(sessions.size());
names.reserve(sessions.size());
size_t max_pid_len = 0;
size_t max_name_len = 0;
for (const auto& session : sessions) {
pids.push_back(fmt::format("{}", session.pid));
names.push_back(session.container_name);
max_pid_len = std::max(max_pid_len, pids.back().size());
max_name_len = std::max(max_name_len, names.back().size());
}
// Same tab-alignment scheme as list_images_command()/list_volumes_command(),
// applied independently to each of the two variable-width columns.
constexpr size_t kTabWidth = 8;
size_t pid_target_tabs = max_pid_len / kTabWidth + 1;
size_t name_target_tabs = max_name_len / kTabWidth + 1;
for (size_t i = 0; i < sessions.size(); ++i) {
size_t pid_tabs_used = pids[i].size() / kTabWidth;
size_t pid_tabs_needed = pid_target_tabs > pid_tabs_used ? pid_target_tabs - pid_tabs_used : 1;
size_t name_tabs_used = names[i].size() / kTabWidth;
size_t name_tabs_needed = name_target_tabs > name_tabs_used ? name_target_tabs - name_tabs_used : 1;
fmt::print("{}{}{}{}{}\n", pids[i], std::string(pid_tabs_needed, '\t'), names[i],
std::string(name_tabs_needed, '\t'), sessions[i].running ? "running" : "exited");
}
return 0;
}
int delete_volume_command(const std::string& name, const std::filesystem::path& config_path, int delete_volume_command(const std::string& name, const std::filesystem::path& config_path,
AppConfig& config, bool delete_directory) { AppConfig& config, bool delete_directory) {
auto it = std::find_if(config.volumes.begin(), config.volumes.end(), auto it = std::find_if(config.volumes.begin(), config.volumes.end(),
@@ -584,7 +627,8 @@ int main(int argc, char* argv[]) {
case 'i': case 'i':
case kListVolumesOpt: case kListVolumesOpt:
case kDeleteVolumeOpt: case kDeleteVolumeOpt:
case kDeleteVolumeFullOpt: { case kDeleteVolumeFullOpt:
case kListProcessesOpt: {
Mode requested; Mode requested;
switch (opt) { switch (opt) {
case 't': case 't':
@@ -614,9 +658,12 @@ int main(int argc, char* argv[]) {
case kDeleteVolumeOpt: case kDeleteVolumeOpt:
requested = Mode::kDeleteVolume; requested = Mode::kDeleteVolume;
break; break;
default: case kDeleteVolumeFullOpt:
requested = Mode::kDeleteVolumeFull; requested = Mode::kDeleteVolumeFull;
break; break;
default:
requested = Mode::kListProcesses;
break;
} }
if (mode != Mode::kNone && mode != requested) { if (mode != Mode::kNone && mode != requested) {
spdlog::error("multiple actions specified"); spdlog::error("multiple actions specified");
@@ -726,6 +773,9 @@ int main(int argc, char* argv[]) {
if (mode == Mode::kDeleteVolume || mode == Mode::kDeleteVolumeFull) { if (mode == Mode::kDeleteVolume || mode == Mode::kDeleteVolumeFull) {
return delete_volume_command(mode_arg, config_path, *config, mode == Mode::kDeleteVolumeFull); return delete_volume_command(mode_arg, config_path, *config, mode == Mode::kDeleteVolumeFull);
} }
if (mode == Mode::kListProcesses) {
return list_processes_command();
}
if (mode == Mode::kRun) { if (mode == Mode::kRun) {
std::vector<std::string> command(argv + optind, argv + argc); std::vector<std::string> command(argv + optind, argv + argc);
// As root, containers-storage mount doesn't need to reexec into a private // As root, containers-storage mount doesn't need to reexec into a private
+50
View File
@@ -103,3 +103,53 @@ void release_session_lock(const SessionLock& lock) {
spdlog::warn("failed to remove session pid file {}: {}", lock.path.string(), ec.message()); spdlog::warn("failed to remove session pid file {}: {}", lock.path.string(), ec.message());
} }
} }
std::vector<SessionInfo> list_sessions() {
std::vector<SessionInfo> sessions;
auto dir = session_run_dir();
std::error_code dir_ec;
if (!std::filesystem::is_directory(dir, dir_ec)) {
return sessions;
}
std::error_code it_ec;
for (auto it = std::filesystem::directory_iterator(dir, it_ec);
!it_ec && it != std::filesystem::directory_iterator(); it.increment(it_ec)) {
const auto& path = it->path();
int fd = open(path.c_str(), O_RDWR);
if (fd < 0) {
spdlog::debug("failed to open session file {}: {}", path.string(), strerror(errno));
continue;
}
char buf[32] = {};
ssize_t n = read(fd, buf, sizeof(buf) - 1);
pid_t pid = n > 0 ? static_cast<pid_t>(std::atoi(buf)) : 0;
bool running = true;
if (flock(fd, LOCK_EX | LOCK_NB) == 0) {
running = false;
flock(fd, LOCK_UN);
}
close(fd);
if (pid <= 0) {
spdlog::debug("skipping malformed session pid file {}", path.string());
continue;
}
std::string filename = path.filename().string();
std::string suffix = fmt::format("-{}", pid);
std::string container_name =
filename.size() > suffix.size() &&
filename.compare(filename.size() - suffix.size(), suffix.size(), suffix) == 0
? filename.substr(0, filename.size() - suffix.size())
: filename;
sessions.push_back(SessionInfo{pid, container_name, running});
}
return sessions;
}
+20
View File
@@ -20,6 +20,7 @@
#include <optional> #include <optional>
#include <string> #include <string>
#include <string_view> #include <string_view>
#include <vector>
#include <sys/types.h> #include <sys/types.h>
@@ -55,3 +56,22 @@ std::optional<SessionLock> create_session_lock(std::string_view container_name,
// logs a warning on failure, never treated as fatal, mirroring run_container()'s // logs a warning on failure, never treated as fatal, mirroring run_container()'s
// own unmount/cleanup-failure handling. // own unmount/cleanup-failure handling.
void release_session_lock(const SessionLock& lock); void release_session_lock(const SessionLock& lock);
struct SessionInfo {
pid_t pid;
std::string container_name; // as recovered from the pid file's own name (sanitized)
bool running;
};
// Scans $XDG_STATE_HOME/slocker-lite/run/ (see session_pid_file_path()) for pid
// files and reports one SessionInfo per readable one, in directory-iteration
// order. `pid` is read from the file's own contents (not parsed from the
// filename, which would be ambiguous for names that themselves contain '-').
// `running` is determined the same way any other tool would check liveness: a
// non-blocking exclusive flock() on the file that succeeds means it's actually
// stale (nothing holds it), so `running` is false in that case -- the lock is
// released again immediately either way, never left held. A file that can't be
// opened or whose contents don't parse as a pid (e.g. removed mid-scan, a race
// that's inherent to scanning a live directory) is skipped, not reported as an
// error. An empty or missing run directory yields an empty result, not an error.
std::vector<SessionInfo> list_sessions();