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>
101 lines
5.6 KiB
PowerShell
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 })
|