igneum/tools/windows/console-watch-bg.ps1
igneum-labs 27c2db64c3 app: every elevated launch through one hidden-console builder; CI check for Windows spawns; PC 1 console watchers
The console-window class (the project lead, 5 October 2026: "Windows Command Processor" windows on PC 1 whenever a remote job runs).
Measured on PC 1 (ae432dc7, Windows 11 Pro 26200, default terminal "Let Windows decide" = Windows Terminal 1.24) with
tools/windows/console-watch.ps1 (job run-20261005-182528): no child a job script starts from the app's headless
console opens a window (powershell, cmd, query, curl, nvidia-smi, wsl --status, a distro, interop cmd and powershell,
powershell -WindowStyle Hidden: 0 windows each); Start-Process in a new console opens a Terminal window (the known-failed
case: 2 windows), the same with -WindowStyle Hidden opens none (the known-finished case). The elevated path
(Start-Process -Verb RunAs -WindowStyle Hidden through the AppInfo service) is the one road left; its watcher
(console-watch-elevated.ps1, job run-20261005-184610) was cancelled at the UAC prompt.

- platform.rs: elevated_ps_line + elevated_command build the one PowerShell line every elevated launch uses (the NVIDIA
  power cap, the sweep helper, the clock sync, an elevated remote job), -WindowStyle Hidden by construction; unit
  tests on the line, the quoting and the Command.
- jobrun.rs: the elevated job path uses it; the relaunch helper's Start-Process carries the reason it has no
  -WindowStyle Hidden (igneum-app.exe is a windows-subsystem program).
- tools/ci/windows-spawn-check.mjs (+ ci.yml): fails when a Command::new in app/igneum-app/src is not quieted,
  a creation_flags is not CREATE_NO_WINDOW alone, a Start-Process the Rust code writes lacks -WindowStyle Hidden or
  -NoNewWindow, or host.cpp spawns without CREATE_NO_WINDOW / SW_HIDE; self-test on known-good and known-bad samples.
- tools/windows/console-watch.ps1, console-watch-bg.ps1, console-watch-elevated.ps1: the watchers (run jobs).

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-06 08:21:46 +00:00

101 lines
5.6 KiB
PowerShell

