diff --git a/CLAUDE.md b/CLAUDE.md index f1029c9..b7a1f54 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 `: 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 `-` 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, ``) 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 diff --git a/README.md b/README.md index 23dfe00..f896098 100644 --- a/README.md +++ b/README.md @@ -62,6 +62,7 @@ slocker-lite -v|--volume slocker-lite --list-volumes slocker-lite --delete-volume slocker-lite --delete-volume-full +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 ` | Remove a named volume from the config. The host directory is left untouched. | | `--delete-volume-full ` | 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 ` | 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). diff --git a/src/main.cpp b/src/main.cpp index e68d05c..c1a0d89 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -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 kLongOptions = {{ +constexpr std::array kLongOptions = {{ {"help", no_argument, nullptr, 'h'}, {"version", no_argument, nullptr, 'V'}, {"test", no_argument, nullptr, 't'}, @@ -89,6 +93,7 @@ constexpr std::array 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 \n" " {0} --delete-volume-full \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 \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 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 pids; + std::vector 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 command(argv + optind, argv + argc); // As root, containers-storage mount doesn't need to reexec into a private diff --git a/src/pid_file.cpp b/src/pid_file.cpp index 566e9bd..e7fc3d6 100644 --- a/src/pid_file.cpp +++ b/src/pid_file.cpp @@ -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 list_sessions() { + std::vector 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(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; +} diff --git a/src/pid_file.h b/src/pid_file.h index 43875b0..50f8bee 100644 --- a/src/pid_file.h +++ b/src/pid_file.h @@ -20,6 +20,7 @@ #include #include #include +#include #include @@ -55,3 +56,22 @@ std::optional 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 list_sessions();