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>
This commit is contained in:
parent
563463b35f
commit
27c2db64c3
10 changed files with 518 additions and 20 deletions
2
.github/workflows/ci.yml
vendored
2
.github/workflows/ci.yml
vendored
|
|
@ -77,6 +77,8 @@ jobs:
|
||||||
run: bash tools/ci/bash-body-check.sh --self-test && bash tools/ci/bash-body-check.sh
|
run: bash tools/ci/bash-body-check.sh --self-test && bash tools/ci/bash-body-check.sh
|
||||||
- name: run jobs test their fetched kit before use, the wiped-jobs-folder class (self-test first, then the tree)
|
- name: run jobs test their fetched kit before use, the wiped-jobs-folder class (self-test first, then the tree)
|
||||||
run: bash tools/ci/kit-path-check.sh --self-test && bash tools/ci/kit-path-check.sh
|
run: bash tools/ci/kit-path-check.sh --self-test && bash tools/ci/kit-path-check.sh
|
||||||
|
- name: every Windows spawn of the app runs with a hidden console (self-test first, then the tree)
|
||||||
|
run: node tools/ci/windows-spawn-check.mjs --self-test && node tools/ci/windows-spawn-check.mjs
|
||||||
- name: pinned guest programs match their manifest and are built only by pin-guests.sh
|
- name: pinned guest programs match their manifest and are built only by pin-guests.sh
|
||||||
run: bash tools/ci/pinned-guests-check.sh
|
run: bash tools/ci/pinned-guests-check.sh
|
||||||
- name: root prover playbooks kill the GPU server and unlink its socket (the root-socket class, 5 October 2026)
|
- name: root prover playbooks kill the GPU server and unlink its socket (the root-socket class, 5 October 2026)
|
||||||
|
|
|
||||||
|
|
@ -1115,16 +1115,11 @@ fn run_script(shared: &Arc<Shared>, job: &Job, sink: &Sink, jobs_dir: &Path, dat
|
||||||
.map(|(k, v)| format!("$env:{k} = '{}'\r\n", v.replace('\'', "''")))
|
.map(|(k, v)| format!("$env:{k} = '{}'\r\n", v.replace('\'', "''")))
|
||||||
.collect();
|
.collect();
|
||||||
let wrapper = dir.join("elevated.ps1");
|
let wrapper = dir.join("elevated.ps1");
|
||||||
let w = format!("{env_lines}& '{}' *>&1 | Out-File -FilePath '{}' -Encoding utf8\r\nexit $LASTEXITCODE\r\n", script.display().to_string().replace('\'', "''"), out_file.display().to_string().replace('\'', "''"));
|
let w = elevated_wrapper(&env_lines, &script.display().to_string(), &out_file.display().to_string());
|
||||||
std::fs::write(&wrapper, [b"\xEF\xBB\xBF".as_slice(), w.as_bytes()].concat()).map_err(|e| e.to_string())?;
|
std::fs::write(&wrapper, [b"\xEF\xBB\xBF".as_slice(), w.as_bytes()].concat()).map_err(|e| e.to_string())?;
|
||||||
let _ = std::fs::remove_file(&out_file);
|
let _ = std::fs::remove_file(&out_file);
|
||||||
let inner = format!("-NoProfile -ExecutionPolicy Bypass -File \"{}\"", wrapper.display());
|
let inner = format!("-NoProfile -ExecutionPolicy Bypass -File \"{}\"", wrapper.display());
|
||||||
// A refused or unanswered UAC prompt makes Start-Process throw (`$p` stays null) and `exit $p.ExitCode`
|
cmd = crate::platform::elevated_command("powershell.exe", &inner);
|
||||||
// would exit 0: the 5 October 2026 driver job on PC 1 was reported "done" after Windows cancelled its
|
|
||||||
// prompt at 122 s. The launch failure is exit 251 and says so on stderr.
|
|
||||||
let ps = format!("try {{ $p = Start-Process -FilePath powershell.exe -ArgumentList '{}' -Verb RunAs -Wait -WindowStyle Hidden -PassThru -ErrorAction Stop }} catch {{ Write-Error ('elevated launch failed (UAC refused, cancelled or timed out): ' + $_.Exception.Message); exit 251 }}; if ($null -eq $p) {{ Write-Error 'elevated launch failed: no process'; exit 251 }}; exit $p.ExitCode", inner.replace('\'', "''"));
|
|
||||||
cmd = Command::new(crate::platform::tool("powershell"));
|
|
||||||
cmd.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &ps]);
|
|
||||||
} else if shell == "powershell" {
|
} else if shell == "powershell" {
|
||||||
cmd = Command::new(crate::platform::tool("powershell"));
|
cmd = Command::new(crate::platform::tool("powershell"));
|
||||||
cmd.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", &script.display().to_string()]);
|
cmd.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-File", &script.display().to_string()]);
|
||||||
|
|
@ -1185,6 +1180,23 @@ fn follow_file(sink: &Sink, path: PathBuf) -> Follow {
|
||||||
Follow { stop, handle }
|
Follow { stop, handle }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The PowerShell wrapper an elevated job runs (its own process, its own environment): the IGNEUM_* values, then one
|
||||||
|
/// line about its console (the elevated process cannot inherit the engine's headless console and gets one of its own;
|
||||||
|
/// `-WindowStyle Hidden` on the launch keeps it hidden, and this line is the running measurement of that on every
|
||||||
|
/// elevated job: "elevated console: hwnd N visible False"), then the script, everything into `out_file` for the engine
|
||||||
|
/// to read back. The console-window class, PC 1, 5 October 2026 (tools/windows/console-watch-elevated.ps1).
|
||||||
|
fn elevated_wrapper(env_lines: &str, script: &str, out_file: &str) -> String {
|
||||||
|
let (script, out) = (crate::platform::ps_quote(script), crate::platform::ps_quote(out_file));
|
||||||
|
format!(
|
||||||
|
"{env_lines}$ErrorActionPreference = 'Continue'\r\n\
|
||||||
|
$igc = ''\r\n\
|
||||||
|
try {{ Add-Type -Name IgCon -Namespace Igneum -MemberDefinition '[DllImport(\"kernel32.dll\")] public static extern System.IntPtr GetConsoleWindow(); [DllImport(\"user32.dll\")] public static extern bool IsWindowVisible(System.IntPtr h);'; $h = [Igneum.IgCon]::GetConsoleWindow(); $igc = \"elevated console: hwnd $h visible $([Igneum.IgCon]::IsWindowVisible($h))\" }} catch {{ $igc = \"elevated console: unknown ($_)\" }}\r\n\
|
||||||
|
$igc | Out-File -FilePath '{out}' -Encoding utf8\r\n\
|
||||||
|
& '{script}' *>&1 | Out-File -FilePath '{out}' -Encoding utf8 -Append\r\n\
|
||||||
|
exit $LASTEXITCODE\r\n"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn finish_ran(ran: Ran, what: &str) -> Result<Done, String> {
|
fn finish_ran(ran: Ran, what: &str) -> Result<Done, String> {
|
||||||
match ran.code {
|
match ran.code {
|
||||||
Some(0) => Ok(Done { status: "done".into(), exit: 0, summary: format!("{what} finished, exit 0"), extra: json!({}) }),
|
Some(0) => Ok(Done { status: "done".into(), exit: 0, summary: format!("{what} finished, exit 0"), extra: json!({}) }),
|
||||||
|
|
@ -1311,10 +1323,24 @@ mod tests {
|
||||||
assert!(d.summary.contains("administrator prompt"), "{}", d.summary);
|
assert!(d.summary.contains("administrator prompt"), "{}", d.summary);
|
||||||
let d = finish_ran(Ran { code: Some(0), timed_out: false }, "script").unwrap();
|
let d = finish_ran(Ran { code: Some(0), timed_out: false }, "script").unwrap();
|
||||||
assert_eq!(d.status, "done");
|
assert_eq!(d.status, "done");
|
||||||
// the launcher string itself: a thrown Start-Process must not fall through to `exit $p.ExitCode`
|
// the launcher string itself (platform::elevated_ps_line since 13755b9): a thrown Start-Process must not fall
|
||||||
let src = include_str!("jobrun.rs");
|
// through to `exit $p.ExitCode`
|
||||||
assert!(src.contains("-Verb RunAs -Wait -WindowStyle Hidden -PassThru -ErrorAction Stop }} catch {{"));
|
let l = crate::platform::elevated_ps_line("powershell.exe", "-NoProfile -File x.ps1");
|
||||||
assert!(src.contains("if ($null -eq $p) {{ Write-Error 'elevated launch failed: no process'; exit 251 }}"));
|
assert!(l.contains("-Verb RunAs -Wait -WindowStyle Hidden -PassThru -ErrorAction Stop } catch {"), "{l}");
|
||||||
|
assert!(l.contains("if ($null -eq $p) { Write-Error 'elevated launch failed: no process'; exit 251 }"), "{l}");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn elevated_wrapper_reports_its_console_then_runs_the_script() {
|
||||||
|
let w = elevated_wrapper("$env:IGNEUM_JOB_ID = 'j1'\r\n", r"C:\jobs\j1\script.ps1", r"C:\jobs\it's\elevated-output.log");
|
||||||
|
assert!(w.starts_with("$env:IGNEUM_JOB_ID = 'j1'\r\n$ErrorActionPreference = 'Continue'\r\n"), "{w}");
|
||||||
|
assert!(w.contains("GetConsoleWindow()") && w.contains("IsWindowVisible("), "{w}");
|
||||||
|
assert!(w.contains("$igc | Out-File -FilePath 'C:\\jobs\\it''s\\elevated-output.log' -Encoding utf8\r\n"), "{w}");
|
||||||
|
assert!(w.contains("& 'C:\\jobs\\j1\\script.ps1' *>&1 | Out-File -FilePath 'C:\\jobs\\it''s\\elevated-output.log' -Encoding utf8 -Append\r\n"), "{w}");
|
||||||
|
assert!(w.ends_with("exit $LASTEXITCODE\r\n"), "{w}");
|
||||||
|
// every line ends in CRLF (the file is written for Windows PowerShell): the env line and six of its own
|
||||||
|
assert_eq!(w.matches("\r\n").count(), 7, "{w:?}");
|
||||||
|
assert_eq!(w.matches('\n').count(), 7, "{w:?}");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
@ -1801,6 +1827,7 @@ fn spawn_relaunch_helper(shared: &Arc<Shared>) -> Result<(), String> {
|
||||||
{
|
{
|
||||||
let dir = std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.to_path_buf())).ok_or("cannot find the install folder")?;
|
let dir = std::env::current_exe().ok().and_then(|p| p.parent().map(|d| d.to_path_buf())).ok_or("cannot find the install folder")?;
|
||||||
let exe = dir.join("igneum-app.exe");
|
let exe = dir.join("igneum-app.exe");
|
||||||
|
// console: igneum-app.exe is a windows-subsystem program in release builds (main.rs), it never gets a console; SW_HIDE would hide the window host it opens
|
||||||
let ps = format!("Start-Sleep 8; Start-Process -FilePath '{}' -ArgumentList '--launch' -WorkingDirectory '{}'", exe.display().to_string().replace('\'', "''"), dir.display().to_string().replace('\'', "''"));
|
let ps = format!("Start-Sleep 8; Start-Process -FilePath '{}' -ArgumentList '--launch' -WorkingDirectory '{}'", exe.display().to_string().replace('\'', "''"), dir.display().to_string().replace('\'', "''"));
|
||||||
c = Command::new(crate::platform::tool("powershell"));
|
c = Command::new(crate::platform::tool("powershell"));
|
||||||
c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", &ps]);
|
c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-WindowStyle", "Hidden", "-Command", &ps]);
|
||||||
|
|
|
||||||
|
|
@ -1278,6 +1278,7 @@ function EngineAlive() { return [bool](Get-Process -Id $EnginePid -ErrorAction S
|
||||||
function Relaunch() {
|
function Relaunch() {
|
||||||
if (EngineAlive) { return }
|
if (EngineAlive) { return }
|
||||||
$exe = Join-Path $InstallDir 'igneum-app.exe'
|
$exe = Join-Path $InstallDir 'igneum-app.exe'
|
||||||
|
# console: igneum-app.exe is a windows-subsystem program (no console); -WindowStyle Hidden would hide the window host it opens
|
||||||
if (Test-Path $exe) { Log 'engine gone and nothing installed: starting the old app again'; Start-Process -FilePath $exe -ArgumentList '--launch' -WorkingDirectory $InstallDir | Out-Null }
|
if (Test-Path $exe) { Log 'engine gone and nothing installed: starting the old app again'; Start-Process -FilePath $exe -ArgumentList '--launch' -WorkingDirectory $InstallDir | Out-Null }
|
||||||
}
|
}
|
||||||
Log "$Mode : engine $EnginePid installer '$Installer' version $Version (the engine keeps mining until the installer runs)"
|
Log "$Mode : engine $EnginePid installer '$Installer' version $Version (the engine keeps mining until the installer runs)"
|
||||||
|
|
@ -1292,6 +1293,7 @@ $setupArgs = @('/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART', '/CLOSEAPPLICAT
|
||||||
try {
|
try {
|
||||||
# no -Verb RunAs: a per-user installer just runs; an administrator installer makes Windows ask, and a declined or
|
# no -Verb RunAs: a per-user installer just runs; an administrator installer makes Windows ask, and a declined or
|
||||||
# timed-out prompt comes back here as an exception with the engine still mining
|
# timed-out prompt comes back here as an exception with the engine still mining
|
||||||
|
# console: the Inno Setup installer is a GUI program (no console), /VERYSILENT shows nothing
|
||||||
$p = Start-Process -FilePath $Installer -ArgumentList $setupArgs -Wait -PassThru
|
$p = Start-Process -FilePath $Installer -ArgumentList $setupArgs -Wait -PassThru
|
||||||
if ($p.ExitCode -eq 0) {
|
if ($p.ExitCode -eq 0) {
|
||||||
if ($Mode -eq 'rollback') { Done $false "Igneum Miner $Version did not stay up twice; the previous version was reinstalled" $true $false }
|
if ($Mode -eq 'rollback') { Done $false "Igneum Miner $Version did not stay up twice; the previous version was reinstalled" $true $false }
|
||||||
|
|
|
||||||
|
|
@ -396,10 +396,7 @@ pub fn sync_clock() -> Result<String, String> {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
let cmd = tool("cmd").display().to_string();
|
let cmd = tool("cmd").display().to_string();
|
||||||
let script = format!("Start-Process -FilePath '{cmd}' -ArgumentList '/c net start w32time & w32tm /resync /force' -Verb RunAs -Wait -WindowStyle Hidden");
|
let mut c = elevated_command(&cmd, "/c net start w32time & w32tm /resync /force");
|
||||||
let mut c = Command::new(tool("powershell"));
|
|
||||||
c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &script]);
|
|
||||||
quiet(&mut c);
|
|
||||||
let out = c.output().map_err(|e| e.to_string())?;
|
let out = c.output().map_err(|e| e.to_string())?;
|
||||||
if out.status.success() {
|
if out.status.success() {
|
||||||
Ok("asked Windows Time to resync (w32tm /resync)".into())
|
Ok("asked Windows Time to resync (w32tm /resync)".into())
|
||||||
|
|
@ -425,12 +422,8 @@ pub fn sync_clock() -> Result<String, String> {
|
||||||
pub fn run_elevated(cmdline: &str) -> Result<(), String> {
|
pub fn run_elevated(cmdline: &str) -> Result<(), String> {
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
{
|
{
|
||||||
let escaped = cmdline.replace('\'', "''");
|
|
||||||
let cmd = tool("cmd").display().to_string();
|
let cmd = tool("cmd").display().to_string();
|
||||||
let script = format!("$p = Start-Process -FilePath '{cmd}' -ArgumentList '/c {escaped}' -Verb RunAs -Wait -WindowStyle Hidden -PassThru; exit $p.ExitCode");
|
let mut c = elevated_command(&cmd, &format!("/c {cmdline}"));
|
||||||
let mut c = Command::new(tool("powershell"));
|
|
||||||
c.args(["-NoProfile", "-ExecutionPolicy", "Bypass", "-Command", &script]);
|
|
||||||
quiet(&mut c);
|
|
||||||
let out = c.output().map_err(|e| e.to_string())?;
|
let out = c.output().map_err(|e| e.to_string())?;
|
||||||
if out.status.success() {
|
if out.status.success() {
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -189,6 +189,7 @@ pub fn bash_line(file: &Path, login: bool, args: &[&str]) -> String {
|
||||||
/// command line exactly as `bash_line` wrote it; elsewhere the words are ordinary arguments (nothing runs wsl there).
|
/// command line exactly as `bash_line` wrote it; elsewhere the words are ordinary arguments (nothing runs wsl there).
|
||||||
/// The caller adds stdio, the hidden-window flag and the timeout.
|
/// The caller adds stdio, the hidden-window flag and the timeout.
|
||||||
pub fn command(wsl_exe: &Path, distro: &str, user: Option<&str>, file: &Path, login: bool, args: &[&str]) -> Command {
|
pub fn command(wsl_exe: &Path, distro: &str, user: Option<&str>, file: &Path, login: bool, args: &[&str]) -> Command {
|
||||||
|
// console: a builder; every caller runs it through run_capture, run_streamed or platform::quiet (tools/ci/windows-spawn-check.mjs)
|
||||||
let mut c = Command::new(wsl_exe);
|
let mut c = Command::new(wsl_exe);
|
||||||
c.args(["-d", distro]);
|
c.args(["-d", distro]);
|
||||||
if let Some(u) = user.filter(|u| !u.is_empty()) {
|
if let Some(u) = user.filter(|u| !u.is_empty()) {
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,7 @@ shard run reported as exit 0, 7a7e873).
|
||||||
| Date | Symptom | Cause | Fix | Proven by |
|
| Date | Symptom | Cause | Fix | Proven by |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| 4 Oct 2026 | Every `ci` run on master red since 67bf226 (eleven pushes), unnoticed | `sim/difficulty/records/testnet-v2-2026-10-04.schedule.log` carried a home path; `.log` was outside the identity scrub's extension list in `tools/ci/identity-check.sh` (and in the mirror's `tools/sync.sh`) | 2996cca: `.log` scrubbed like the other text files; the record rewritten with `~`; the same list in igneum-public `tools/sync.sh` (local commit e18256d, not pushed) | `bash tools/ci/identity-check.sh` 0 hits locally; run 37226816xxx on master green |
|
| 4 Oct 2026 | Every `ci` run on master red since 67bf226 (eleven pushes), unnoticed | `sim/difficulty/records/testnet-v2-2026-10-04.schedule.log` carried a home path; `.log` was outside the identity scrub's extension list in `tools/ci/identity-check.sh` (and in the mirror's `tools/sync.sh`) | 2996cca: `.log` scrubbed like the other text files; the record rewritten with `~`; the same list in igneum-public `tools/sync.sh` (local commit e18256d, not pushed) | `bash tools/ci/identity-check.sh` 0 hits locally; run 37226816xxx on master green |
|
||||||
|
| 5 Oct 2026 | PC 1 (Windows 11 Pro 26200, default terminal Windows Terminal 1.24): "Windows Command Processor" windows whenever a remote job runs (the project lead) | measured, not guessed: `tools/windows/console-watch.ps1` (job run-20261005-182528) started every candidate child from the app's job runner, whose console is headless (`conhost.exe 0x4`, hwnd 0), with a user32 EnumWindows sampler every 30 ms: powershell, cmd, query, curl, nvidia-smi, wsl --status, a distro, interop cmd and powershell, `powershell -WindowStyle Hidden`, `Start-Process -WindowStyle Hidden`: 0 windows each; `Start-Process cmd` in a new console: a Terminal window and a cmd PseudoConsoleWindow (the known-failed case fires). The 25-minute background watcher (console-watch-bg.ps1, run-20261005-184330, 18:44 to 19:09 UTC, every 200 ms) across an app restart, a build job, two run jobs, two collect jobs and the sweep helper's elevated launch at 19:04:43: 0 console or Terminal windows, 69 conhost starts (every one `conhost.exe 0x4`, headless, under curl, wsl, wslhost, powershell), 1 cmd.exe (under wslhost, WSL interop, no window). The one road that creates a console of its own is the elevated launch (`Start-Process -Verb RunAs`, the AppInfo service: the power cap, the sweep helper, the clock sync, an elevated job); it carried `-WindowStyle Hidden` in four copies, and "Windows Command Processor" is also the name on the UAC prompt the engine raises for cmd.exe (the sweep helper prompted at 17:00, 17:30 and 18:12 UTC, the power cap at every start; the elevated watcher's own prompt, run-20261005-184610, timed out unanswered at 122 s) | `platform::elevated_ps_line` + `elevated_command`: one builder for every elevated launch, hidden by construction, exit 251 when the prompt is refused; the elevated job wrapper reports its own console (`elevated console: hwnd N visible False`) on every elevated job; `tools/ci/windows-spawn-check.mjs` fails CI on a Command::new without the quiet flag, a creation_flags other than CREATE_NO_WINDOW, a Start-Process without -WindowStyle Hidden/-NoNewWindow, or a host.cpp spawn without CREATE_NO_WINDOW / SW_HIDE | the watcher's known-failed case (2 windows) and known-finished case (0); the CI check's self-test (9 cases) and the tree (0 hits); the igneum-app test suite on PC 1 |
|
||||||
| 4 Oct 2026 | `collect-pc1-board3` printed PowerShell parse errors (`.Name`, `.AdapterRAM`) | the publishing shell expanded `$_` inside double quotes to nothing before the command reached the jobs file; nothing to do with Format-List or Out-String (board2 and board4 printed their values) | publish-jobs.sh refuses a collect command that pipes into a script block without `$_` or `$PSItem` | the eaten form refused with the reason, the single-quoted form published to a test folder |
|
| 4 Oct 2026 | `collect-pc1-board3` printed PowerShell parse errors (`.Name`, `.AdapterRAM`) | the publishing shell expanded `$_` inside double quotes to nothing before the command reached the jobs file; nothing to do with Format-List or Out-String (board2 and board4 printed their values) | publish-jobs.sh refuses a collect command that pipes into a script block without `$_` or `$PSItem` | the eaten form refused with the reason, the single-quoted form published to a test folder |
|
||||||
| 4 Oct 2026 | the same job reported `done (exit 0)` over `command exit Some(1)` | `run_collect` in `app/igneum-app/src/jobrun.rs` builds `Done` from the upload count only; the command's exit code is logged and dropped | branch `bugfix-collect-exit`, 35ccdc8 rebased on c257444 (app engine; merge by the main session) | `cargo test --bin igneum-app`: all 28 tests pass on the rebased branch; the new one covers the board3 shape (`Some(1)` is failed exit 1), `Some(0)` done, the cap as timeout, failed uploads still failing |
|
| 4 Oct 2026 | the same job reported `done (exit 0)` over `command exit Some(1)` | `run_collect` in `app/igneum-app/src/jobrun.rs` builds `Done` from the upload count only; the command's exit code is logged and dropped | branch `bugfix-collect-exit`, 35ccdc8 rebased on c257444 (app engine; merge by the main session) | `cargo test --bin igneum-app`: all 28 tests pass on the rebased branch; the new one covers the board3 shape (`Some(1)` is failed exit 1), `Some(0)` done, the cap as timeout, failed uploads still failing |
|
||||||
| 4 Oct 2026 | `publish-jobs.sh --deploy` said "not reachable, differs from the local one, or does not verify yet" after a deploy that had succeeded | one check the instant the CLI returned, while the edge still served the previous file; the deploy's own exit status was hidden by `\|\| true` | `verify_live`: up to `--tries` (12) checks 5 s apart, each failure names its condition; `publish-jobs.sh verify` re-checks on its own; a failed deploy stops before the check | finished: `verify --tries 2` against the live file (try 1 of 2); failed: a local server with an older file ("differs", both publish stamps named) and a closed port ("is not reachable") |
|
| 4 Oct 2026 | `publish-jobs.sh --deploy` said "not reachable, differs from the local one, or does not verify yet" after a deploy that had succeeded | one check the instant the CLI returned, while the edge still served the previous file; the deploy's own exit status was hidden by `\|\| true` | `verify_live`: up to `--tries` (12) checks 5 s apart, each failure names its condition; `publish-jobs.sh verify` re-checks on its own; a failed deploy stops before the check | finished: `verify --tries 2` against the live file (try 1 of 2); failed: a local server with an older file ("differs", both publish stamps named) and a closed port ("is not reachable") |
|
||||||
|
|
|
||||||
106
tools/ci/windows-spawn-check.mjs
Normal file
106
tools/ci/windows-spawn-check.mjs
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
// The console-window class (5 October 2026, PC 1): a child the app starts on Windows without CREATE_NO_WINDOW, or an
|
||||||
|
// elevated child started without SW_HIDE, gets a console of its own, and on Windows 11 with Windows Terminal as the
|
||||||
|
// default terminal that console is a visible Terminal window on the user's desk. Rule: every process the app starts
|
||||||
|
// on Windows runs with a hidden console. This check fails CI when
|
||||||
|
// - a `Command::new(` in app/igneum-app/src is not quieted within 20 lines: crate::platform::quiet, run_timeout,
|
||||||
|
// run_capture, run_streamed, spawn_detached, elevated_command, or creation_flags(0x0800_0000) (CREATE_NO_WINDOW);
|
||||||
|
// programs that only exist off Windows (nohup, osascript, pkexec, hdiutil, ...) are allowed, and a
|
||||||
|
// `// console: <reason>` comment on the line or the line above allows a builder the caller quiets;
|
||||||
|
// the check looks 2 lines back as well, for `run_timeout(\n Command::new(...)`;
|
||||||
|
// - a `creation_flags(` carries anything but 0x0800_0000 (DETACHED_PROCESS made powershell exit at start-up,
|
||||||
|
// 0.3.0 to 0.3.4, docs/bugs.md);
|
||||||
|
// - a PowerShell `Start-Process` written by the Rust code lacks `-WindowStyle Hidden` or `-NoNewWindow` (a GUI
|
||||||
|
// program, which never gets a console, takes a `# console: <reason>` comment on the line or the line above);
|
||||||
|
// - app/windows/host.cpp calls CreateProcessW without CREATE_NO_WINDOW or sets up a ShellExecuteExW without
|
||||||
|
// nShow = SW_HIDE (ShellExecuteW "open" of a URL is the browser, allowed).
|
||||||
|
// node tools/ci/windows-spawn-check.mjs the tree
|
||||||
|
// node tools/ci/windows-spawn-check.mjs --self-test the rules on known-good and known-bad samples
|
||||||
|
import { readFileSync, readdirSync, statSync } from 'node:fs';
|
||||||
|
import { join, resolve, dirname, relative } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
||||||
|
const QUIET = /\bquiet\(|\brun_timeout\(|\brun_capture\(|\brun_streamed\(|\bspawn_detached\(|\belevated_command\(|creation_flags\(0x0800_0000\)/;
|
||||||
|
const OFF_WINDOWS = /Command::new\((?:crate::platform::)?tool\("(?:nohup|osascript|pkexec|hdiutil|ditto|open|xattr|system_profiler|sysctl|sntp|scutil|caffeinate)"\)|Command::new\("(?:pkexec|xdg-open|\/bin\/bash|\/usr\/bin\/[a-z]+)"\)|Command::new\(staged\.join\("Contents\/MacOS/;
|
||||||
|
const WINDOW = 20;
|
||||||
|
|
||||||
|
export function checkRust(text, file) {
|
||||||
|
const lines = text.split('\n');
|
||||||
|
const out = [];
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const l = lines[i];
|
||||||
|
if (/Command::new\(/.test(l) && !/^\s*\/\//.test(l)) {
|
||||||
|
const allowed = OFF_WINDOWS.test(l) || /\/\/ console:/.test(l) || (i > 0 && /\/\/ console:/.test(lines[i - 1]));
|
||||||
|
if (!allowed) {
|
||||||
|
const span = lines.slice(Math.max(0, i - 2), i + WINDOW).join('\n'); // 2 lines back: run_timeout(\n Command::new(...)
|
||||||
|
if (!QUIET.test(span)) out.push(`${file}:${i + 1}: Command::new without a hidden console within ${WINDOW} lines (quiet, run_timeout, run_capture, run_streamed, spawn_detached, elevated_command or creation_flags(0x0800_0000)); add one, or a '// console: <reason>' comment`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const cf = /creation_flags\(([^)]*)\)/.exec(l);
|
||||||
|
if (cf && cf[1].trim() !== '0x0800_0000') out.push(`${file}:${i + 1}: creation_flags(${cf[1]}) is not CREATE_NO_WINDOW alone (0x0800_0000)`);
|
||||||
|
if (/Start-Process\b/.test(l) && !/^\s*\/\//.test(l) && !/-WindowStyle Hidden|-NoNewWindow/.test(l) && !/(\/\/|#) console:/.test(l) && !(i > 0 && /(\/\/|#) console:/.test(lines[i - 1]))) out.push(`${file}:${i + 1}: Start-Process without -WindowStyle Hidden or -NoNewWindow (a GUI program takes a '# console: <reason>' comment)`);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkHost(text, file) {
|
||||||
|
const lines = text.split('\n');
|
||||||
|
const out = [];
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const l = lines[i];
|
||||||
|
if (/CreateProcessW?\s*\(/.test(l) && !/CREATE_NO_WINDOW/.test(lines.slice(i, i + 3).join('\n'))) out.push(`${file}:${i + 1}: CreateProcess without CREATE_NO_WINDOW`);
|
||||||
|
if (/ShellExecuteExW?\s*\(/.test(l) && !/nShow\s*=\s*SW_HIDE/.test(lines.slice(Math.max(0, i - 12), i + 1).join('\n'))) out.push(`${file}:${i + 1}: ShellExecuteEx without nShow = SW_HIDE in the 12 lines before it`);
|
||||||
|
if (/ShellExecuteW?\s*\(/.test(l) && !/ShellExecuteExW?/.test(l) && !/L"open"/.test(l)) out.push(`${file}:${i + 1}: ShellExecute that is not the browser "open" of a URL`);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function walk(dir, ext, acc = []) {
|
||||||
|
for (const e of readdirSync(dir)) {
|
||||||
|
const p = join(dir, e);
|
||||||
|
if (statSync(p).isDirectory()) { if (e !== 'target') walk(p, ext, acc); } else if (p.endsWith(ext)) acc.push(p);
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selfTest() {
|
||||||
|
const good = `fn a() {\n let mut c = Command::new(crate::platform::tool("powershell"));\n c.args(["-NoProfile"]);\n crate::platform::quiet(&mut c);\n c.spawn();\n}\n`;
|
||||||
|
const bad = `fn a() {\n let mut c = Command::new(crate::platform::tool("powershell"));\n c.args(["-NoProfile"]);\n c.spawn();\n}\n`;
|
||||||
|
const badFlag = `c.creation_flags(0x0000_0008);\n`;
|
||||||
|
const badPs = `let ps = format!("Start-Process -FilePath '{}' -Wait", exe);\n`;
|
||||||
|
const okPs = `let ps = format!("Start-Process -FilePath '{}' -Wait -WindowStyle Hidden", exe);\n`;
|
||||||
|
const offWin = `let out = Command::new(tool("osascript")).args(["-e", "x"]).output();\n`;
|
||||||
|
const allowed = `// console: the caller quiets it\nlet mut c = Command::new(wsl_exe);\n`;
|
||||||
|
const hostGood = `sei.nShow = SW_HIDE;\nif (!ShellExecuteExW(&sei)) {}\nCreateProcessW(exe, buf, nullptr, nullptr, TRUE, CREATE_NO_WINDOW, nullptr, dir, &si, &pi);\nShellExecuteW(nullptr, L"open", url, nullptr, nullptr, SW_SHOWNORMAL);\n`;
|
||||||
|
const hostBad = `sei.nShow = SW_SHOW;\nif (!ShellExecuteExW(&sei)) {}\nCreateProcessW(exe, buf, nullptr, nullptr, TRUE, 0, nullptr, dir, &si, &pi);\n`;
|
||||||
|
const cases = [
|
||||||
|
['quieted Command', checkRust(good, 't.rs').length === 0],
|
||||||
|
['bare Command fails', checkRust(bad, 't.rs').length === 1],
|
||||||
|
['DETACHED_PROCESS fails', checkRust(badFlag, 't.rs').length === 1],
|
||||||
|
['Start-Process without Hidden fails', checkRust(badPs, 't.rs').length === 1],
|
||||||
|
['Start-Process with Hidden passes', checkRust(okPs, 't.rs').length === 0],
|
||||||
|
['off-Windows program passes', checkRust(offWin, 't.rs').length === 0],
|
||||||
|
['console: comment passes', checkRust(allowed, 't.rs').length === 0],
|
||||||
|
['host.cpp good passes', checkHost(hostGood, 'h.cpp').length === 0],
|
||||||
|
['host.cpp bad fails twice', checkHost(hostBad, 'h.cpp').length === 2],
|
||||||
|
];
|
||||||
|
let fail = 0;
|
||||||
|
for (const [name, ok] of cases) { console.log(`${ok ? 'ok ' : 'FAIL'} ${name}`); if (!ok) fail++; }
|
||||||
|
return fail;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
||||||
|
if (process.argv.includes('--self-test')) {
|
||||||
|
const f = selfTest();
|
||||||
|
console.log(f ? `windows-spawn: self-test FAILED (${f})` : 'windows-spawn: self-test ok');
|
||||||
|
process.exit(f ? 1 : 0);
|
||||||
|
}
|
||||||
|
const problems = [];
|
||||||
|
for (const f of walk(join(ROOT, 'app', 'igneum-app', 'src'), '.rs')) problems.push(...checkRust(readFileSync(f, 'utf8'), relative(ROOT, f)));
|
||||||
|
const host = join(ROOT, 'app', 'windows', 'host.cpp');
|
||||||
|
try { problems.push(...checkHost(readFileSync(host, 'utf8'), relative(ROOT, host))); } catch {}
|
||||||
|
for (const p of problems) console.log(p);
|
||||||
|
console.log(problems.length ? `windows-spawn: ${problems.length} spawn(s) without a hidden console` : 'windows-spawn: every Windows spawn in app/igneum-app/src and app/windows/host.cpp runs with a hidden console');
|
||||||
|
process.exit(problems.length ? 1 : 0);
|
||||||
|
}
|
||||||
101
tools/windows/console-watch-bg.ps1
Normal file
101
tools/windows/console-watch-bg.ps1
Normal file
|
|
@ -0,0 +1,101 @@
|
||||||
|
# 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 })
|
||||||
84
tools/windows/console-watch-elevated.ps1
Normal file
84
tools/windows/console-watch-elevated.ps1
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
# Console-window watcher for the ELEVATED job path (app/igneum-app/src/jobrun.rs run_script with elevated=true: the
|
||||||
|
# app's headless powershell runs `Start-Process powershell.exe -Verb RunAs -Wait -WindowStyle Hidden`, the AppInfo
|
||||||
|
# service creates this process after the UAC prompt). The engine's power cap, the sweep helper and the clock sync take
|
||||||
|
# the same road with cmd.exe (platform.rs run_elevated, sync_clock; app/windows/host.cpp runElevated). This script
|
||||||
|
# runs INSIDE the elevated process and reports whether its own console has a window, which host serves it, and
|
||||||
|
# whether a Windows Terminal window appeared for it. One UAC prompt on the PC; a few seconds.
|
||||||
|
# packaging/ota/publish-jobs.sh add --kind run --target ae432dc7 --elevated --timeout-minutes 3 \
|
||||||
|
# --script tools/windows/console-watch-elevated.ps1 --title "PC 1: elevated console watcher" --deploy
|
||||||
|
$ErrorActionPreference = 'Continue'
|
||||||
|
$src = @'
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Text;
|
||||||
|
public static class IgWin3 {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
'@
|
||||||
|
Add-Type -TypeDefinition $src
|
||||||
|
function Say([string] $m) { Write-Output $m }
|
||||||
|
function ProcName([int] $procId) { try { (Get-Process -Id $procId -ErrorAction Stop).ProcessName } catch { 'gone' } }
|
||||||
|
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 140) { $cl = $cl.Substring(0, 140) + '...' }
|
||||||
|
$out += ($ci.Name + " pid " + $p + " [" + $cl + "]")
|
||||||
|
$p = $ci.ParentProcessId
|
||||||
|
}
|
||||||
|
return ($out -join ' <- ')
|
||||||
|
}
|
||||||
|
$me = [System.Diagnostics.Process]::GetCurrentProcess()
|
||||||
|
$id = [Security.Principal.WindowsIdentity]::GetCurrent()
|
||||||
|
$admin = (New-Object Security.Principal.WindowsPrincipal($id)).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||||
|
Say ("RESULT elevated: " + $admin + " user " + $id.Name + " session " + $me.SessionId + " chain " + (Chain $me.Id))
|
||||||
|
$hwnd = [IgWin3]::GetConsoleWindow()
|
||||||
|
$cls = ''
|
||||||
|
if ($hwnd -ne [IntPtr]::Zero) { $sb = New-Object System.Text.StringBuilder 256; [void][IgWin3]::GetClassName($hwnd, $sb, 256); $cls = $sb.ToString() }
|
||||||
|
$vis = if ($hwnd -ne [IntPtr]::Zero) { [IgWin3]::IsWindowVisible($hwnd) } else { 'no window' }
|
||||||
|
Say ("RESULT self-console: hwnd " + $hwnd + " class [" + $cls + "] visible " + $vis)
|
||||||
|
# the hosts that serve this process: a conhost with this pid as parent (classic), or an OpenConsole + WindowsTerminal
|
||||||
|
# pair started by svchost in the last seconds (the default-terminal handoff)
|
||||||
|
$since = (Get-Date).AddSeconds(-20)
|
||||||
|
foreach ($h in @(Get-CimInstance Win32_Process -Filter "Name='conhost.exe' OR Name='OpenConsole.exe' OR Name='WindowsTerminal.exe'" -ErrorAction SilentlyContinue)) {
|
||||||
|
$created = try { [Management.ManagementDateTimeConverter]::ToDateTime($h.CreationDate) } catch { $null }
|
||||||
|
if ($h.ParentProcessId -eq $me.Id -or ($created -and $created -gt $since)) {
|
||||||
|
Say ("RESULT host: " + $h.Name + " pid " + $h.ProcessId + " parent " + (ProcName $h.ParentProcessId) + " started " + $(if ($created) { $created.ToUniversalTime().ToString('HH:mm:ss') } else { '?' }) + " cmd " + $h.CommandLine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$wins = @([IgWin3]::Visible() | Where-Object { $f = $_.Split('|', 4); $f[2] -eq 'CASCADIA_HOSTING_WINDOW_CLASS' -or $f[2] -eq 'ConsoleWindowClass' -or $f[2] -eq 'PseudoConsoleWindow' })
|
||||||
|
Say ("RESULT console-windows-now: " + $wins.Count)
|
||||||
|
foreach ($w in $wins) { $f = $w.Split('|', 4); Say ("RESULT window: " + (ProcName ([int]$f[1])) + " pid " + $f[1] + " [" + $f[2] + "] " + $f[3]) }
|
||||||
|
# a child the elevated script starts the way the sweep helper and the power cap do (cmd, inherited console), watched
|
||||||
|
$before = @{}; foreach ($w in [IgWin3]::Visible()) { $before[$w.Split('|', 4)[0]] = 1 }
|
||||||
|
$p = Start-Process -FilePath cmd.exe -ArgumentList '/c ping -n 3 127.0.0.1 >nul' -NoNewWindow -PassThru
|
||||||
|
$seen = @{}
|
||||||
|
for ($i = 0; $i -lt 30; $i++) {
|
||||||
|
foreach ($w in [IgWin3]::Visible()) { $f = $w.Split('|', 4); if (-not $before.ContainsKey($f[0]) -and -not $seen.ContainsKey($f[0])) { $seen[$f[0]] = (ProcName ([int]$f[1])) + " pid " + $f[1] + " [" + $f[2] + "] " + $f[3] } }
|
||||||
|
Start-Sleep -Milliseconds 100
|
||||||
|
}
|
||||||
|
try { $p.WaitForExit(10000) | Out-Null } catch {}
|
||||||
|
Say ("RESULT probe cmd-inherit-elevated: " + $seen.Count + " new window(s)")
|
||||||
|
foreach ($s in $seen.Values) { Say ("RESULT window: " + $s + " (probe cmd-inherit-elevated)") }
|
||||||
|
exit 0
|
||||||
181
tools/windows/console-watch.ps1
Normal file
181
tools/windows/console-watch.ps1
Normal file
|
|
@ -0,0 +1,181 @@
|
||||||
|
# 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 <n>: ... exit code, duration, how many windows and hosts appeared
|
||||||
|
# RESULT window: <process> [<class>] <title> (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
|
||||||
Loading…
Reference in a new issue