@aryacode/dsh-plugin-arya-computer-use 1.0.1 → 1.0.3
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/README.md +11 -0
- package/index.js +83 -2
- package/mcp/adapter-server.mjs +30 -5
- package/mcp/browser-launcher.mjs +36 -6
- package/package.json +2 -1
- package/scripts/hide-consoles.ps1 +120 -0
package/README.md
CHANGED
|
@@ -97,6 +97,17 @@ node mcp/browser-launcher.mjs # → agent-browser mcp --tools core [--p
|
|
|
97
97
|
|
|
98
98
|
So sign-in reuse and attaching to your own Chrome are settings, not hand-edited rows. Web-lane changes land on the next MCP restart; the settings page says so.
|
|
99
99
|
|
|
100
|
+
## Plugin market
|
|
101
|
+
|
|
102
|
+
Installing from Plugins -> Arya Plugin Market needs no manual source: on first run the plugin registers
|
|
103
|
+
`https://cdn.jsdelivr.net/gh/KonnectSuite/dsh-arya-computer-use@main/index.json`
|
|
104
|
+
with the market (existing sources are kept) and every npm release shows up there as an update. Releases go
|
|
105
|
+
through npm staged publishing, so a publish is visible in the market once the version is approved on npmjs.com.
|
|
106
|
+
|
|
107
|
+
The plugin manager blocks dependency build scripts by default. The engines (`agent-desktop`, `agent-browser`)
|
|
108
|
+
fetch their native binary in a postinstall script; approve those two builds when installing, or let 1.0.1+
|
|
109
|
+
repair them itself at start-up - the status route reports the outcome under `mcp.engines`.
|
|
110
|
+
|
|
100
111
|
## Tool presets
|
|
101
112
|
|
|
102
113
|
- `lite` (default) — all 16 desktop tools listed up front.
|
package/index.js
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
import z from '@deepseek-ai/schemastery';
|
|
20
20
|
import os from 'node:os';
|
|
21
21
|
import { readFile, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
22
|
-
import { execFileSync } from 'node:child_process';
|
|
22
|
+
import { execFileSync, spawn } from 'node:child_process';
|
|
23
23
|
import { dirname, join } from 'node:path';
|
|
24
24
|
import { createRequire } from 'node:module';
|
|
25
25
|
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
@@ -33,9 +33,15 @@ export const inject = ['tools'];
|
|
|
33
33
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
34
34
|
const SKILL_DIR = join(HERE, 'skills', 'arya-desktop');
|
|
35
35
|
const SKILL_FILE = join(SKILL_DIR, 'SKILL.md');
|
|
36
|
+
const CONSOLE_HIDER_SCRIPT = join(HERE, 'scripts', 'hide-consoles.ps1');
|
|
36
37
|
const PROVIDER_NAME = 'arya-computer-use';
|
|
37
38
|
const RANK = 350;
|
|
38
39
|
const STATUS_PATH = '/plugins/arya/status';
|
|
40
|
+
// Market index that lists this plugin. The market ships no default source, so the
|
|
41
|
+
// plugin registers its own on first apply: users then see "Arya Computer Use" in
|
|
42
|
+
// Plugins -> Arya Plugin Market without pasting a URL, and every release published
|
|
43
|
+
// to npm shows up there as an update.
|
|
44
|
+
const MARKET_INDEX_URL = 'https://cdn.jsdelivr.net/gh/KonnectSuite/dsh-arya-computer-use@main/index.json';
|
|
39
45
|
const PLUGIN_VERSION = (() => {
|
|
40
46
|
try { return JSON.parse(readFileSync(join(HERE, 'package.json'), 'utf8')).version ?? '0.0.0'; }
|
|
41
47
|
catch { return '0.0.0'; }
|
|
@@ -256,10 +262,83 @@ Object.defineProperty(Config, '~standard', { value: { ...validation, validate(in
|
|
|
256
262
|
return result;
|
|
257
263
|
} } });
|
|
258
264
|
|
|
265
|
+
// Register this plugin's market index with the plugin market, once, so the
|
|
266
|
+
// market lists Arya Computer Use without the user adding a source by hand.
|
|
267
|
+
// setIndexes keeps any source the user already configured.
|
|
268
|
+
function ensureMarketSource(ctx) {
|
|
269
|
+
const canonical = (url) => {
|
|
270
|
+
try { return new URL(url).href; }
|
|
271
|
+
catch { return url; }
|
|
272
|
+
};
|
|
273
|
+
try {
|
|
274
|
+
ctx.inject(['pluginMarket'], (scope) => {
|
|
275
|
+
void (async () => {
|
|
276
|
+
try {
|
|
277
|
+
const market = scope.pluginMarket ?? scope.get('pluginMarket');
|
|
278
|
+
if (!market || typeof market.listIndexes !== 'function' || typeof market.setIndexes !== 'function') return;
|
|
279
|
+
const sources = market.listIndexes() ?? [];
|
|
280
|
+
const urls = sources.map(entry => entry?.url).filter(url => typeof url === 'string');
|
|
281
|
+
if (urls.some(url => canonical(url) === canonical(MARKET_INDEX_URL))) return;
|
|
282
|
+
await market.setIndexes([...urls, MARKET_INDEX_URL]);
|
|
283
|
+
ctx.logger.info('dsh-arya-computer-use: registered its market index source');
|
|
284
|
+
} catch (error) {
|
|
285
|
+
ctx.logger.warn('dsh-arya-computer-use: could not register the market index source: %s', error?.message ?? String(error));
|
|
286
|
+
}
|
|
287
|
+
})();
|
|
288
|
+
});
|
|
289
|
+
} catch (error) {
|
|
290
|
+
ctx.logger?.warn?.('dsh-arya-computer-use: plugin market is unavailable: %s', error?.message ?? String(error));
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
// Internal hook so a diagnostic can exercise the market registration.
|
|
295
|
+
export { ensureMarketSource as __ensureMarketSource };
|
|
296
|
+
|
|
259
297
|
/* -------------------------------------------------------------------- main */
|
|
260
298
|
|
|
299
|
+
// Windows gives a console window titled after its executable to any console
|
|
300
|
+
// child that was not created with CREATE_NO_WINDOW. Both engines ship a JS shim
|
|
301
|
+
// (agent-browser/bin/agent-browser.js, agent-desktop/bin/agent-desktop) that
|
|
302
|
+
// runs the native helper with `stdio: 'inherit'` and `windowsHide: false`, so a
|
|
303
|
+
// black console stays on the desktop for as long as the helper lives - the
|
|
304
|
+
// browser daemon keeps it for the whole session - and closing it kills the
|
|
305
|
+
// helper, which is why it comes straight back. The plugin spawns the natives
|
|
306
|
+
// itself wherever the call site allows it; this watcher covers whatever else
|
|
307
|
+
// starts them (a lane restart, an older launcher, a helper started outside the
|
|
308
|
+
// plugin). It hides only classic console windows whose title or image names one
|
|
309
|
+
// of the two engine helpers, and hiding leaves stdio alone, so the helper keeps
|
|
310
|
+
// working.
|
|
311
|
+
function startConsoleHider(ctx) {
|
|
312
|
+
if (process.platform !== 'win32') return () => {};
|
|
313
|
+
try {
|
|
314
|
+
if (!statSync(CONSOLE_HIDER_SCRIPT).isFile()) return () => {};
|
|
315
|
+
} catch {
|
|
316
|
+
return () => {};
|
|
317
|
+
}
|
|
318
|
+
let child;
|
|
319
|
+
try {
|
|
320
|
+
child = spawn(
|
|
321
|
+
'powershell.exe',
|
|
322
|
+
['-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', CONSOLE_HIDER_SCRIPT, '-ParentPid', String(process.pid)],
|
|
323
|
+
{ stdio: 'ignore', windowsHide: true },
|
|
324
|
+
);
|
|
325
|
+
} catch (error) {
|
|
326
|
+
ctx.logger.warn('dsh-arya-computer-use: could not start the console watcher: %s', error?.message ?? String(error));
|
|
327
|
+
return () => {};
|
|
328
|
+
}
|
|
329
|
+
child.on('error', error => {
|
|
330
|
+
ctx.logger.warn('dsh-arya-computer-use: console watcher stopped: %s', error?.message ?? String(error));
|
|
331
|
+
});
|
|
332
|
+
child.unref?.();
|
|
333
|
+
return () => {
|
|
334
|
+
try { child.kill(); } catch { /* already gone */ }
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
261
338
|
export async function apply(ctx, config) {
|
|
339
|
+
const stopConsoleHider = startConsoleHider(ctx);
|
|
262
340
|
registerSkillProvider(ctx);
|
|
341
|
+
ensureMarketSource(ctx);
|
|
263
342
|
void (async () => {
|
|
264
343
|
try {
|
|
265
344
|
const engines = await ensureEngines();
|
|
@@ -400,7 +479,9 @@ export async function apply(ctx, config) {
|
|
|
400
479
|
state: readJson(runtime.stateFile),
|
|
401
480
|
});
|
|
402
481
|
}
|
|
403
|
-
return () => {
|
|
482
|
+
return () => {
|
|
483
|
+
stopConsoleHider();
|
|
484
|
+
};
|
|
404
485
|
}
|
|
405
486
|
|
|
406
487
|
function isLoopbackAddress(address) {
|
package/mcp/adapter-server.mjs
CHANGED
|
@@ -25,6 +25,25 @@ const require = createRequire(import.meta.url);
|
|
|
25
25
|
const CLI_BIN = require.resolve('agent-desktop/bin/agent-desktop');
|
|
26
26
|
const OCR_PS1 = join(HERE, '..', 'scripts', 'ocr.ps1');
|
|
27
27
|
|
|
28
|
+
// Windows console-window trap, the same one browser-launcher.mjs documents and
|
|
29
|
+
// avoids: bin/agent-desktop (the npm shim) spawns the native exe with
|
|
30
|
+
// stdio:'inherit' + windowsHide:false, so every tool call pops a console window
|
|
31
|
+
// on the user's desktop. Spawn the native binary directly instead; stdio stays
|
|
32
|
+
// piped and windowsHide holds. Null when the platform has no released binary,
|
|
33
|
+
// in which case the shim is the only way to run the CLI.
|
|
34
|
+
const NATIVE_BIN = (() => {
|
|
35
|
+
try {
|
|
36
|
+
const pkgDir = dirname(require.resolve('agent-desktop/package.json'));
|
|
37
|
+
const { resolve } = require('agent-desktop/lib/platform');
|
|
38
|
+
const entry = resolve(process.platform, process.arch);
|
|
39
|
+
if (!entry || !entry.released || !entry.binaryName) return null;
|
|
40
|
+
const bin = join(pkgDir, 'bin', entry.binaryName);
|
|
41
|
+
return existsSync(bin) ? bin : null;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
})();
|
|
46
|
+
|
|
28
47
|
// Engine state root: the engine's own default (~/.agent-desktop) — it creates
|
|
29
48
|
// that root with its protected DACL and passes its private-file checks.
|
|
30
49
|
// Do NOT point AGENT_DESKTOP_HOME at AppData\Local: inherited AppContainer ACEs
|
|
@@ -179,11 +198,17 @@ function runCli(args, { headed = effectiveHeaded() } = {}) {
|
|
|
179
198
|
const env = { ...process.env };
|
|
180
199
|
if (state.sessionStarted) env.AGENT_DESKTOP_SESSION = state.sessionId;
|
|
181
200
|
return new Promise((resolve) => {
|
|
182
|
-
const child =
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
201
|
+
const child = NATIVE_BIN
|
|
202
|
+
? spawn(NATIVE_BIN, argv, {
|
|
203
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
204
|
+
windowsHide: true,
|
|
205
|
+
env,
|
|
206
|
+
})
|
|
207
|
+
: spawn(process.execPath, [CLI_BIN, ...argv], {
|
|
208
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
209
|
+
windowsHide: true,
|
|
210
|
+
env,
|
|
211
|
+
});
|
|
187
212
|
let stdout = '';
|
|
188
213
|
let stderr = '';
|
|
189
214
|
child.stdout.on('data', (d) => (stdout += d));
|
package/mcp/browser-launcher.mjs
CHANGED
|
@@ -3,18 +3,41 @@
|
|
|
3
3
|
// DSH launches this file instead of agent-browser itself so the browser lane can
|
|
4
4
|
// follow the plugin's settings (tool surface, Chrome profile, session, CDP
|
|
5
5
|
// endpoint, headed window) without editing the Cordis patch by hand. The real
|
|
6
|
-
// server's stdio is
|
|
6
|
+
// server's stdio is piped through this process, a transparent relay: stdin and
|
|
7
7
|
// stdout carry the MCP protocol untouched, and settings changes are picked up on
|
|
8
8
|
// the next MCP restart.
|
|
9
|
+
//
|
|
10
|
+
// Two window traps on Windows, both avoided here:
|
|
11
|
+
// - Node ignores windowsHide when stdio is 'inherit', so stdio is piped.
|
|
12
|
+
// - bin/agent-browser.js (the npm CLI shim) spawns the native exe with
|
|
13
|
+
// stdio:'inherit' + windowsHide:false, which pops a console window for the
|
|
14
|
+
// grandchild on the user's desktop. We skip the shim and spawn the native
|
|
15
|
+
// exe directly; it speaks MCP over stdio itself.
|
|
9
16
|
|
|
10
17
|
import { spawn } from 'node:child_process';
|
|
11
18
|
import { existsSync, readFileSync } from 'node:fs';
|
|
12
19
|
import { createRequire } from 'node:module';
|
|
13
|
-
import { homedir } from 'node:os';
|
|
14
|
-
import { join } from 'node:path';
|
|
20
|
+
import { homedir, platform, arch } from 'node:os';
|
|
21
|
+
import { dirname, join } from 'node:path';
|
|
15
22
|
|
|
16
23
|
const require = createRequire(import.meta.url);
|
|
17
|
-
const
|
|
24
|
+
const SHIM = require.resolve('agent-browser/bin/agent-browser.js');
|
|
25
|
+
|
|
26
|
+
// Resolve the platform native binary shipped inside the agent-browser package,
|
|
27
|
+
// mirroring bin/agent-browser.js's naming (with the win32-arm64 → x64 fallback).
|
|
28
|
+
function nativeBin() {
|
|
29
|
+
const dir = dirname(require.resolve('agent-browser/package.json'));
|
|
30
|
+
const osKey = { darwin: 'darwin', linux: 'linux', win32: 'win32' }[platform()];
|
|
31
|
+
const archKey = { x64: 'x64', arm64: 'arm64' }[arch()];
|
|
32
|
+
if (!osKey || !archKey) return null;
|
|
33
|
+
const ext = osKey === 'win32' ? '.exe' : '';
|
|
34
|
+
let name = `agent-browser-${osKey}-${archKey}${ext}`;
|
|
35
|
+
if (osKey === 'win32' && archKey === 'arm64' && !existsSync(join(dir, name))) {
|
|
36
|
+
name = 'agent-browser-win32-x64.exe';
|
|
37
|
+
}
|
|
38
|
+
const bin = join(dir, name);
|
|
39
|
+
return existsSync(bin) ? bin : null;
|
|
40
|
+
}
|
|
18
41
|
|
|
19
42
|
function controlDir() {
|
|
20
43
|
const local = process.env.LOCALAPPDATA || join(homedir(), 'AppData', 'Local');
|
|
@@ -35,7 +58,7 @@ function readSettings() {
|
|
|
35
58
|
const text = (value) => (typeof value === 'string' ? value.trim() : '');
|
|
36
59
|
|
|
37
60
|
export function browserArgs(settings = {}) {
|
|
38
|
-
const args = [
|
|
61
|
+
const args = [SHIM, 'mcp'];
|
|
39
62
|
const tools = text(settings.browserTools);
|
|
40
63
|
if (tools) args.push('--tools', tools);
|
|
41
64
|
const cdp = text(settings.browserCdp);
|
|
@@ -52,12 +75,19 @@ export function browserArgs(settings = {}) {
|
|
|
52
75
|
|
|
53
76
|
function main() {
|
|
54
77
|
const args = browserArgs(readSettings());
|
|
55
|
-
const
|
|
78
|
+
const native = nativeBin();
|
|
79
|
+
const child = native
|
|
80
|
+
? spawn(native, args.slice(1), { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true })
|
|
81
|
+
: spawn(process.execPath, args, { stdio: ['pipe', 'pipe', 'pipe'], windowsHide: true });
|
|
56
82
|
child.on('error', (error) => {
|
|
57
83
|
process.stderr.write(`arya browser lane: could not start agent-browser: ${error.message}\n`);
|
|
58
84
|
process.exit(1);
|
|
59
85
|
});
|
|
60
86
|
child.on('exit', (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
|
|
87
|
+
process.stdin.pipe(child.stdin);
|
|
88
|
+
child.stdout.pipe(process.stdout);
|
|
89
|
+
child.stderr.pipe(process.stderr);
|
|
90
|
+
child.stdin.on('error', () => { /* child gone mid-write; the exit handler ends us */ });
|
|
61
91
|
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
62
92
|
process.on(signal, () => { try { child.kill(signal); } catch { /* already gone */ } });
|
|
63
93
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aryacode/dsh-plugin-arya-computer-use",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "Computer use for DSH/AryaCode: agent-desktop UIA engine, agent-browser web lane, takeover UI with a ghost cursor",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"scripts/windows-takeover.ps1",
|
|
30
30
|
"scripts/windows-takeover.cs",
|
|
31
31
|
"scripts/ocr.ps1",
|
|
32
|
+
"scripts/hide-consoles.ps1",
|
|
32
33
|
"skills/arya-desktop/SKILL.md",
|
|
33
34
|
"locale/en.json",
|
|
34
35
|
"icon.svg",
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
# Arya Computer Use - hide the console windows the automation sidecars hand to
|
|
2
|
+
# the desktop.
|
|
3
|
+
#
|
|
4
|
+
# Windows gives a console window named after the executable to any console
|
|
5
|
+
# process that was not started with CREATE_NO_WINDOW. Both engines ship a JS
|
|
6
|
+
# shim (agent-browser/bin/agent-browser.js, agent-desktop/bin/agent-desktop)
|
|
7
|
+
# that runs the native helper with `stdio: 'inherit'` and `windowsHide: false`,
|
|
8
|
+
# and a native helper that outlives the call (the browser daemon) keeps its
|
|
9
|
+
# console for the whole session. The plugin starts the natives itself wherever
|
|
10
|
+
# it can, so the shim never runs, but a lane restart or a helper started by
|
|
11
|
+
# something else still puts a black console on screen - and closing it kills the
|
|
12
|
+
# helper, which is why it comes back.
|
|
13
|
+
#
|
|
14
|
+
# This watcher hides those windows instead of closing them. Hiding only removes
|
|
15
|
+
# the chrome: stdio is piped, the helper keeps running and stays reachable.
|
|
16
|
+
# Nothing else is touched - the window must be a classic console AND its title
|
|
17
|
+
# or its image path must name one of the two engine helpers.
|
|
18
|
+
#
|
|
19
|
+
# Started hidden by index.js for the lifetime of the host process
|
|
20
|
+
# (-ParentPid). -Once -Report prints what would be hidden and changes nothing.
|
|
21
|
+
|
|
22
|
+
param(
|
|
23
|
+
[int]$ParentPid = 0,
|
|
24
|
+
[int]$IntervalMs = 600,
|
|
25
|
+
[switch]$Once,
|
|
26
|
+
[switch]$Report
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
$ErrorActionPreference = 'Stop'
|
|
30
|
+
|
|
31
|
+
# Console window titles on Windows default to the application path, and the
|
|
32
|
+
# vendor shim does not overwrite them. A few releases rename the helper, so the
|
|
33
|
+
# match stays on the package name plus its bin directory.
|
|
34
|
+
$script:TitlePattern = 'agent-(browser|desktop)(-win32-(x64|arm64))?\.exe'
|
|
35
|
+
$script:PathPattern = '\\node_modules\\(agent-browser|agent-desktop)\\bin\\'
|
|
36
|
+
|
|
37
|
+
Add-Type -Namespace AryaCU -Name Win32 -MemberDefinition @'
|
|
38
|
+
public delegate bool EnumWindowsProc(System.IntPtr hWnd, System.IntPtr lParam);
|
|
39
|
+
|
|
40
|
+
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
|
41
|
+
public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, System.IntPtr lParam);
|
|
42
|
+
|
|
43
|
+
[System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
|
|
44
|
+
public static extern int GetWindowTextW(System.IntPtr hWnd, System.Text.StringBuilder lpString, int nMaxCount);
|
|
45
|
+
|
|
46
|
+
[System.Runtime.InteropServices.DllImport("user32.dll", CharSet = System.Runtime.InteropServices.CharSet.Unicode)]
|
|
47
|
+
public static extern int GetClassNameW(System.IntPtr hWnd, System.Text.StringBuilder lpClassName, int nMaxCount);
|
|
48
|
+
|
|
49
|
+
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
|
50
|
+
public static extern bool IsWindowVisible(System.IntPtr hWnd);
|
|
51
|
+
|
|
52
|
+
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
|
53
|
+
public static extern bool ShowWindow(System.IntPtr hWnd, int nCmdShow);
|
|
54
|
+
|
|
55
|
+
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
|
56
|
+
public static extern uint GetWindowThreadProcessId(System.IntPtr hWnd, out uint lpdwProcessId);
|
|
57
|
+
'@
|
|
58
|
+
|
|
59
|
+
# Returns the visible console windows that belong to one of the engine helpers.
|
|
60
|
+
function Get-EngineConsoles {
|
|
61
|
+
$found = New-Object System.Collections.ArrayList
|
|
62
|
+
|
|
63
|
+
$callback = [AryaCU.Win32+EnumWindowsProc] {
|
|
64
|
+
param([IntPtr]$hWnd, [IntPtr]$lParam)
|
|
65
|
+
|
|
66
|
+
if (-not [AryaCU.Win32]::IsWindowVisible($hWnd)) { return $true }
|
|
67
|
+
|
|
68
|
+
$class = New-Object System.Text.StringBuilder 256
|
|
69
|
+
[void][AryaCU.Win32]::GetClassNameW($hWnd, $class, 256)
|
|
70
|
+
# ConPTY terminals and Windows Terminal use their own classes; only the
|
|
71
|
+
# classic console host is a candidate.
|
|
72
|
+
if ($class.ToString() -ne 'ConsoleWindowClass') { return $true }
|
|
73
|
+
|
|
74
|
+
$title = New-Object System.Text.StringBuilder 1024
|
|
75
|
+
[void][AryaCU.Win32]::GetWindowTextW($hWnd, $title, 1024)
|
|
76
|
+
$text = $title.ToString()
|
|
77
|
+
|
|
78
|
+
$ownerPid = 0
|
|
79
|
+
[void][AryaCU.Win32]::GetWindowThreadProcessId($hWnd, [ref]$ownerPid)
|
|
80
|
+
|
|
81
|
+
$hit = $text -match $script:TitlePattern
|
|
82
|
+
if (-not $hit -and $ownerPid -gt 0) {
|
|
83
|
+
# The title can be blanked by the shim's parent; fall back to the image
|
|
84
|
+
# path when the process is readable (the host runs elevated, so it is).
|
|
85
|
+
try {
|
|
86
|
+
$path = (Get-Process -Id $ownerPid -ErrorAction Stop).Path
|
|
87
|
+
if ($path -and $path -match $script:PathPattern) { $hit = $true }
|
|
88
|
+
} catch { }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if ($hit) {
|
|
92
|
+
[void]$found.Add([pscustomobject]@{ Handle = $hWnd; Title = $text; OwnerPid = $ownerPid })
|
|
93
|
+
}
|
|
94
|
+
return $true
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
[void][AryaCU.Win32]::EnumWindows($callback, [IntPtr]::Zero)
|
|
98
|
+
return @($found.ToArray())
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
while ($true) {
|
|
102
|
+
try {
|
|
103
|
+
foreach ($window in Get-EngineConsoles) {
|
|
104
|
+
if ($Report) {
|
|
105
|
+
'{0} {1} {2}' -f $window.OwnerPid, $window.Handle, $window.Title
|
|
106
|
+
} else {
|
|
107
|
+
[void][AryaCU.Win32]::ShowWindow($window.Handle, 0) # SW_HIDE
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
} catch {
|
|
111
|
+
if ($Once) { throw }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if ($Once) { break }
|
|
115
|
+
|
|
116
|
+
Start-Sleep -Milliseconds ([Math]::Max(200, $IntervalMs))
|
|
117
|
+
|
|
118
|
+
# Leave with the host: an orphaned watcher would keep polling forever.
|
|
119
|
+
if ($ParentPid -gt 0 -and -not (Get-Process -Id $ParentPid -ErrorAction SilentlyContinue)) { break }
|
|
120
|
+
}
|