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.
|
# maintenance status — pick the upstream one.
|
||||||
tree-sitter-md = "0.5"
|
tree-sitter-md = "0.5"
|
||||||
# T M4.4 process supervisor: signal sending without `unsafe`. Keep
|
# T M4.4 process supervisor: signal sending without `unsafe`. Keep
|
||||||
# the feature surface tight to keep build time low (no syscalls
|
# the feature surface tight to keep build time low. `poll` feeds the
|
||||||
# beyond `kill(2)` for v0.1).
|
# compile-mode group readers (cancellable poll-based reads, Q#CM3).
|
||||||
nix = { version = "0.29", default-features = false, features = ["signal", "user", "fs", "term", "socket"] }
|
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
|
# T M4.4 PTY mode: portable abstraction over openpty / fork+exec
|
||||||
# with controlling-tty wiring. The crate uses internal `unsafe`
|
# with controlling-tty wiring. The crate uses internal `unsafe`
|
||||||
# but exposes a fully safe API; pmacs's own `unsafe_code = "forbid"`
|
# 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, ()| {
|
methods.add_method("is_modified", |lua, this, ()| {
|
||||||
with_registry(lua, |r| Ok(resolve(r, this.0)?.is_modified()))
|
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()
|
.ok()
|
||||||
.flatten()
|
.flatten()
|
||||||
.unwrap_or(false);
|
.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 {
|
Ok(ProcessSpec {
|
||||||
label,
|
label,
|
||||||
command,
|
command,
|
||||||
|
|
@ -6983,6 +7016,8 @@ fn lua_to_spec(table: &Table) -> mlua::Result<ProcessSpec> {
|
||||||
mode,
|
mode,
|
||||||
restart,
|
restart,
|
||||||
ansi_events,
|
ansi_events,
|
||||||
|
stdin,
|
||||||
|
group,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -10796,7 +10831,25 @@ fn install_motion(editor: &Table, lua: &Lua, core: &SharedCore) -> mlua::Result<
|
||||||
let cc = core.clone();
|
let cc = core.clone();
|
||||||
editor.set(
|
editor.set(
|
||||||
"jump_back",
|
"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(())
|
Ok(())
|
||||||
|
|
@ -12584,6 +12637,34 @@ mod tests {
|
||||||
assert!(called);
|
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]
|
#[test]
|
||||||
fn buffer_remove_prunes_buffer_local_keymaps() {
|
fn buffer_remove_prunes_buffer_local_keymaps() {
|
||||||
let (lua, _reg, _cmds, kms, _hks) = fresh();
|
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