Synced from monorepo
Synced from monorepo Changes: - grok-shell: send an expired external-provider credential to the sign-in flow, not a 401 loop - pager: clickable ▲ jumps to the top of the response being read - grok-shell: keep a large task log from making the completion message too long - Plan viewer scrollbar: widen grab zone to the border column; fix striped thumb in Terminal.app - pager: poll the tmux probe teardown grace instead of sleeping it - security: vendor-compat MCP kill switch is now actually enforced when reported as on - grok-shell: restore session eviction when a leader client disconnects - Bump rust-toolchain to 1.93.0 - workspace: lexical-normalize permission path patterns before glob matching - pager: reject garbage Enter in the /resume picker - pager: show Mermaid affordances in plan mode preview - pager: drop manage-account link from /session-info - workspace: auto-approve read-only git queries; defer write floor to auto classifier - Add free-form pattern editor to the "Always allow" command prompt - grok-shell: fix /btw caching - pager: Tab walks answers in the ask_user_question card - External-provider auth refresh: single 7s attempt instead of 3×5s - pager: don't resurrect finished background tasks as Running when completion arrives first - pager: report tmux truecolor clamping in Doctor - Fix plan viewer scrollbar click+drag hijacked by comment gutter - pager/shell: stop double Recap after the same last turn - sampler: preserve x-should-retry through stream collection - pager: clear plan-mode indicator immediately when the user approves a plan - pager: tmux does not re-read its config on reattach Source-Revision: 64c4de99cc822b25ce9c54ab5a4f372093d0885d
This commit is contained in:
parent
a422116582
commit
780d1388ff
323 changed files with 12258 additions and 7226 deletions
|
|
@ -39,6 +39,9 @@
|
|||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
|
||||
mod process_resources;
|
||||
pub use process_resources::{ProcessResources, sample_process_memory, sample_process_resources};
|
||||
|
||||
mod process_scope;
|
||||
pub use process_scope::{ProcessScope, global_process_scope};
|
||||
|
||||
|
|
@ -491,6 +494,30 @@ impl ProcessGroup {
|
|||
}
|
||||
}
|
||||
|
||||
/// Whether any process still exists in this group. `None` where the
|
||||
/// platform cannot say (Windows, `EPERM`); treat it as alive.
|
||||
///
|
||||
/// Over-reports, never under-reports: an unreaped zombie is still a
|
||||
/// process, so it counts as live. Filtering zombies out would let a
|
||||
/// reaped leader with a live descendant look empty.
|
||||
pub fn has_live_members(&self) -> Option<bool> {
|
||||
#[cfg(unix)]
|
||||
{
|
||||
let Some(leader) = self.leader else {
|
||||
return Some(false);
|
||||
};
|
||||
match nix::sys::signal::killpg(nix::unistd::Pid::from_raw(leader.get() as i32), None) {
|
||||
Ok(()) => Some(true),
|
||||
Err(nix::errno::Errno::ESRCH) => Some(false),
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
#[cfg(windows)]
|
||||
{
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Ask an interactive shell to hang up. Its job-control children each live
|
||||
/// in their own process group, which no `killpg` here reaches, but a shell
|
||||
/// forwards the hangup to them before it exits.
|
||||
|
|
@ -826,6 +853,29 @@ mod tests {
|
|||
kill_on_parent_death_std(&mut cmd);
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn has_live_members_tracks_the_group_emptying() {
|
||||
let mut group = ProcessGroup::new().expect("group");
|
||||
assert_eq!(group.has_live_members(), Some(false));
|
||||
|
||||
let mut cmd = std::process::Command::new("sleep");
|
||||
cmd.arg("1000")
|
||||
.stdin(std::process::Stdio::null())
|
||||
.stdout(std::process::Stdio::null())
|
||||
.stderr(std::process::Stdio::null());
|
||||
detach_std_command(&mut cmd);
|
||||
#[allow(clippy::disallowed_methods)] // test: exercises ProcessGroup directly
|
||||
let mut child = cmd.spawn().expect("spawn sleeper");
|
||||
group.attach_std(&child).expect("attach");
|
||||
assert_eq!(group.has_live_members(), Some(true));
|
||||
|
||||
group.kill().expect("kill group");
|
||||
// Required: an unreaped zombie still reports live.
|
||||
child.wait().expect("reap sleeper");
|
||||
assert_eq!(group.has_live_members(), Some(false));
|
||||
}
|
||||
|
||||
/// Debug builds enforce the top-of-doc caveat that arming and spawning
|
||||
/// happen on the same (long-lived) thread — pdeathsig binds to the
|
||||
/// spawning thread's lifetime, so a cross-thread arm+spawn must fail
|
||||
|
|
|
|||
195
crates/codegen/xai-tty-utils/src/process_resources.rs
Normal file
195
crates/codegen/xai-tty-utils/src/process_resources.rs
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
//! This process's resource gauges.
|
||||
//!
|
||||
//! Lives beside `process_scope` so every caller shares one reader.
|
||||
|
||||
/// Fields are `None` where the platform offers no cheap equivalent. Threads
|
||||
/// and open files are Linux-only, matching what the resource soaks bound.
|
||||
#[derive(Clone, Copy, Debug, Default)]
|
||||
pub struct ProcessResources {
|
||||
pub rss_bytes: Option<u64>,
|
||||
/// Since the process started, not since the last sample.
|
||||
pub peak_rss_bytes: Option<u64>,
|
||||
pub footprint_bytes: Option<u64>,
|
||||
pub threads: Option<u64>,
|
||||
pub open_files: Option<u64>,
|
||||
}
|
||||
|
||||
/// One syscall on macOS, one file read plus two directory reads on Linux.
|
||||
pub fn sample_process_resources() -> ProcessResources {
|
||||
imp::sample()
|
||||
}
|
||||
|
||||
/// Memory only, leaving `threads` and `open_files` unset. Skips the two
|
||||
/// directory scans on Linux, for callers that sample on a timer.
|
||||
pub fn sample_process_memory() -> ProcessResources {
|
||||
imp::sample_memory()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod imp {
|
||||
use super::ProcessResources;
|
||||
|
||||
// Hand-rolled `task_vm_info` prefix through `phys_footprint` (the kernel
|
||||
// accepts any count ≤ the current struct revision; passing the prefix
|
||||
// count returns exactly these fields). Layout per XNU osfmk/mach/task_info.h.
|
||||
#[repr(C)]
|
||||
#[derive(Default)]
|
||||
struct TaskVmInfoPrefix {
|
||||
virtual_size: u64,
|
||||
region_count: i32,
|
||||
page_size: i32,
|
||||
resident_size: u64,
|
||||
resident_size_peak: u64,
|
||||
device: u64,
|
||||
device_peak: u64,
|
||||
internal: u64,
|
||||
internal_peak: u64,
|
||||
external: u64,
|
||||
external_peak: u64,
|
||||
reusable: u64,
|
||||
reusable_peak: u64,
|
||||
purgeable_volatile_pmap: u64,
|
||||
purgeable_volatile_resident: u64,
|
||||
purgeable_volatile_virtual: u64,
|
||||
compressed: u64,
|
||||
compressed_peak: u64,
|
||||
compressed_lifetime: u64,
|
||||
phys_footprint: u64,
|
||||
}
|
||||
|
||||
const TASK_VM_INFO: u32 = 22;
|
||||
// mach natural_t (u32) units.
|
||||
const PREFIX_COUNT: u32 = (size_of::<TaskVmInfoPrefix>() / size_of::<u32>()) as u32;
|
||||
|
||||
unsafe extern "C" {
|
||||
// libSystem: the calling task's control port and task_info(2).
|
||||
static mach_task_self_: u32;
|
||||
fn task_info(task: u32, flavor: u32, info: *mut u8, count: *mut u32) -> i32;
|
||||
}
|
||||
|
||||
pub(super) fn sample_memory() -> ProcessResources {
|
||||
sample()
|
||||
}
|
||||
|
||||
pub(super) fn sample() -> ProcessResources {
|
||||
let mut info = TaskVmInfoPrefix::default();
|
||||
let mut count = PREFIX_COUNT;
|
||||
// SAFETY: `info` is a properly sized/aligned out-buffer and `count`
|
||||
// tells the kernel its length in natural_t units; TASK_VM_INFO on
|
||||
// the caller's own task port cannot fault.
|
||||
let kr = unsafe {
|
||||
task_info(
|
||||
mach_task_self_,
|
||||
TASK_VM_INFO,
|
||||
(&raw mut info).cast::<u8>(),
|
||||
&raw mut count,
|
||||
)
|
||||
};
|
||||
if kr != 0 {
|
||||
return ProcessResources::default();
|
||||
}
|
||||
ProcessResources {
|
||||
rss_bytes: Some(info.resident_size),
|
||||
peak_rss_bytes: Some(info.resident_size_peak),
|
||||
footprint_bytes: Some(info.phys_footprint),
|
||||
threads: None,
|
||||
open_files: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
mod imp {
|
||||
use super::ProcessResources;
|
||||
|
||||
pub(super) fn sample() -> ProcessResources {
|
||||
ProcessResources {
|
||||
threads: count_entries("/proc/self/task"),
|
||||
open_files: count_entries("/proc/self/fd"),
|
||||
..sample_memory()
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn sample_memory() -> ProcessResources {
|
||||
let status = std::fs::read_to_string("/proc/self/status").unwrap_or_default();
|
||||
ProcessResources {
|
||||
rss_bytes: field_kb(&status, "VmRSS:").map(|kb| kb * 1024),
|
||||
peak_rss_bytes: field_kb(&status, "VmHWM:").map(|kb| kb * 1024),
|
||||
footprint_bytes: None,
|
||||
threads: None,
|
||||
open_files: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// `/proc/self/status` reports these in kB, so this needs no page size.
|
||||
fn field_kb(status: &str, prefix: &str) -> Option<u64> {
|
||||
status
|
||||
.lines()
|
||||
.find_map(|line| line.strip_prefix(prefix))
|
||||
.and_then(|value| value.split_whitespace().next())
|
||||
.and_then(|kb| kb.parse().ok())
|
||||
}
|
||||
|
||||
/// The read's own transient descriptor closes with the iterator, so
|
||||
/// repeated samples stay comparable.
|
||||
fn count_entries(dir: &str) -> Option<u64> {
|
||||
Some(std::fs::read_dir(dir).ok()?.count() as u64)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "macos", target_os = "linux")))]
|
||||
mod imp {
|
||||
use super::ProcessResources;
|
||||
|
||||
pub(super) fn sample() -> ProcessResources {
|
||||
ProcessResources::default()
|
||||
}
|
||||
|
||||
pub(super) fn sample_memory() -> ProcessResources {
|
||||
ProcessResources::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{sample_process_memory, sample_process_resources};
|
||||
|
||||
#[test]
|
||||
fn a_running_process_reports_its_own_gauges() {
|
||||
let usage = sample_process_resources();
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
{
|
||||
let rss = usage.rss_bytes.expect("rss");
|
||||
assert!(rss > 0);
|
||||
assert!(
|
||||
usage.peak_rss_bytes.expect("peak") >= rss,
|
||||
"peak is a high-water mark, so it can never trail current"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
assert!(usage.footprint_bytes.expect("footprint") > 0);
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
assert!(usage.threads.expect("threads") >= 1);
|
||||
assert!(usage.open_files.expect("open files") >= 1);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "linux"))]
|
||||
assert_eq!((usage.threads, usage.open_files), (None, None));
|
||||
}
|
||||
|
||||
/// The reason this function exists is the scans it skips, so a rewrite
|
||||
/// that delegated to the full sampler would pass everything but this.
|
||||
#[test]
|
||||
fn the_memory_only_sample_leaves_the_counts_unset() {
|
||||
let usage = sample_process_memory();
|
||||
|
||||
assert_eq!((usage.threads, usage.open_files), (None, None));
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "linux"))]
|
||||
assert!(usage.rss_bytes.expect("rss") > 0);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue