caproom 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/caproom.ps1 +218 -0
- package/package.json +1 -1
package/bin/caproom.ps1
CHANGED
|
@@ -21,6 +21,9 @@ usage: caproom [--limit <mb>] [--interval <sec>] -- <command> [args...]
|
|
|
21
21
|
caproom status <pid>
|
|
22
22
|
caproom guard [--threshold <pct>] [--interval <sec>] <pid...>
|
|
23
23
|
caproom init <command> [--limit <mb>]
|
|
24
|
+
caproom top --json [--pid <pid>] [--park-min-mb <mb>]
|
|
25
|
+
caproom watch [--threshold-mb <mb>] [--auto-park] [--auto-wake-free-pct <pct>] [--json] <pid...>
|
|
26
|
+
caproom setup / freemem
|
|
24
27
|
|
|
25
28
|
--limit <mb> memory cap in MB (default: 4096). On Windows this caps
|
|
26
29
|
committed virtual memory (Job Object ProcessMemoryLimit);
|
|
@@ -406,6 +409,204 @@ function Invoke-Capped {
|
|
|
406
409
|
|
|
407
410
|
if ($args.Count -eq 0) { Show-Usage }
|
|
408
411
|
|
|
412
|
+
$script:CaproomNtLoaded = $false
|
|
413
|
+
function Ensure-NtSuspend {
|
|
414
|
+
# Whole-tree park needs NtSuspendProcess/NtResumeProcess (ntdll) —
|
|
415
|
+
# the Windows analogue of kill -STOP/-CONT. Loaded lazily, once.
|
|
416
|
+
if ($script:CaproomNtLoaded) { return }
|
|
417
|
+
try {
|
|
418
|
+
Add-Type -Namespace Caproom -Name Nt -MemberDefinition @'
|
|
419
|
+
[DllImport("ntdll.dll")] public static extern int NtSuspendProcess(IntPtr processHandle);
|
|
420
|
+
[DllImport("ntdll.dll")] public static extern int NtResumeProcess(IntPtr processHandle);
|
|
421
|
+
[DllImport("kernel32.dll", SetLastError=true)] public static extern IntPtr OpenProcess(int desiredAccess, bool inheritHandle, int processId);
|
|
422
|
+
[DllImport("kernel32.dll")] public static extern bool CloseHandle(IntPtr handle);
|
|
423
|
+
'@
|
|
424
|
+
$script:CaproomNtLoaded = $true
|
|
425
|
+
} catch {
|
|
426
|
+
[Console]::Error.WriteLine('caproom watch: cannot load ntdll suspend/resume — --auto-park unavailable')
|
|
427
|
+
throw
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function Get-CaproomSnapshot {
|
|
432
|
+
# One CIM query -> ByPid map, Children map (only live parents), Roots
|
|
433
|
+
# (pids whose parent is not in the snapshot). Mirrors posix read_snapshot.
|
|
434
|
+
$procs = @(Get-CimInstance Win32_Process -Property ProcessId,ParentProcessId,Name,CommandLine,WorkingSetSize)
|
|
435
|
+
$byPid = @{}
|
|
436
|
+
foreach ($p in $procs) { $byPid[[int]$p.ProcessId] = $p }
|
|
437
|
+
$children = @{}
|
|
438
|
+
foreach ($p in $procs) {
|
|
439
|
+
$ppid = [int]$p.ParentProcessId
|
|
440
|
+
if ($byPid.ContainsKey($ppid)) {
|
|
441
|
+
if (-not $children.ContainsKey($ppid)) { $children[$ppid] = New-Object System.Collections.Generic.List[int] }
|
|
442
|
+
$children[$ppid].Add([int]$p.ProcessId)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
$roots = @($procs | Where-Object { -not $byPid.ContainsKey([int]$_.ParentProcessId) } | ForEach-Object { [int]$_.ProcessId })
|
|
446
|
+
return @{ ByPid=$byPid; Children=$children; Roots=$roots }
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
function Get-TreeStats {
|
|
450
|
+
param([hashtable]$Snap, [int]$RootPid)
|
|
451
|
+
$rss = [long]0
|
|
452
|
+
$pids = New-Object System.Collections.Generic.List[int]
|
|
453
|
+
$stack = New-Object System.Collections.Generic.Stack[int]
|
|
454
|
+
$seen = @{}
|
|
455
|
+
$stack.Push($RootPid)
|
|
456
|
+
while ($stack.Count -gt 0) {
|
|
457
|
+
$cur = $stack.Pop()
|
|
458
|
+
if ($seen.ContainsKey($cur)) { continue }
|
|
459
|
+
$seen[$cur] = $true
|
|
460
|
+
if (-not $Snap.ByPid.ContainsKey($cur)) { continue }
|
|
461
|
+
$proc = $Snap.ByPid[$cur]
|
|
462
|
+
if ($proc.WorkingSetSize) { $rss += [long]$proc.WorkingSetSize }
|
|
463
|
+
$pids.Add($cur)
|
|
464
|
+
if ($Snap.Children.ContainsKey($cur)) { foreach ($c in $Snap.Children[$cur]) { $stack.Push($c) } }
|
|
465
|
+
}
|
|
466
|
+
return @{ RssKb = [long]($rss / 1KB); Pids = $pids }
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function Invoke-Top {
|
|
470
|
+
# schema:1 rows identical in shape to the POSIX build. One honest
|
|
471
|
+
# divergence: Windows exposes no cheap sleep-state, so state is always
|
|
472
|
+
# 'running' and park_candidate keys off tree size alone — the reason
|
|
473
|
+
# string says so instead of pretending a sleep check happened.
|
|
474
|
+
param([int]$FilterPid = 0, [int]$ParkMinMb = 512)
|
|
475
|
+
$snap = Get-CaproomSnapshot
|
|
476
|
+
$ts = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
|
|
477
|
+
$limitMb = 4096; if ($env:CAPROOM_LIMIT_MB) { $limitMb = [int]$env:CAPROOM_LIMIT_MB }
|
|
478
|
+
$parkMinKb = [long]$ParkMinMb * 1024
|
|
479
|
+
$rows = New-Object System.Collections.Generic.List[object]
|
|
480
|
+
foreach ($root in $snap.Roots) {
|
|
481
|
+
if ($FilterPid -ne 0 -and $root -ne $FilterPid) { continue }
|
|
482
|
+
$st = Get-TreeStats -Snap $snap -RootPid $root
|
|
483
|
+
$cmd = ''
|
|
484
|
+
if ($snap.ByPid.ContainsKey($root)) {
|
|
485
|
+
$procRow = $snap.ByPid[$root]
|
|
486
|
+
if ($procRow.CommandLine) { $cmd = [string]$procRow.CommandLine } else { $cmd = [string]$procRow.Name }
|
|
487
|
+
}
|
|
488
|
+
$cand = $false; $reason = ''
|
|
489
|
+
if ([long]$st.RssKb -ge $parkMinKb) {
|
|
490
|
+
$cand = $true
|
|
491
|
+
$reason = "tree_rss $($st.RssKb)KB >= ${parkMinKb}KB park threshold (win32: no sleep-state check)"
|
|
492
|
+
}
|
|
493
|
+
$rows.Add([pscustomobject]@{
|
|
494
|
+
pid = $root; cmd = $cmd; tree_rss_kb = $st.RssKb
|
|
495
|
+
tree_pids = @($st.Pids.ToArray()); state = 'running'
|
|
496
|
+
park_candidate = $cand; reason = $reason
|
|
497
|
+
})
|
|
498
|
+
}
|
|
499
|
+
$envelope = [pscustomobject]@{ schema = 1; ts = $ts; limit_mb_default = $limitMb; processes = @($rows.ToArray()) }
|
|
500
|
+
ConvertTo-Json -Compress -Depth 6 -InputObject $envelope
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
function Invoke-Watch {
|
|
504
|
+
# Same NDJSON contract as the POSIX watcher (schema:1 events on stdout
|
|
505
|
+
# under --json). Explicit pids only; naming the pid IS the opt-in for
|
|
506
|
+
# --auto-park, same rule as POSIX.
|
|
507
|
+
$thresholdMb = 2048; $intervalSec = 5.0; $auto = $false; $wake = -1.0; $json = $false
|
|
508
|
+
$targets = New-Object System.Collections.Generic.List[int]
|
|
509
|
+
for ($i = 0; $i -lt $args.Count; $i++) {
|
|
510
|
+
switch ($args[$i]) {
|
|
511
|
+
'--threshold-mb' { $thresholdMb = [int]$args[$i + 1]; $i++ }
|
|
512
|
+
'--interval' { $intervalSec = [double]$args[$i + 1]; $i++ }
|
|
513
|
+
'--auto-park' { $auto = $true }
|
|
514
|
+
'--auto-wake-free-pct' { $wake = [double]$args[$i + 1]; $i++ }
|
|
515
|
+
'--json' { $json = $true }
|
|
516
|
+
default {
|
|
517
|
+
try { $targets.Add([int]$args[$i]) }
|
|
518
|
+
catch { [Console]::Error.WriteLine("caproom: unknown watch arg $($args[$i])"); exit 1 }
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
if ($targets.Count -eq 0) {
|
|
523
|
+
[Console]::Error.WriteLine('usage: caproom watch [--threshold-mb <mb>] [--interval <sec>] [--auto-park] [--auto-wake-free-pct <pct>] [--json] <pid...>')
|
|
524
|
+
exit 1
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function Emit([object]$Ev) {
|
|
528
|
+
[Console]::Out.WriteLine((ConvertTo-Json -Compress -Depth 6 -InputObject $Ev))
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
$mode = 'watch'; if ($auto) { $mode = 'auto-park' }
|
|
532
|
+
$ts0 = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
|
|
533
|
+
if ($json) {
|
|
534
|
+
Emit ([pscustomobject]@{ schema = 1; event = 'started'; ts = $ts0; mode = $mode; threshold_kb = ($thresholdMb * 1024); pids = @($targets.ToArray()) })
|
|
535
|
+
} else {
|
|
536
|
+
$armed = ''; if ($auto) { $armed = ', AUTO-PARK ARMED' }
|
|
537
|
+
[Console]::Error.WriteLine("caproom: watching $($targets.Count) pid(s), tree threshold ${thresholdMb}MB, poll ${intervalSec}s$armed")
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
$parkedByUs = New-Object System.Collections.Generic.List[int]
|
|
541
|
+
$breaching = @{}
|
|
542
|
+
while ($true) {
|
|
543
|
+
$liveSet = @{}
|
|
544
|
+
foreach ($q in @(Get-CimInstance Win32_Process -Property ProcessId)) { $liveSet[[int]$q.ProcessId] = $true }
|
|
545
|
+
$alive = New-Object System.Collections.Generic.List[int]
|
|
546
|
+
foreach ($tpid in $targets) { if ($liveSet.ContainsKey($tpid)) { $alive.Add($tpid) } }
|
|
547
|
+
if ($alive.Count -eq 0) {
|
|
548
|
+
if ($json) { Emit ([pscustomobject]@{ schema = 1; event = 'all-exited'; ts = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds() }) }
|
|
549
|
+
[Console]::Error.WriteLine('caproom: watch: all watched pids exited')
|
|
550
|
+
exit 0
|
|
551
|
+
}
|
|
552
|
+
$targets = $alive
|
|
553
|
+
|
|
554
|
+
if ($wake -ge 0 -and $parkedByUs.Count -gt 0) {
|
|
555
|
+
$os = Get-CimInstance Win32_OperatingSystem
|
|
556
|
+
$pct = [int]($os.FreePhysicalMemory * 100 / $os.TotalVisibleMemorySize)
|
|
557
|
+
if ($pct -ge $wake) {
|
|
558
|
+
Ensure-NtSuspend
|
|
559
|
+
foreach ($wpid in @($parkedByUs.ToArray())) {
|
|
560
|
+
if (-not $liveSet.ContainsKey($wpid)) { continue }
|
|
561
|
+
$h = [Caproom.Nt]::OpenProcess(0x0800, $false, $wpid)
|
|
562
|
+
if ($h -ne [IntPtr]::Zero) {
|
|
563
|
+
[void][Caproom.Nt]::NtResumeProcess($h); [void][Caproom.Nt]::CloseHandle($h)
|
|
564
|
+
if ($json) { Emit ([pscustomobject]@{ schema = 1; event = 'woke'; ts = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds(); pid = $wpid; free_pct = $pct }) }
|
|
565
|
+
else { [Console]::Error.WriteLine("caproom: watch: free mem ${pct}% >= ${wake}% — resuming pid $wpid") }
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
$parkedByUs.Clear()
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
$snap = Get-CaproomSnapshot
|
|
573
|
+
$threshKb = [long]$thresholdMb * 1024
|
|
574
|
+
foreach ($tpid in $targets) {
|
|
575
|
+
if (-not $snap.ByPid.ContainsKey($tpid)) { continue }
|
|
576
|
+
$st = Get-TreeStats -Snap $snap -RootPid $tpid
|
|
577
|
+
if ([long]$st.RssKb -ge $threshKb) {
|
|
578
|
+
if ($breaching.ContainsKey($tpid)) { continue }
|
|
579
|
+
$breaching[$tpid] = $true
|
|
580
|
+
$now = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
|
|
581
|
+
if ($auto) {
|
|
582
|
+
Ensure-NtSuspend
|
|
583
|
+
$stopped = 0
|
|
584
|
+
foreach ($cp in $st.Pids) {
|
|
585
|
+
$h = [Caproom.Nt]::OpenProcess(0x0800, $false, $cp)
|
|
586
|
+
if ($h -ne [IntPtr]::Zero) {
|
|
587
|
+
[void][Caproom.Nt]::NtSuspendProcess($h); [void][Caproom.Nt]::CloseHandle($h)
|
|
588
|
+
$parkedByUs.Add($cp); $stopped++
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
if ($json) { Emit ([pscustomobject]@{ schema = 1; event = 'parked'; ts = $now; pid = $tpid; tree_rss_kb = $st.RssKb; tree_pids = @($st.Pids.ToArray()); stopped = $stopped }) }
|
|
592
|
+
else { [Console]::Error.WriteLine("caproom: watch: tree of pid $tpid hit $($st.RssKb)KB (>= $([int]$threshKb)KB) — PARKED tree ($stopped pids)") }
|
|
593
|
+
} else {
|
|
594
|
+
if ($json) { Emit ([pscustomobject]@{ schema = 1; event = 'breach'; ts = $now; pid = $tpid; tree_rss_kb = $st.RssKb }) }
|
|
595
|
+
else { [Console]::Error.WriteLine("caproom: watch: tree of pid $tpid hit $($st.RssKb)KB (>= $([int]$threshKb)KB) — no --auto-park, reporting only") }
|
|
596
|
+
}
|
|
597
|
+
} else {
|
|
598
|
+
if ($breaching.ContainsKey($tpid)) {
|
|
599
|
+
$breaching.Remove($tpid)
|
|
600
|
+
$now2 = [DateTimeOffset]::UtcNow.ToUnixTimeSeconds()
|
|
601
|
+
if ($json) { Emit ([pscustomobject]@{ schema = 1; event = 'recovered'; ts = $now2; pid = $tpid; tree_rss_kb = $st.RssKb }) }
|
|
602
|
+
else { [Console]::Error.WriteLine("caproom: watch: pid $tpid back under threshold ($($st.RssKb)KB)") }
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
Start-Sleep -Seconds $intervalSec
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
409
610
|
function Invoke-Setup {
|
|
410
611
|
# Bind headroom management to PowerShell sessions in ANY terminal:
|
|
411
612
|
# writes ~/.caproom/shell.ps1 (single source) and marker-patches
|
|
@@ -468,6 +669,23 @@ switch ($args[0]) {
|
|
|
468
669
|
Write-Output ([int]($os.FreePhysicalMemory * 100 / $os.TotalVisibleMemorySize))
|
|
469
670
|
exit 0
|
|
470
671
|
}
|
|
672
|
+
'top' {
|
|
673
|
+
$fpid = 0; $parkMin = 512
|
|
674
|
+
for ($i = 1; $i -lt $args.Count; $i++) {
|
|
675
|
+
switch ($args[$i]) {
|
|
676
|
+
'--json' { }
|
|
677
|
+
'--pid' { $fpid = [int]$args[$i + 1]; $i++ }
|
|
678
|
+
'--park-min-mb' { $parkMin = [int]$args[$i + 1]; $i++ }
|
|
679
|
+
default { [Console]::Error.WriteLine("caproom: unknown top flag $($args[$i])"); exit 1 }
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
Invoke-Top -FilterPid $fpid -ParkMinMb $parkMin
|
|
683
|
+
exit 0
|
|
684
|
+
}
|
|
685
|
+
'watch' {
|
|
686
|
+
Invoke-Watch @($args | Select-Object -Skip 1)
|
|
687
|
+
exit 0
|
|
688
|
+
}
|
|
471
689
|
'park' {
|
|
472
690
|
if ($args.Count -lt 2) { [Console]::Error.WriteLine('usage: caproom park <pid>'); exit 1 }
|
|
473
691
|
Invoke-Park -TargetPid ([int]$args[1]); exit 0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "caproom",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Memory-cap any command (AI coding agents, builds, background jobs) on macOS, Linux, and Windows — real enforcement via Docker cgroups, Windows Job Objects, or a polling watchdog, plus park/wake to reclaim idle process memory without killing.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"caproom": "bin/caproom.js",
|