# Console-window watcher for a Windows PC running the Igneum Miner app. A signed `run` job (app/igneum-app/src/jobrun.rs, # shell powershell, not elevated): while a sampler thread enumerates the visible top-level windows (user32 EnumWindows, # IsWindowVisible, GetWindowThreadProcessId, GetClassName, GetWindowText) and the console host processes (conhost, # OpenConsole, WindowsTerminal, with their command lines and parents) every 30 ms, the main thread starts each # candidate child the way a job script or the app does, and every window or host that appears during a probe is # reported against it: # RESULT terminal: ... the default-terminal delegation (HKCU\Console\%%Startup) and the host process counts # RESULT self: ... the job's own console (hidden or not) and the conhost that serves it # RESULT probe : ... exit code, duration, how many windows and hosts appeared # RESULT window: [] (probe <n>) # RESULT host: <name> pid <p> parent <process> cmd <command line> (probe <n>) # Trust test (CLAUDE.md: a watcher is trusted only after a known-finished and a known-failed case): probe # start-process-new-console MUST report a window (cmd in a new console); start-process-hidden is the same with # -WindowStyle Hidden. 5 October 2026: written for PC 1 (ae432dc7, Windows 11 Pro 26200), where the project lead saw # "Windows Command Processor" windows whenever a remote job ran. # packaging/ota/publish-jobs.sh add --kind run --target ae432dc7 --script tools/windows/console-watch.ps1 \ # --timeout-minutes 5 --title "PC 1: console window watcher" --deploy $ErrorActionPreference = 'Continue' $src = @' using System; using System.Collections.Generic; using System.Runtime.InteropServices; using System.Text; public static class IgWin { 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); [DllImport("kernel32.dll")] public static extern IntPtr GetConsoleWindow(); 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; } } '@ try { Add-Type -TypeDefinition $src -ErrorAction Stop } catch { if (-not ([System.Management.Automation.PSTypeName]'IgWin').Type) { Write-Output ("RESULT error: Add-Type failed: " + $_); exit 2 } } function Say([string] $m) { Write-Output $m } function ProcName([int] $procId) { try { (Get-Process -Id $procId -ErrorAction Stop).ProcessName } catch { 'gone' } } # ---- the default terminal and the hosts present before anything starts ------------------------------------------------ $CONHOST_ID = '{B23D10C0-E52E-411E-9D5B-C09FDF709C7D}' $DECIDE_ID = '{00000000-0000-0000-0000-000000000000}' $k = Get-ItemProperty -Path 'HKCU:\Console\%%Startup' -ErrorAction SilentlyContinue $dc = if ($k) { [string]$k.DelegationConsole } else { '(absent)' } $dt = if ($k) { [string]$k.DelegationTerminal } else { '(absent)' } $meaning = if ($dc -eq $CONHOST_ID) { 'Windows Console Host (conhost)' } elseif ($dc -eq $DECIDE_ID -or $dc -eq '(absent)') { 'Let Windows decide (Windows Terminal on Windows 11 22H2 and later when it is installed)' } else { 'a terminal package, Windows Terminal or its preview' } $wtPkg = (Get-AppxPackage -Name 'Microsoft.WindowsTerminal*' -ErrorAction SilentlyContinue | ForEach-Object { $_.Name + ' ' + $_.Version }) -join ', ' Say ("RESULT terminal: DelegationConsole=" + $dc + " DelegationTerminal=" + $dt + " -> " + $meaning + "; Windows Terminal package: " + $(if ($wtPkg) { $wtPkg } else { 'none' })) $os = Get-CimInstance Win32_OperatingSystem Say ("RESULT os: " + $os.Caption + " build " + $os.BuildNumber + " user " + $env:USERNAME + " session " + [System.Diagnostics.Process]::GetCurrentProcess().SessionId) $counts = @{} foreach ($n in 'conhost', 'OpenConsole', 'WindowsTerminal', 'cmd', 'powershell', 'wsl', 'wslhost') { $counts[$n] = @(Get-Process -Name $n -ErrorAction SilentlyContinue).Count } Say ("RESULT hosts-before: conhost " + $counts['conhost'] + " OpenConsole " + $counts['OpenConsole'] + " WindowsTerminal " + $counts['WindowsTerminal'] + " cmd " + $counts['cmd'] + " powershell " + $counts['powershell'] + " wsl " + $counts['wsl'] + " wslhost " + $counts['wslhost']) # the job's own console and the chain above it $me = [System.Diagnostics.Process]::GetCurrentProcess() $meCim = Get-CimInstance Win32_Process -Filter "ProcessId=$($me.Id)" $parent = if ($meCim) { ProcName $meCim.ParentProcessId } else { '?' } $hwnd = [IgWin]::GetConsoleWindow() $selfVis = if ($hwnd -ne [IntPtr]::Zero) { [IgWin]::IsWindowVisible($hwnd) } else { 'no window' } $ownHost = Get-CimInstance Win32_Process -Filter "Name='conhost.exe' OR Name='OpenConsole.exe'" | Where-Object { $_.ParentProcessId -eq $me.Id -or $_.ParentProcessId -eq $meCim.ParentProcessId } $ownLine = if ($ownHost) { ($ownHost | ForEach-Object { $_.Name + ' pid ' + $_.ProcessId + ' parent ' + (ProcName $_.ParentProcessId) + ' cmd ' + $_.CommandLine }) -join ' ; ' } else { 'none with this script or its parent as parent' } Say ("RESULT self: powershell pid " + $me.Id + " parent " + $parent + " (pid " + $meCim.ParentProcessId + "); console hwnd " + $hwnd + " visible " + $selfVis + "; host " + $ownLine) foreach ($w in [IgWin]::Visible()) { $f = $w.Split('|', 4) if ($f[2] -eq 'ConsoleWindowClass' -or $f[2] -eq 'CASCADIA_HOSTING_WINDOW_CLASS') { Say ("RESULT window-before: " + (ProcName ([int]$f[1])) + " [" + $f[2] + "] " + $f[3]) } } # ---- the sampler thread: every new visible window and every new console host, with the time it was first seen ------- $sync = [hashtable]::Synchronized(@{ win = [hashtable]::Synchronized(@{}); hosts = [hashtable]::Synchronized(@{}); stop = $false; ticks = 0; err = '' }) $rs = [runspacefactory]::CreateRunspace() $rs.Open() $rs.SessionStateProxy.SetVariable('sync', $sync) $rs.SessionStateProxy.SetVariable('src', $src) $sampler = [powershell]::Create() $sampler.Runspace = $rs [void]$sampler.AddScript({ try { if (-not ([System.Management.Automation.PSTypeName]'IgWin').Type) { Add-Type -TypeDefinition $src } $first = $true while (-not $sync.stop) { $now = Get-Date foreach ($w in [IgWin]::Visible()) { $f = $w.Split('|', 4) if (-not $sync.win.ContainsKey($f[0])) { $pn = try { (Get-Process -Id ([int]$f[1]) -ErrorAction Stop).ProcessName } catch { 'gone' } $sync.win[$f[0]] = @{ t = $now; procId = [int]$f[1]; proc = $pn; cls = $f[2]; title = $f[3]; base = $first } } } foreach ($p in @(Get-Process -Name conhost, OpenConsole, WindowsTerminal -ErrorAction SilentlyContinue)) { if (-not $sync.hosts.ContainsKey($p.Id)) { $ci = Get-CimInstance Win32_Process -Filter "ProcessId=$($p.Id)" -ErrorAction SilentlyContinue $ppid = if ($ci) { $ci.ParentProcessId } else { 0 } $ppn = try { (Get-Process -Id $ppid -ErrorAction Stop).ProcessName } catch { 'gone' } $sync.hosts[$p.Id] = @{ t = $now; name = $p.ProcessName; cmd = $(if ($ci) { [string]$ci.CommandLine } else { '?' }); ppid = $ppid; pproc = $ppn; base = $first } } } $first = $false $sync.ticks++ Start-Sleep -Milliseconds 30 } } catch { $sync.err = [string]$_ } }) $handle = $sampler.BeginInvoke() $t = 0 while ($sync.ticks -lt 2 -and $t -lt 100) { Start-Sleep -Milliseconds 50; $t++ } if ($sync.ticks -lt 2) { Say ("RESULT error: the sampler did not start: " + $sync.err); exit 2 } Say ("sampler running: " + $sync.win.Count + " visible windows and " + $sync.hosts.Count + " console hosts at the start") # ---- probes: each one as a job script or the app would start it ---------------------------------------------------- $report = New-Object System.Collections.ArrayList function Probe([string] $name, [scriptblock] $body) { Start-Sleep -Milliseconds 400 $t0 = Get-Date $global:LASTEXITCODE = 0 $err = '' try { & $body 2>&1 | Out-Null } catch { $err = [string]$_ } $rc = $LASTEXITCODE Start-Sleep -Milliseconds 600 $t1 = Get-Date $ms = [int]($t1 - $t0).TotalMilliseconds - 600 $wins = @($sync.win.GetEnumerator() | Where-Object { -not $_.Value.base -and $_.Value.t -ge $t0 -and $_.Value.t -le $t1 -and -not $_.Value.reported }) $hosts = @($sync.hosts.GetEnumerator() | Where-Object { -not $_.Value.base -and $_.Value.t -ge $t0 -and $_.Value.t -le $t1 -and -not $_.Value.reported }) Say ("RESULT probe " + $name + ": exit " + $rc + " in " + $ms + " ms, " + $wins.Count + " window(s), " + $hosts.Count + " host(s)" + $(if ($err) { "; error " + $err } else { '' })) foreach ($w in $wins) { $w.Value.reported = $true; Say ("RESULT window: " + $w.Value.proc + " [" + $w.Value.cls + "] " + $w.Value.title + " (probe " + $name + ")") } foreach ($h in $hosts) { $h.Value.reported = $true; Say ("RESULT host: " + $h.Value.name + " pid " + $h.Key + " parent " + $h.Value.pproc + " cmd " + $h.Value.cmd + " (probe " + $name + ")") } } $distro = 'Ubuntu-24.04' $haveDistro = $false try { $haveDistro = ((& wsl.exe -l -q 2>$null) -replace "`0", '' | Where-Object { $_.Trim() -eq $distro }).Count -gt 0 } catch {} Say ("wsl distro " + $distro + ": " + $(if ($haveDistro) { 'present' } else { 'absent, the distro probes are skipped' })) # a. the plain children a job script starts (CreateProcess, the console inherited) Probe 'powershell-inherit' { & powershell.exe -NoProfile -ExecutionPolicy Bypass -Command 'Start-Sleep -Milliseconds 1200' } Probe 'cmd-c-inherit' { & cmd.exe /c 'ping -n 3 127.0.0.1 >nul' } Probe 'query-session' { & query.exe session } Probe 'curl-version' { & curl.exe --version } Probe 'nvidia-smi-L' { & nvidia-smi.exe -L } Probe 'powershell-windowstyle-hidden-inherit' { & powershell.exe -NoProfile -WindowStyle Hidden -Command 'Start-Sleep -Milliseconds 1200' } Probe 'wsl-status' { & wsl.exe --status } if ($haveDistro) { Probe 'wsl-distro-sleep' { & wsl.exe -d Ubuntu-24.04 -u root -- sleep 1 } Probe 'wsl-interop-cmd' { & wsl.exe -d Ubuntu-24.04 -u root -- cmd.exe /c 'ping -n 3 127.0.0.1' } Probe 'wsl-interop-powershell' { & wsl.exe -d Ubuntu-24.04 -u root -- powershell.exe -NoProfile -Command 'Start-Sleep -Milliseconds 1200' } } # b. the trust test: a new console (ShellExecute) must show; the same hidden Probe 'start-process-new-console' { Start-Process -FilePath cmd.exe -ArgumentList '/c ping -n 3 127.0.0.1' -Wait } Probe 'start-process-hidden' { Start-Process -FilePath cmd.exe -ArgumentList '/c ping -n 3 127.0.0.1' -WindowStyle Hidden -Wait } # c. the elevated job's shape without the elevation: powershell in a new hidden console through ShellExecute Probe 'start-process-powershell-hidden' { Start-Process -FilePath powershell.exe -ArgumentList '-NoProfile -ExecutionPolicy Bypass -Command Start-Sleep -Milliseconds 1200' -WindowStyle Hidden -Wait } # d. candidate fixes: a headless conhost of our own around the child; the children of a headless session Probe 'conhost-headless-cmd' { & conhost.exe --headless cmd.exe /c 'ping -n 3 127.0.0.1 >nul' } if ($haveDistro) { Probe 'conhost-headless-wsl-interop' { & conhost.exe --headless wsl.exe -d Ubuntu-24.04 -u root -- cmd.exe /c 'ping -n 3 127.0.0.1' } } Probe 'conhost-headless-start-process-hidden' { & conhost.exe --headless powershell.exe -NoProfile -Command "Start-Process -FilePath cmd.exe -ArgumentList '/c ping -n 3 127.0.0.1' -WindowStyle Hidden -Wait" } # ---- the end: anything the probes did not claim --------------------------------------------------------------------- Start-Sleep -Milliseconds 800 $sync.stop = $true try { [void]$sampler.EndInvoke($handle) } catch {} $sampler.Dispose(); $rs.Close() $stray = @($sync.win.GetEnumerator() | Where-Object { -not $_.Value.base -and -not $_.Value.reported }) foreach ($w in $stray) { Say ("RESULT window: " + $w.Value.proc + " [" + $w.Value.cls + "] " + $w.Value.title + " (between probes)") } $strayH = @($sync.hosts.GetEnumerator() | Where-Object { -not $_.Value.base -and -not $_.Value.reported }) foreach ($h in $strayH) { Say ("RESULT host: " + $h.Value.name + " pid " + $h.Key + " parent " + $h.Value.pproc + " cmd " + $h.Value.cmd + " (between probes)") } $counts = @{} foreach ($n in 'conhost', 'OpenConsole', 'WindowsTerminal') { $counts[$n] = @(Get-Process -Name $n -ErrorAction SilentlyContinue).Count } Say ("RESULT hosts-after: conhost " + $counts['conhost'] + " OpenConsole " + $counts['OpenConsole'] + " WindowsTerminal " + $counts['WindowsTerminal'] + "; sampler ticks " + $sync.ticks + $(if ($sync.err) { "; sampler error " + $sync.err } else { '' })) exit 0