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).
129 lines
4.0 KiB
Rust
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)
|
|
}
|