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:
@@ -18,7 +18,13 @@ Source layout (all under `src/`):
|
||||
- `main.cpp` — CLI entry point, dependency checks, orchestration (`mount_image()`,
|
||||
`run_container()`, `cleanup_image()`, `unmount_image()`, `list_images_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,
|
||||
volumes, default command) without mounting or running the image — extend it
|
||||
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
|
||||
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.
|
||||
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
|
||||
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`,
|
||||
@@ -267,7 +285,8 @@ Build directory is `buildDir/` (already configured).
|
||||
full flag list: `-m/--mount`, `-r/--run`, `-u/--umount`, `-c/--cleanup`,
|
||||
`-l/--list-images`, `-i/--inspect`, `-n/--no-nsenter`, `--user`, `--group`,
|
||||
`--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`
|
||||
|
||||
## Code style
|
||||
|
||||
@@ -62,6 +62,7 @@ slocker-lite -v|--volume <name> <directory>
|
||||
slocker-lite --list-volumes
|
||||
slocker-lite --delete-volume <name>
|
||||
slocker-lite --delete-volume-full <name>
|
||||
slocker-lite --list-processes
|
||||
slocker-lite -t|--test
|
||||
slocker-lite -h|--help
|
||||
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. |
|
||||
| `--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. |
|
||||
| `--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. |
|
||||
| `--log-level <level>` | Set log verbosity (`trace`, `debug`, `info`, `warn`, `error`, `critical`, `off`). |
|
||||
| `-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
|
||||
./buildDir/slocker-lite --delete-volume-full mydata
|
||||
|
||||
# List currently running (and any leftover, exited) --run sessions
|
||||
./buildDir/slocker-lite --list-processes
|
||||
```
|
||||
|
||||
## 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
|
||||
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.
|
||||
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
|
||||
reasoning behind each of the above, and known gaps).
|
||||
|
||||
+58
-8
@@ -35,6 +35,7 @@
|
||||
#include "config_file.h"
|
||||
#include "containers_storage.h"
|
||||
#include "oci_image.h"
|
||||
#include "pid_file.h"
|
||||
#include "process.h"
|
||||
#include "user_spec.h"
|
||||
#include "volume_mount.h"
|
||||
@@ -55,13 +56,15 @@ enum class Mode {
|
||||
kListVolumes,
|
||||
kDeleteVolume,
|
||||
kDeleteVolumeFull,
|
||||
kInspect
|
||||
kInspect,
|
||||
kListProcesses
|
||||
};
|
||||
|
||||
// --log-level/--user/--group/--list-volumes/--delete-volume[-full]/--hostname have
|
||||
// no short form (--log-level's was freed up so -l could become --list-images; -u is
|
||||
// already --umount; the rest have no natural free letter left, or don't need one),
|
||||
// so they need long-option vals outside the printable-char range short options use.
|
||||
// --log-level/--user/--group/--list-volumes/--delete-volume[-full]/--hostname/
|
||||
// --list-processes have no short form (--log-level's was freed up so -l could
|
||||
// become --list-images; -u is already --umount; the rest have no natural free
|
||||
// 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 kUserOpt = 257;
|
||||
constexpr int kGroupOpt = 258;
|
||||
@@ -69,8 +72,9 @@ constexpr int kListVolumesOpt = 259;
|
||||
constexpr int kDeleteVolumeOpt = 260;
|
||||
constexpr int kDeleteVolumeFullOpt = 261;
|
||||
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'},
|
||||
{"version", no_argument, nullptr, 'V'},
|
||||
{"test", no_argument, nullptr, 't'},
|
||||
@@ -89,6 +93,7 @@ constexpr std::array<struct option, 19> kLongOptions = {{
|
||||
{"delete-volume-full", required_argument, nullptr, kDeleteVolumeFullOpt},
|
||||
{"inspect", required_argument, nullptr, 'i'},
|
||||
{"hostname", required_argument, nullptr, kHostnameOpt},
|
||||
{"list-processes", no_argument, nullptr, kListProcessesOpt},
|
||||
{nullptr, 0, nullptr, 0},
|
||||
}};
|
||||
|
||||
@@ -104,6 +109,7 @@ void print_usage(const char* prog) {
|
||||
" {0} --list-volumes\n"
|
||||
" {0} --delete-volume <name>\n"
|
||||
" {0} --delete-volume-full <name>\n"
|
||||
" {0} --list-processes\n"
|
||||
" {0} -t|--test\n"
|
||||
" {0} -h|--help\n"
|
||||
" {0} -V|--version\n"
|
||||
@@ -160,6 +166,10 @@ void print_usage(const char* prog) {
|
||||
" --delete-volume-full <name>\n"
|
||||
" like --delete-volume, but also recursively\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"
|
||||
" --log-level <level> set log verbosity (trace, debug, info, warn,\n"
|
||||
" error, critical, off)\n"
|
||||
@@ -426,6 +436,39 @@ int list_volumes_command(const AppConfig& config) {
|
||||
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,
|
||||
AppConfig& config, bool delete_directory) {
|
||||
auto it = std::find_if(config.volumes.begin(), config.volumes.end(),
|
||||
@@ -584,7 +627,8 @@ int main(int argc, char* argv[]) {
|
||||
case 'i':
|
||||
case kListVolumesOpt:
|
||||
case kDeleteVolumeOpt:
|
||||
case kDeleteVolumeFullOpt: {
|
||||
case kDeleteVolumeFullOpt:
|
||||
case kListProcessesOpt: {
|
||||
Mode requested;
|
||||
switch (opt) {
|
||||
case 't':
|
||||
@@ -614,9 +658,12 @@ int main(int argc, char* argv[]) {
|
||||
case kDeleteVolumeOpt:
|
||||
requested = Mode::kDeleteVolume;
|
||||
break;
|
||||
default:
|
||||
case kDeleteVolumeFullOpt:
|
||||
requested = Mode::kDeleteVolumeFull;
|
||||
break;
|
||||
default:
|
||||
requested = Mode::kListProcesses;
|
||||
break;
|
||||
}
|
||||
if (mode != Mode::kNone && mode != requested) {
|
||||
spdlog::error("multiple actions specified");
|
||||
@@ -726,6 +773,9 @@ int main(int argc, char* argv[]) {
|
||||
if (mode == Mode::kDeleteVolume || 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) {
|
||||
std::vector<std::string> command(argv + optind, argv + argc);
|
||||
// As root, containers-storage mount doesn't need to reexec into a private
|
||||
|
||||
@@ -103,3 +103,53 @@ void release_session_lock(const SessionLock& lock) {
|
||||
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,6 +20,7 @@
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#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
|
||||
// own unmount/cleanup-failure handling.
|
||||
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();
|
||||
|
||||
Reference in New Issue
Block a user