# Background console-window watcher for a Windows PC running the Igneum Miner app: the second half of
# tools/windows/console-watch.ps1. That one proved (PC 1, 5 October 2026, job run-20261005-182528) that no child a
# job script starts from the app's headless console opens a window; this one finds what does. A signed `run` job
# starts a detached PowerShell (Start-Process -WindowStyle Hidden, the shape the first watcher showed opens nothing)
# and returns at once; the detached process samples for WATCH_MINUTES and writes <job dir>\console-windows.log:
# <utc> window <process> pid <p> [<class>] <title> a new visible console or terminal window
# <utc> proc <name> pid <p> cmd <command line> <- <parent chain, name pid and command line each>
# a new cmd, powershell, wsl, conhost, OpenConsole or WindowsTerminal
# <utc> gone <name> pid <p> one of those ended (the window's life)
# Read it back with a collect job: packaging/ota/publish-jobs.sh add --kind collect --target ae432dc7 \
# --glob "app/jobs/<this job id>/console-windows.log" --deploy
$ErrorActionPreference = 'Continue'
$minutes = 25
$dir = $env:IGNEUM_JOB_DIR
$log = Join-Path $dir 'console-windows.log'
$bg = Join-Path $dir 'bg.ps1'
# the detached body: params first (PowerShell wants them at the top), then the sampler
$body = @'
param([string] $Log, [int] $Minutes)
$ErrorActionPreference = 'Continue'
$src = @"
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
public static class IgWin2 {
public delegate bool EnumProc(IntPtr h, IntPtr l);
[DllImport("user32.dll")] public static extern bool EnumWindows(EnumProc p, IntPtr l);
[DllImport("user32.dll")] public static extern bool IsWindowVisible(IntPtr h);
[DllImport("user32.dll")] public static extern uint GetWindowThreadProcessId(IntPtr h, out uint pid);
[DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetWindowText(IntPtr h, StringBuilder s, int n);
[DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int GetClassName(IntPtr h, StringBuilder s, int n);
public static List<string> Visible() {
var list = new List<string>();
EnumWindows((h, l) => {
if (!IsWindowVisible(h)) return true;
uint pid; GetWindowThreadProcessId(h, out pid);
var t = new StringBuilder(512); GetWindowText(h, t, 512);
var c = new StringBuilder(256); GetClassName(h, c, 256);
list.Add(((long)h).ToString() + "|" + pid + "|" + c + "|" + t);
return true;
}, IntPtr.Zero);
return list;
}
}
"@
Add-Type -TypeDefinition $src
function Now() { (Get-Date).ToUniversalTime().ToString('HH:mm:ss.fff') }
function Put([string] $l) { Add-Content -Path $Log -Value $l -Encoding UTF8 }
function Chain([int] $procId) {
$out = @(); $seen = @{}; $p = $procId
for ($i = 0; $i -lt 6 -and $p -gt 0 -and -not $seen.ContainsKey($p); $i++) {
$seen[$p] = 1
$ci = Get-CimInstance Win32_Process -Filter "ProcessId=$p" -ErrorAction SilentlyContinue
if (-not $ci) { $out += ("pid " + $p + " gone"); break }
$cl = [string]$ci.CommandLine; if ($cl.Length -gt 160) { $cl = $cl.Substring(0, 160) + '...' }
$out += ($ci.Name + " pid " + $p + " [" + $cl + "]")
$p = $ci.ParentProcessId
}
return ($out -join ' <- ')
}
$watch = 'cmd', 'powershell', 'pwsh', 'wsl', 'wslhost', 'conhost', 'OpenConsole', 'WindowsTerminal'
$classes = 'ConsoleWindowClass', 'CASCADIA_HOSTING_WINDOW_CLASS', 'PseudoConsoleWindow'
$knownWin = @{}; $knownProc = @{}
$first = $true
$end = (Get-Date).AddMinutes($Minutes)
Put ((Now) + " start: watching for " + $Minutes + " min, pid " + $PID)
while ((Get-Date) -lt $end) {
try {
foreach ($w in [IgWin2]::Visible()) {
$f = $w.Split('|', 4)
if ($knownWin.ContainsKey($f[0])) { continue }
$knownWin[$f[0]] = 1
if ($first) { continue }
$pn = try { (Get-Process -Id ([int]$f[1]) -ErrorAction Stop).ProcessName } catch { 'gone' }
if ($classes -contains $f[2] -or $watch -contains $pn) { Put ((Now) + " window " + $pn + " pid " + $f[1] + " [" + $f[2] + "] " + $f[3]) }
}
$live = @{}
foreach ($p in @(Get-Process -Name $watch -ErrorAction SilentlyContinue)) {
$live[$p.Id] = 1
if ($knownProc.ContainsKey($p.Id)) { continue }
$knownProc[$p.Id] = $p.ProcessName
if ($first) { continue }
Put ((Now) + " proc " + $p.ProcessName + " pid " + $p.Id + " " + (Chain $p.Id))
}
foreach ($k in @($knownProc.Keys)) { if (-not $live.ContainsKey($k)) { if (-not $first) { Put ((Now) + " gone " + $knownProc[$k] + " pid " + $k) }; $knownProc.Remove($k) } }
if ($first) { Put ((Now) + " baseline: " + $knownWin.Count + " visible windows, " + $knownProc.Count + " watched processes: " + (($knownProc.GetEnumerator() | ForEach-Object { $_.Value + ' ' + $_.Key }) -join ', ')) }
$first = $false
} catch { Put ((Now) + " error " + $_) }
Start-Sleep -Milliseconds 200
}
Put ((Now) + " end")
'@
[IO.File]::WriteAllText($bg, $body, (New-Object System.Text.UTF8Encoding($true)))
if (Test-Path $log) { Remove-Item $log -Force }
$p = Start-Process -FilePath powershell.exe -ArgumentList @('-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', $bg, '-Log', $log, '-Minutes', $minutes) -WindowStyle Hidden -PassThru
Start-Sleep -Seconds 3
$alive = try { -not (Get-Process -Id $p.Id -ErrorAction Stop).HasExited } catch { $false }
Write-Output ("RESULT watcher: pid " + $p.Id + " alive " + $alive + " for " + $minutes + " min, log " + $log)
if (Test-Path $log) { Get-Content $log | ForEach-Object { Write-Output ("RESULT first: " + $_) } }
exit $(if ($alive) { 0 } else { 1 })