feat(process): group lifecycle + null stdin; buf:revision(); jump_back hook parity
Supervisor (Q#CM3, framing additions 1-2): ProcessSpec gains stdin="null" (Stdio::null, no writer thread, immediate EOF) and group=true — process_group(0) spawn, group-directed fatal signals, liveness-probed TERM-to-KILL reap ledger (insert-if-absent arming, per-tick kill(-pgid,0) probe, GROUP_TERM_GRACE=500ms), leader-exit group TERM before the final drain with in-drain deadline enforcement plus ESRCH quiescence window and absolute cancel cap, poll-based cancellable readers (nix poll feature added), shutdown ledger force-kill + probe-to-ESRCH, maybe_restart gated once shut_down. Both options are pipe-mode-only and rejected at spawn under PTY. Nine unit tests cover framing acceptance 34, including the TERM-ignoring redirected survivor, the pipe-holding descendant tick bound, and setsid-escapee resource reclamation via the per-runtime active-reader counter. Bindings (additions 3-4): buf:revision() exposes the edit revision (bumped by edit/undo/redo — unit-pinned); pmacs.editor.jump_back now fires buffer.after-switch exactly when the jump changed buffers, matching pmacs.window.switch_buffer; pmacs.process.spawn parses the stdin/group spec keys. Deviation from the framing letter, called out for review: the active-reader counter field is always present (one Arc + two atomics per reader lifetime) rather than cfg(test)-gated — gating the field would spread cfg attributes through every construction site; only the probe accessor is test-gated. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01VoiEyuPjoBhvwACf8HAnLB
This commit is contained in:
parent
9a9774fc39
commit
a7d5a6fedf
|
|
@ -154,9 +154,9 @@ tree-sitter-cpp = "0.23"
|
|||
# maintenance status — pick the upstream one.
|
||||
tree-sitter-md = "0.5"
|
||||
# T M4.4 process supervisor: signal sending without `unsafe`. Keep
|
||||
# the feature surface tight to keep build time low (no syscalls
|
||||
# beyond `kill(2)` for v0.1).
|
||||
nix = { version = "0.29", default-features = false, features = ["signal", "user", "fs", "term", "socket"] }
|
||||
# the feature surface tight to keep build time low. `poll` feeds the
|
||||
# compile-mode group readers (cancellable poll-based reads, Q#CM3).
|
||||
nix = { version = "0.29", default-features = false, features = ["signal", "user", "fs", "term", "socket", "poll"] }
|
||||
# T M4.4 PTY mode: portable abstraction over openpty / fork+exec
|
||||
# with controlling-tty wiring. The crate uses internal `unsafe`
|
||||
# but exposes a fully safe API; pmacs's own `unsafe_code = "forbid"`
|
||||
|
|
|
|||
|
|
@ -1181,6 +1181,17 @@ fn add_query_methods<M: UserDataMethods<BufferIdLua>>(methods: &mut M) {
|
|||
})
|
||||
});
|
||||
|
||||
// Edit revision: bumped by every edit, undo, and redo. The
|
||||
// compile-mode external-edit guard (Q#CM2) records this after
|
||||
// each of its own writes and resyncs on mismatch — byte length
|
||||
// is not an edit-integrity token (a same-length replace changes
|
||||
// content while preserving length).
|
||||
methods.add_method("revision", |lua, this, ()| {
|
||||
with_registry(lua, |r| {
|
||||
i64::try_from(resolve(r, this.0)?.revision()).map_err(mlua::Error::external)
|
||||
})
|
||||
});
|
||||
|
||||
methods.add_method("is_modified", |lua, this, ()| {
|
||||
with_registry(lua, |r| Ok(resolve(r, this.0)?.is_modified()))
|
||||
});
|
||||
|
|
@ -6974,6 +6985,28 @@ fn lua_to_spec(table: &Table) -> mlua::Result<ProcessSpec> {
|
|||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or(false);
|
||||
// Compile-mode process shape (Q#CM3). Both options are
|
||||
// pipe-mode-only; the supervisor rejects them at spawn under PTY
|
||||
// so misconfiguration surfaces as a spawn error, not silence.
|
||||
let stdin = match table
|
||||
.get::<Option<String>>("stdin")
|
||||
.ok()
|
||||
.flatten()
|
||||
.as_deref()
|
||||
{
|
||||
None | Some("piped") => crate::process::StdinMode::Piped,
|
||||
Some("null") => crate::process::StdinMode::Null,
|
||||
Some(other) => {
|
||||
return Err(mlua::Error::external(format!(
|
||||
"stdin must be \"piped\" or \"null\"; got {other:?}"
|
||||
)));
|
||||
}
|
||||
};
|
||||
let group = table
|
||||
.get::<Option<bool>>("group")
|
||||
.ok()
|
||||
.flatten()
|
||||
.unwrap_or(false);
|
||||
Ok(ProcessSpec {
|
||||
label,
|
||||
command,
|
||||
|
|
@ -6983,6 +7016,8 @@ fn lua_to_spec(table: &Table) -> mlua::Result<ProcessSpec> {
|
|||
mode,
|
||||
restart,
|
||||
ansi_events,
|
||||
stdin,
|
||||
group,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -10796,7 +10831,25 @@ fn install_motion(editor: &Table, lua: &Lua, core: &SharedCore) -> mlua::Result<
|
|||
let cc = core.clone();
|
||||
editor.set(
|
||||
"jump_back",
|
||||
lua.create_function(move |_, ()| Ok(cc.borrow_mut().jump_back()))?,
|
||||
lua.create_function(move |lua, ()| {
|
||||
let (jumped, buffer_changed) = {
|
||||
let mut core = cc.borrow_mut();
|
||||
let before = core.active_buffer_id();
|
||||
let jumped = core.jump_back();
|
||||
(jumped, core.active_buffer_id() != before)
|
||||
};
|
||||
// Parity with `pmacs.window.switch_buffer` (compile-mode
|
||||
// additions #3): a jump that lands in another buffer
|
||||
// clears the destination window's overlays exactly like
|
||||
// any other switch, so overlay subscribers need the same
|
||||
// re-attach signal. Without this, RET → M-, permanently
|
||||
// stripped a generated buffer's styling. Same-buffer
|
||||
// jumps stay hook-silent.
|
||||
if buffer_changed {
|
||||
run_hook_if_defined(lua, "buffer.after-switch", mlua::MultiValue::new());
|
||||
}
|
||||
Ok(jumped)
|
||||
})?,
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
|
|
@ -12584,6 +12637,34 @@ mod tests {
|
|||
assert!(called);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_revision_bumps_on_edit_undo_and_redo() {
|
||||
// Compile-mode's external-edit guard (Q#CM2) leans on all
|
||||
// three bump sources: a same-length replace changes content
|
||||
// without changing length, and undo/redo are exactly the
|
||||
// mutations the guard exists to catch.
|
||||
let (lua, _reg, _cmds, _kms, _hks) = fresh();
|
||||
let ok: bool = lua
|
||||
.load(
|
||||
r#"
|
||||
local b = pmacs.buffer.from_bytes("rev", "abcd")
|
||||
local r0 = b:revision()
|
||||
b:insert(4, "e")
|
||||
local r1 = b:revision()
|
||||
b:replace(0, 1, "X") -- same-length replace still bumps
|
||||
local r2 = b:revision()
|
||||
assert(b:undo(), "undo applies")
|
||||
local r3 = b:revision()
|
||||
assert(b:redo(), "redo applies")
|
||||
local r4 = b:revision()
|
||||
return r1 > r0 and r2 > r1 and r3 > r2 and r4 > r3
|
||||
"#,
|
||||
)
|
||||
.eval()
|
||||
.unwrap();
|
||||
assert!(ok, "revision must be strictly monotonic across edit/undo/redo");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn buffer_remove_prunes_buffer_local_keymaps() {
|
||||
let (lua, _reg, _cmds, kms, _hks) = fresh();
|
||||
|
|
|
|||
888
src/process.rs
888
src/process.rs
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue