Files
Kigi-CLI/crates/codegen/ptyctl-cli/src/registry.rs
T
ZacharyZhang-NY a02b555e66 docs(comments): rewrite comments across all crates to the guidelines
Sweep every first-party crate source (1956 .rs files) to the project comment
guidelines: delete redundant restatements, decorative banners, change
narration, and end-of-line comments; keep and tighten the crucial ones
(invariants, bug rationale, SAFETY blocks, ported-source attribution).

No functional code changed. Every edit is proven comment-only against the
prior tree by a comment-stripping lexer (string/char/raw-string aware) plus a
separate doctest-fence check. Where removing a comment made rustfmt or clippy
want to re-lay-out adjacent code, the minimal triggering comment is restored so
code tokens stay byte-identical.

Gates green: cargo fmt --all --check (0 diffs), cargo check and cargo clippy
--workspace --all-targets (0 warnings).

Adds scripts/check_codegen_comment_guidelines.py — the enforcement gate for
these guidelines (flags banners, end-of-line comments, change narration, and
commented-out code).
2026-07-23 16:55:39 -04:00

129 lines
4.0 KiB
Rust

//! Named session registry stored at ~/.local/state/ptyctl/sessions/.
use std::fs;
use std::path::PathBuf;
use anyhow::{Context, Result, bail};
use serde::{Deserialize, Serialize};
/// Information about a registered session.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionInfo {
pub port: u16,
pub pid: Option<u32>,
pub command: Vec<String>,
pub cwd: String,
pub started_at: String,
}
/// Get the session registry directory.
fn registry_dir() -> Result<PathBuf> {
if let Ok(dir) = std::env::var("PTYCTL_SESSION_DIR") {
return Ok(PathBuf::from(dir));
}
let state_dir = dirs::state_dir()
.or_else(dirs::data_local_dir)
.context("cannot determine state directory")?;
Ok(state_dir.join("ptyctl").join("sessions"))
}
/// Register a named session.
pub fn register_session(name: &str, info: &SessionInfo) -> Result<()> {
let dir = registry_dir()?;
fs::create_dir_all(&dir).context("failed to create session registry directory")?;
let path = dir.join(format!("{name}.json"));
let json = serde_json::to_string_pretty(info)?;
// Atomic write: write to temp file, then rename.
let tmp = dir.join(format!(".{name}.json.tmp"));
fs::write(&tmp, &json).context("failed to write session file")?;
fs::rename(&tmp, &path).context("failed to rename session file")?;
Ok(())
}
/// Look up a named session.
pub fn lookup_session(name: &str) -> Result<SessionInfo> {
let dir = registry_dir()?;
let path = dir.join(format!("{name}.json"));
if !path.exists() {
bail!("session '{name}' not found");
}
let json = fs::read_to_string(&path).context("failed to read session file")?;
let info: SessionInfo = serde_json::from_str(&json).context("failed to parse session file")?;
Ok(info)
}
/// Remove a named session.
pub fn unregister_session(name: &str) -> Result<()> {
let dir = registry_dir()?;
let path = dir.join(format!("{name}.json"));
if path.exists() {
fs::remove_file(&path).context("failed to remove session file")?;
}
Ok(())
}
/// Check whether a registered session's ptyctl server is reachable.
///
/// Probes `GET /query/status` and requires a 200 with the ptyctl status body
/// shape — a bare TCP connect (or bare 200) would misread an unrelated
/// process on a recycled port as live. Not PID-based because the recorded
/// PID is the child, which may exit while a `--linger` server is still up.
pub async fn server_alive(port: u16) -> bool {
let Ok(client) = reqwest::Client::builder()
.timeout(std::time::Duration::from_millis(500))
.build()
else {
return false;
};
match client
.get(format!("http://127.0.0.1:{port}/query/status"))
.send()
.await
{
// Require the status body shape, not just a 200, so wildcard-200 servers read dead.
Ok(resp) if resp.status() == reqwest::StatusCode::OK => resp
.json::<serde_json::Value>()
.await
.is_ok_and(|v| v.get("size").is_some()),
_ => false,
}
}
/// List all registered sessions.
pub fn list_sessions() -> Result<Vec<(String, SessionInfo)>> {
let dir = registry_dir()?;
if !dir.exists() {
return Ok(Vec::new());
}
let mut sessions = Vec::new();
for entry in fs::read_dir(&dir).context("failed to read session directory")? {
let entry = entry?;
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) == Some("json") {
let name = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string();
if name.starts_with('.') {
// skip temp files
continue;
}
if let Ok(json) = fs::read_to_string(&path)
&& let Ok(info) = serde_json::from_str::<SessionInfo>(&json)
{
sessions.push((name, info));
}
}
}
Ok(sessions)
}