fullcourtdefense-cli 1.26.0 → 1.26.2
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/dist/commands/daemon.d.ts +2 -9
- package/dist/commands/daemon.js +22 -38
- package/dist/commands/doctor.js +1 -1
- package/dist/perfSnapshot.d.ts +14 -3
- package/dist/perfSnapshot.js +56 -22
- package/dist/selfTest.js +36 -24
- package/dist/version.json +1 -1
- package/dist/windowsTaskState.d.ts +40 -0
- package/dist/windowsTaskState.js +43 -0
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { BotGuardConfig } from '../config';
|
|
2
2
|
import { ProtectAllArgs } from './mcpGateway';
|
|
3
|
+
import { windowsTaskRunsInteractive, windowsTaskRunsVisibly } from '../windowsTaskState';
|
|
3
4
|
export interface DaemonArgs extends ProtectAllArgs {
|
|
4
5
|
install?: string;
|
|
5
6
|
uninstall?: string;
|
|
@@ -83,15 +84,7 @@ export declare function registerHiddenTask(taskName: string, xml: string | strin
|
|
|
83
84
|
/** True when an existing scheduled task's action still runs wscript/cscript
|
|
84
85
|
* (the pre-1.22.1 VBS launcher) — the migration trigger. */
|
|
85
86
|
export declare function windowsTaskReferencesScriptHost(taskName: string): boolean;
|
|
86
|
-
|
|
87
|
-
* the 1.22.1 window-flash bug (console window on every trigger). Migration
|
|
88
|
-
* trigger for the windowless S4U principal. */
|
|
89
|
-
export declare function windowsTaskRunsInteractive(taskName: string): boolean;
|
|
90
|
-
/** True when triggering the task would open a VISIBLE console window:
|
|
91
|
-
* InteractiveToken principal AND a bare console action (not wrapped in
|
|
92
|
-
* `conhost --headless`). This — not the principal alone — is the flash
|
|
93
|
-
* condition; a conhost-wrapped InteractiveToken task is fully windowless. */
|
|
94
|
-
export declare function windowsTaskRunsVisibly(taskName: string): boolean;
|
|
87
|
+
export { windowsTaskRunsInteractive, windowsTaskRunsVisibly };
|
|
95
88
|
/** Snapshot of the resident daemon for out-of-process callers (watchdog/status). */
|
|
96
89
|
export interface DaemonRuntimeState {
|
|
97
90
|
alive: boolean;
|
package/dist/commands/daemon.js
CHANGED
|
@@ -33,6 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.windowsTaskRunsVisibly = exports.windowsTaskRunsInteractive = void 0;
|
|
36
37
|
exports.daemonDiscoverSweepArgs = daemonDiscoverSweepArgs;
|
|
37
38
|
exports.summarizeDiscoverStderr = summarizeDiscoverStderr;
|
|
38
39
|
exports.discoverSweepCredentialEnv = discoverSweepCredentialEnv;
|
|
@@ -44,8 +45,6 @@ exports.buildTaskXml = buildTaskXml;
|
|
|
44
45
|
exports.buildTaskXmlVariants = buildTaskXmlVariants;
|
|
45
46
|
exports.registerHiddenTask = registerHiddenTask;
|
|
46
47
|
exports.windowsTaskReferencesScriptHost = windowsTaskReferencesScriptHost;
|
|
47
|
-
exports.windowsTaskRunsInteractive = windowsTaskRunsInteractive;
|
|
48
|
-
exports.windowsTaskRunsVisibly = windowsTaskRunsVisibly;
|
|
49
48
|
exports.daemonRuntimeState = daemonRuntimeState;
|
|
50
49
|
exports.spawnDetachedDaemon = spawnDetachedDaemon;
|
|
51
50
|
exports.pidIsAlive = pidIsAlive;
|
|
@@ -82,6 +81,9 @@ const honeypot_1 = require("../honeypot");
|
|
|
82
81
|
const windowsAudit_1 = require("./windowsAudit");
|
|
83
82
|
const hook_1 = require("./hook");
|
|
84
83
|
const verdictIpc_1 = require("../verdictIpc");
|
|
84
|
+
const windowsTaskState_1 = require("../windowsTaskState");
|
|
85
|
+
Object.defineProperty(exports, "windowsTaskRunsInteractive", { enumerable: true, get: function () { return windowsTaskState_1.windowsTaskRunsInteractive; } });
|
|
86
|
+
Object.defineProperty(exports, "windowsTaskRunsVisibly", { enumerable: true, get: function () { return windowsTaskState_1.windowsTaskRunsVisibly; } });
|
|
85
87
|
const COLOR = {
|
|
86
88
|
reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
|
|
87
89
|
red: '\x1b[31m', yellow: '\x1b[33m', green: '\x1b[32m', cyan: '\x1b[36m', gray: '\x1b[90m',
|
|
@@ -632,10 +634,18 @@ async function runDaemon(args, config) {
|
|
|
632
634
|
body: JSON.stringify({ shieldId: creds.shieldId, machineId: identity.machineId, ...payload }),
|
|
633
635
|
signal: AbortSignal.timeout(15_000),
|
|
634
636
|
});
|
|
637
|
+
if (!resp.ok) {
|
|
638
|
+
// Log the STATUS and the server's own words. Swallowing them turned a
|
|
639
|
+
// precise backend rejection into "network or backend rejection" in the
|
|
640
|
+
// console, which cost a Cloud Run log dig to diagnose — on the very
|
|
641
|
+
// feature whose job is to explain a machine remotely.
|
|
642
|
+
const body = await resp.text().catch(() => '');
|
|
643
|
+
log(`Diagnostics upload rejected: HTTP ${resp.status}${body ? ` — ${body.slice(0, 300).replace(/\s+/g, ' ')}` : ''}`);
|
|
644
|
+
}
|
|
635
645
|
return resp.ok;
|
|
636
646
|
}
|
|
637
|
-
catch {
|
|
638
|
-
log(
|
|
647
|
+
catch (error) {
|
|
648
|
+
log(`Diagnostics upload failed (network: ${error.message}) — the bundle stays available locally.`);
|
|
639
649
|
return false;
|
|
640
650
|
}
|
|
641
651
|
};
|
|
@@ -932,7 +942,11 @@ async function runDaemon(args, config) {
|
|
|
932
942
|
bundleState = { mode: bundle.mode, source: bundle.source, policyHash: bundle.policyHash };
|
|
933
943
|
}
|
|
934
944
|
catch { /* snapshot still useful without mode context */ }
|
|
935
|
-
|
|
945
|
+
// Async collection — the benchmark spawns real hook children that ask
|
|
946
|
+
// THIS daemon for IPC verdicts, so the event loop must stay free or
|
|
947
|
+
// the snapshot measures its own blockage (2.5s IPC timeout per hook)
|
|
948
|
+
// instead of the true per-event cost.
|
|
949
|
+
const snapshot = await (0, perfSnapshot_1.collectPerfSnapshot)({ cliVersion: cliVersion(), ...bundleState });
|
|
936
950
|
const uploaded = await uploadDiagnostics({ perfSnapshot: snapshot });
|
|
937
951
|
if (!uploaded)
|
|
938
952
|
throw new Error('Perf snapshot could not be uploaded (network or backend rejection).');
|
|
@@ -1553,36 +1567,6 @@ function windowsTaskReferencesScriptHost(taskName) {
|
|
|
1553
1567
|
return false;
|
|
1554
1568
|
}
|
|
1555
1569
|
}
|
|
1556
|
-
/** True when an existing task still runs with an InteractiveToken principal —
|
|
1557
|
-
* the 1.22.1 window-flash bug (console window on every trigger). Migration
|
|
1558
|
-
* trigger for the windowless S4U principal. */
|
|
1559
|
-
function windowsTaskRunsInteractive(taskName) {
|
|
1560
|
-
return windowsTaskXmlState(taskName).interactive;
|
|
1561
|
-
}
|
|
1562
|
-
/** True when triggering the task would open a VISIBLE console window:
|
|
1563
|
-
* InteractiveToken principal AND a bare console action (not wrapped in
|
|
1564
|
-
* `conhost --headless`). This — not the principal alone — is the flash
|
|
1565
|
-
* condition; a conhost-wrapped InteractiveToken task is fully windowless. */
|
|
1566
|
-
function windowsTaskRunsVisibly(taskName) {
|
|
1567
|
-
const state = windowsTaskXmlState(taskName);
|
|
1568
|
-
return state.interactive && !state.headless;
|
|
1569
|
-
}
|
|
1570
|
-
function windowsTaskXmlState(taskName) {
|
|
1571
|
-
try {
|
|
1572
|
-
const query = (0, child_process_1.spawnSync)('schtasks', ['/Query', '/TN', taskName, '/XML'], {
|
|
1573
|
-
encoding: 'utf8', windowsHide: true, timeout: 10_000,
|
|
1574
|
-
});
|
|
1575
|
-
if (query.status !== 0 || typeof query.stdout !== 'string')
|
|
1576
|
-
return { interactive: false, headless: false };
|
|
1577
|
-
return {
|
|
1578
|
-
interactive: /<LogonType>\s*InteractiveToken\s*<\/LogonType>/i.test(query.stdout),
|
|
1579
|
-
headless: /conhost(\.exe)?<\/Command>[\s\S]*?--headless/i.test(query.stdout),
|
|
1580
|
-
};
|
|
1581
|
-
}
|
|
1582
|
-
catch {
|
|
1583
|
-
return { interactive: false, headless: false };
|
|
1584
|
-
}
|
|
1585
|
-
}
|
|
1586
1570
|
/**
|
|
1587
1571
|
* Upgrade an EXISTING (working) task to the windowless S4U principal. Tries
|
|
1588
1572
|
* ONLY the S4U XML — on failure (unelevated daemon) the current task is left
|
|
@@ -1715,7 +1699,7 @@ function startDaemonNowWindows(viaTask = false) {
|
|
|
1715
1699
|
}
|
|
1716
1700
|
}
|
|
1717
1701
|
catch { /* not running */ }
|
|
1718
|
-
if (viaTask && !windowsTaskRunsVisibly(TASK_NAME)) {
|
|
1702
|
+
if (viaTask && !(0, windowsTaskState_1.windowsTaskRunsVisibly)(TASK_NAME)) {
|
|
1719
1703
|
// Start through the scheduled task so the daemon runs with the task's
|
|
1720
1704
|
// LIMITED (non-elevated) token. Launching directly from an elevated MSI
|
|
1721
1705
|
// custom action would leave an elevated daemon that a normal-user CLI can
|
|
@@ -1825,12 +1809,12 @@ function ensureWindowsAutostartHealthy(logFn) {
|
|
|
1825
1809
|
// registration always works and is just as windowless. The updater kick
|
|
1826
1810
|
// stays as the final backstop for machines where even that failed.
|
|
1827
1811
|
let migrationDenied = false;
|
|
1828
|
-
if (daemonTask.status === 0 && !legacyDaemon && windowsTaskRunsVisibly(TASK_NAME)) {
|
|
1812
|
+
if (daemonTask.status === 0 && !legacyDaemon && (0, windowsTaskState_1.windowsTaskRunsVisibly)(TASK_NAME)) {
|
|
1829
1813
|
const daemonXml = buildDaemonTaskXml();
|
|
1830
1814
|
migrationDenied = !(upgradeTaskWindowless(TASK_NAME, daemonXml[0], logFn)
|
|
1831
1815
|
|| rewrapTaskHeadless(TASK_NAME, daemonXml[1], logFn)) || migrationDenied;
|
|
1832
1816
|
}
|
|
1833
|
-
if (watchdogQuery.status === 0 && !legacyWatchdog && windowsTaskRunsVisibly(WATCHDOG_TASK_NAME)) {
|
|
1817
|
+
if (watchdogQuery.status === 0 && !legacyWatchdog && (0, windowsTaskState_1.windowsTaskRunsVisibly)(WATCHDOG_TASK_NAME)) {
|
|
1834
1818
|
const watchdogXml = buildWatchdogTaskXml();
|
|
1835
1819
|
migrationDenied = !(upgradeTaskWindowless(WATCHDOG_TASK_NAME, watchdogXml[0], logFn)
|
|
1836
1820
|
|| rewrapTaskHeadless(WATCHDOG_TASK_NAME, watchdogXml[1], logFn)) || migrationDenied;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -105,7 +105,7 @@ async function perfCheck(apiUrl, config) {
|
|
|
105
105
|
else {
|
|
106
106
|
console.log('mode: not enrolled (local defaults, monitor-first)');
|
|
107
107
|
}
|
|
108
|
-
const snapshot = (0, perfSnapshot_1.collectPerfSnapshot)({ mode, source, policyHash });
|
|
108
|
+
const snapshot = await (0, perfSnapshot_1.collectPerfSnapshot)({ mode, source, policyHash });
|
|
109
109
|
const hp = snapshot.hotPath;
|
|
110
110
|
console.log(`node spawn floor: ${hp.nodeSpawnFloorMs}ms median — OS+AV cost of any per-event process`);
|
|
111
111
|
console.log(`hook end-to-end: ${hp.hookMedianMs}ms median, ${hp.hookMinMs}-${hp.hookMaxMs}ms range — per IDE event (shell/MCP/file)`);
|
package/dist/perfSnapshot.d.ts
CHANGED
|
@@ -7,6 +7,17 @@
|
|
|
7
7
|
* only. Nothing here installs a profiler, starts a timer, or samples in the
|
|
8
8
|
* background — every number is collected in one shot, in a few seconds, and
|
|
9
9
|
* the process that collected it exits.
|
|
10
|
+
*
|
|
11
|
+
* NON-BLOCKING BY CONTRACT: everything here uses async `spawn`, never
|
|
12
|
+
* `spawnSync`. The remote action runs INSIDE the daemon — the same process
|
|
13
|
+
* that answers verdict IPC for every live hook. A spawnSync benchmark froze
|
|
14
|
+
* the daemon's event loop, so each benchmarked hook child asked the daemon,
|
|
15
|
+
* got silence, waited out the full IPC response timeout (~2.5s) and fell back
|
|
16
|
+
* to local evaluation: the snapshot reported "hook median 2605ms" on a machine
|
|
17
|
+
* whose real per-event cost was milliseconds — the measurement was poisoning
|
|
18
|
+
* itself. Async spawns keep the daemon answering during the benchmark, so the
|
|
19
|
+
* number is the true production path (and real developer hooks keep their
|
|
20
|
+
* fast path while a snapshot is being collected).
|
|
10
21
|
*/
|
|
11
22
|
export interface PerfProcessInfo {
|
|
12
23
|
pid: number;
|
|
@@ -67,13 +78,13 @@ export interface PerfSnapshot {
|
|
|
67
78
|
distress: unknown[];
|
|
68
79
|
}
|
|
69
80
|
/** Micro-bench of the per-event hot path: node spawn floor, hook end-to-end, cmd guard. */
|
|
70
|
-
export declare function benchHotPath(entryJs?: string): PerfHotPathBench
|
|
81
|
+
export declare function benchHotPath(entryJs?: string): Promise<PerfHotPathBench>;
|
|
71
82
|
/**
|
|
72
83
|
* Per-process footprint of every FullCourtDefense-owned process (daemon, MCP
|
|
73
84
|
* gateways, desktop chat watcher, watchdog): working set, cumulative CPU,
|
|
74
85
|
* uptime, handle count. On-demand only — this spawns ONE OS query and exits.
|
|
75
86
|
*/
|
|
76
|
-
export declare function collectProcessFootprint(): PerfProcessInfo[]
|
|
87
|
+
export declare function collectProcessFootprint(): Promise<PerfProcessInfo[]>;
|
|
77
88
|
/** Disk footprint: our logs/spool sizes, spool backlog, update-attempt markers. */
|
|
78
89
|
export declare function collectDiskState(): PerfDiskState;
|
|
79
90
|
export interface CollectPerfSnapshotInput {
|
|
@@ -86,6 +97,6 @@ export interface CollectPerfSnapshotInput {
|
|
|
86
97
|
entryJs?: string;
|
|
87
98
|
}
|
|
88
99
|
/** One-shot collection of the full snapshot. Completes in a few seconds. */
|
|
89
|
-
export declare function collectPerfSnapshot(input?: CollectPerfSnapshotInput): PerfSnapshot
|
|
100
|
+
export declare function collectPerfSnapshot(input?: CollectPerfSnapshotInput): Promise<PerfSnapshot>;
|
|
90
101
|
/** One-line human summary for machine-action result reporting. */
|
|
91
102
|
export declare function summarizePerfSnapshot(s: PerfSnapshot): string;
|
package/dist/perfSnapshot.js
CHANGED
|
@@ -55,23 +55,61 @@ function fileSizeKB(p) {
|
|
|
55
55
|
return undefined;
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Run one child to completion WITHOUT blocking the event loop, returning
|
|
60
|
+
* elapsed wall time and captured stdout. The event loop staying free is the
|
|
61
|
+
* whole point (see the header): the daemon must keep answering verdict IPC
|
|
62
|
+
* while these children run. Never throws — a spawn failure or timeout still
|
|
63
|
+
* resolves with whatever elapsed/output there is.
|
|
64
|
+
*/
|
|
65
|
+
function runTimed(cmd, args, opts) {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const t0 = Date.now();
|
|
68
|
+
let stdout = '';
|
|
69
|
+
let settled = false;
|
|
70
|
+
let killTimer;
|
|
71
|
+
const finish = () => {
|
|
72
|
+
if (settled)
|
|
73
|
+
return;
|
|
74
|
+
settled = true;
|
|
75
|
+
if (killTimer)
|
|
76
|
+
clearTimeout(killTimer);
|
|
77
|
+
resolve({ elapsedMs: Date.now() - t0, stdout });
|
|
78
|
+
};
|
|
79
|
+
let child;
|
|
80
|
+
try {
|
|
81
|
+
child = (0, child_process_1.spawn)(cmd, args, { windowsHide: true, stdio: ['pipe', 'pipe', 'ignore'] });
|
|
82
|
+
}
|
|
83
|
+
catch {
|
|
84
|
+
finish();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
killTimer = setTimeout(() => { try {
|
|
88
|
+
child.kill();
|
|
89
|
+
}
|
|
90
|
+
catch { /* already gone */ } }, opts.timeoutMs);
|
|
91
|
+
child.on('error', finish);
|
|
92
|
+
child.on('close', finish);
|
|
93
|
+
child.stdout?.on('data', (chunk) => { stdout += String(chunk); });
|
|
94
|
+
child.stdin?.on('error', () => { });
|
|
95
|
+
if (opts.input !== undefined)
|
|
96
|
+
child.stdin?.write(opts.input);
|
|
97
|
+
child.stdin?.end();
|
|
98
|
+
});
|
|
99
|
+
}
|
|
58
100
|
/** Micro-bench of the per-event hot path: node spawn floor, hook end-to-end, cmd guard. */
|
|
59
|
-
function benchHotPath(entryJs) {
|
|
101
|
+
async function benchHotPath(entryJs) {
|
|
60
102
|
const entry = entryJs || process.argv[1] || '';
|
|
103
|
+
// Sequential on purpose: overlapping children would contend for CPU and
|
|
104
|
+
// inflate each other's numbers.
|
|
61
105
|
const spawnTimes = [];
|
|
62
106
|
for (let i = 0; i < 5; i++) {
|
|
63
|
-
|
|
64
|
-
(0, child_process_1.spawnSync)(process.execPath, ['-e', '0'], { windowsHide: true, timeout: 30_000 });
|
|
65
|
-
spawnTimes.push(Date.now() - t0);
|
|
107
|
+
spawnTimes.push((await runTimed(process.execPath, ['-e', '0'], { timeoutMs: 30_000 })).elapsedMs);
|
|
66
108
|
}
|
|
67
109
|
const hookEvent = JSON.stringify({ hook_event_name: 'beforeShellExecution', command: 'echo fcd-perf-probe' });
|
|
68
110
|
const hookTimes = [];
|
|
69
111
|
for (let i = 0; i < 5; i++) {
|
|
70
|
-
|
|
71
|
-
(0, child_process_1.spawnSync)(process.execPath, [entry, 'hook', '--event', 'shell', '--fcd-managed', 'true'], {
|
|
72
|
-
input: hookEvent, encoding: 'utf8', windowsHide: true, timeout: 60_000,
|
|
73
|
-
});
|
|
74
|
-
hookTimes.push(Date.now() - t0);
|
|
112
|
+
hookTimes.push((await runTimed(process.execPath, [entry, 'hook', '--event', 'shell', '--fcd-managed', 'true'], { input: hookEvent, timeoutMs: 60_000 })).elapsedMs);
|
|
75
113
|
}
|
|
76
114
|
let cmdGuardOverheadMs;
|
|
77
115
|
const guardJs = path.join(os.homedir(), '.fullcourtdefense-cmd-guard.js');
|
|
@@ -79,12 +117,8 @@ function benchHotPath(entryJs) {
|
|
|
79
117
|
const direct = [];
|
|
80
118
|
const guarded = [];
|
|
81
119
|
for (let i = 0; i < 4; i++) {
|
|
82
|
-
|
|
83
|
-
(
|
|
84
|
-
direct.push(Date.now() - t0);
|
|
85
|
-
t0 = Date.now();
|
|
86
|
-
(0, child_process_1.spawnSync)(process.execPath, [guardJs, 'echo', 'fcd-perf'], { windowsHide: true, timeout: 30_000 });
|
|
87
|
-
guarded.push(Date.now() - t0);
|
|
120
|
+
direct.push((await runTimed(process.env.ComSpec || 'cmd.exe', ['/d', '/c', 'echo', 'fcd-perf'], { timeoutMs: 30_000 })).elapsedMs);
|
|
121
|
+
guarded.push((await runTimed(process.execPath, [guardJs, 'echo', 'fcd-perf'], { timeoutMs: 30_000 })).elapsedMs);
|
|
88
122
|
}
|
|
89
123
|
cmdGuardOverheadMs = Math.max(0, median(guarded) - median(direct));
|
|
90
124
|
}
|
|
@@ -102,11 +136,11 @@ function benchHotPath(entryJs) {
|
|
|
102
136
|
* gateways, desktop chat watcher, watchdog): working set, cumulative CPU,
|
|
103
137
|
* uptime, handle count. On-demand only — this spawns ONE OS query and exits.
|
|
104
138
|
*/
|
|
105
|
-
function collectProcessFootprint() {
|
|
139
|
+
async function collectProcessFootprint() {
|
|
106
140
|
try {
|
|
107
141
|
if (process.platform === 'win32') {
|
|
108
142
|
// On-demand action (never a hot path), so one PowerShell query is fine.
|
|
109
|
-
const ps =
|
|
143
|
+
const ps = await runTimed('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', `
|
|
110
144
|
$now = Get-Date
|
|
111
145
|
Get-CimInstance Win32_Process |
|
|
112
146
|
Where-Object { $_.CommandLine -and ($_.CommandLine -match 'fullcourtdefense' -or $_.CommandLine -match 'FullCourtDefense') -and $_.CommandLine -notmatch 'Get-CimInstance' } |
|
|
@@ -121,7 +155,7 @@ function collectProcessFootprint() {
|
|
|
121
155
|
cmd = if ($_.CommandLine.Length -gt 160) { $_.CommandLine.Substring(0,160) } else { $_.CommandLine }
|
|
122
156
|
}
|
|
123
157
|
} | ConvertTo-Json -Compress
|
|
124
|
-
`], {
|
|
158
|
+
`], { timeoutMs: 20_000 });
|
|
125
159
|
const raw = (ps.stdout || '').trim();
|
|
126
160
|
if (!raw)
|
|
127
161
|
return [];
|
|
@@ -138,7 +172,7 @@ function collectProcessFootprint() {
|
|
|
138
172
|
}));
|
|
139
173
|
}
|
|
140
174
|
// POSIX: rss (KB), cumulative cpu time, elapsed, args.
|
|
141
|
-
const ps =
|
|
175
|
+
const ps = await runTimed('ps', ['-eo', 'pid=,rss=,time=,etimes=,args='], { timeoutMs: 20_000 });
|
|
142
176
|
const lines = (ps.stdout || '').split('\n').filter(l => /fullcourtdefense/i.test(l) && !/ps -eo/.test(l));
|
|
143
177
|
const result = [];
|
|
144
178
|
for (const line of lines) {
|
|
@@ -190,8 +224,8 @@ function collectDiskState() {
|
|
|
190
224
|
};
|
|
191
225
|
}
|
|
192
226
|
/** One-shot collection of the full snapshot. Completes in a few seconds. */
|
|
193
|
-
function collectPerfSnapshot(input = {}) {
|
|
194
|
-
const processes = collectProcessFootprint();
|
|
227
|
+
async function collectPerfSnapshot(input = {}) {
|
|
228
|
+
const processes = await collectProcessFootprint();
|
|
195
229
|
return {
|
|
196
230
|
collectedAt: new Date().toISOString(),
|
|
197
231
|
cliVersion: input.cliVersion,
|
|
@@ -203,7 +237,7 @@ function collectPerfSnapshot(input = {}) {
|
|
|
203
237
|
node: process.version,
|
|
204
238
|
},
|
|
205
239
|
enforcement: { mode: input.mode, source: input.source, policyHash: input.policyHash },
|
|
206
|
-
hotPath: benchHotPath(input.entryJs),
|
|
240
|
+
hotPath: await benchHotPath(input.entryJs),
|
|
207
241
|
processes,
|
|
208
242
|
totalWorkingSetMB: Math.round(processes.reduce((sum, p) => sum + p.workingSetMB, 0) * 10) / 10,
|
|
209
243
|
disk: collectDiskState(),
|
package/dist/selfTest.js
CHANGED
|
@@ -9,6 +9,7 @@ const machineKeyFile_1 = require("./machineKeyFile");
|
|
|
9
9
|
const runtimeConfig_1 = require("./runtimeConfig");
|
|
10
10
|
const integrity_1 = require("./integrity");
|
|
11
11
|
const selfUpdate_1 = require("./selfUpdate");
|
|
12
|
+
const windowsTaskState_1 = require("./windowsTaskState");
|
|
12
13
|
/** Windows scheduled-task probe: exists + (when readable) its last result code. */
|
|
13
14
|
function queryScheduledTask(taskName) {
|
|
14
15
|
const query = (0, child_process_1.spawnSync)('schtasks', ['/Query', '/TN', taskName, '/V', '/FO', 'LIST'], {
|
|
@@ -145,38 +146,49 @@ async function runDeepSelfTest(input) {
|
|
|
145
146
|
add({ id: 'watchdog_task', label: 'Watchdog task registered', ok: false, detail: error instanceof Error ? error.message : String(error) });
|
|
146
147
|
}
|
|
147
148
|
}
|
|
148
|
-
// 9. Windowless
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
149
|
+
// 9. Windowless tasks. What users complain about is a console window flashing
|
|
150
|
+
// on every trigger (the "5-minute flicker" class); <Hidden>true</Hidden> does
|
|
151
|
+
// NOT suppress it. Two independent defences prevent it: the S4U logon type,
|
|
152
|
+
// and wrapping the action in `conhost --headless`. Either one is sufficient,
|
|
153
|
+
// so this check reports the OBSERVABLE symptom — never the principal alone,
|
|
154
|
+
// which is red on every healthy MSI install (Windows denies S4U registration
|
|
155
|
+
// even to SYSTEM, which is exactly why the conhost wrapper exists).
|
|
153
156
|
if (process.platform === 'win32') {
|
|
154
|
-
const
|
|
157
|
+
const flashing = listVisiblyFlashingFcdTasks();
|
|
158
|
+
const conhostShielded = listConhostShieldedFcdTasks();
|
|
155
159
|
add({
|
|
156
160
|
id: 'windowless_tasks',
|
|
157
|
-
label: 'Scheduled tasks are windowless
|
|
158
|
-
ok:
|
|
159
|
-
detail:
|
|
160
|
-
?
|
|
161
|
-
|
|
161
|
+
label: 'Scheduled tasks are windowless',
|
|
162
|
+
ok: flashing.length === 0,
|
|
163
|
+
detail: flashing.length === 0
|
|
164
|
+
? conhostShielded.length === 0
|
|
165
|
+
? 'no FCD task can open a console window (S4U principals)'
|
|
166
|
+
: `no FCD task can open a console window (${conhostShielded.length} still on an InteractiveToken principal, made windowless by conhost --headless)`
|
|
167
|
+
: `visible console window on every trigger of: ${flashing.join(', ')} — bare console action on an InteractiveToken principal (neither S4U nor conhost --headless)`,
|
|
162
168
|
});
|
|
163
169
|
}
|
|
164
170
|
return { ok: checks.every(check => check.ok), checks, ranAt: new Date().toISOString() };
|
|
165
171
|
}
|
|
166
|
-
/** FCD per-user tasks
|
|
172
|
+
/** FCD per-user tasks that could flash a console window on every trigger. */
|
|
167
173
|
const WINDOWED_TASK_CANDIDATES = ['FullCourtDefense Daemon', 'FullCourtDefense Daemon Watchdog', 'FullCourtDefense Auto-Protect', 'FullCourtDefenseDesktopDiscover'];
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
174
|
+
/**
|
|
175
|
+
* Tasks that would actually open a VISIBLE window. Delegates to the shared
|
|
176
|
+
* `windowsTaskRunsVisibly` so this check can never drift from the daemon's own
|
|
177
|
+
* definition of the flash condition again: an InteractiveToken principal is
|
|
178
|
+
* harmless once the action runs through `conhost --headless`, and that is the
|
|
179
|
+
* normal state of an MSI install, because current Windows builds refuse S4U
|
|
180
|
+
* registration even to SYSTEM. Reporting on the principal alone failed every
|
|
181
|
+
* healthy machine and trained operators to ignore the whole report.
|
|
182
|
+
*/
|
|
183
|
+
function listVisiblyFlashingFcdTasks() {
|
|
184
|
+
return WINDOWED_TASK_CANDIDATES.filter(taskName => (0, windowsTaskState_1.windowsTaskRunsVisibly)(taskName));
|
|
185
|
+
}
|
|
186
|
+
/** Registered tasks still on InteractiveToken but made windowless by conhost. */
|
|
187
|
+
function listConhostShieldedFcdTasks() {
|
|
188
|
+
return WINDOWED_TASK_CANDIDATES.filter(taskName => {
|
|
189
|
+
const state = (0, windowsTaskState_1.windowsTaskXmlState)(taskName);
|
|
190
|
+
return state.exists && state.interactive && state.headless;
|
|
191
|
+
});
|
|
180
192
|
}
|
|
181
193
|
/** Compact one-line summary for the machine-action result card. */
|
|
182
194
|
function summarizeSelfTest(report) {
|
package/dist/version.json
CHANGED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows scheduled-task principal/action state — the single source of truth
|
|
3
|
+
* for "would this task flash a console window?".
|
|
4
|
+
*
|
|
5
|
+
* Why this is its own module: the daemon (which registers and self-heals the
|
|
6
|
+
* tasks) and the deep self-test (which reports on them) both need the answer,
|
|
7
|
+
* and `daemon.ts` already imports `selfTest.ts` for the health_check action, so
|
|
8
|
+
* a direct import back would be a cycle. Both now depend on this leaf instead.
|
|
9
|
+
*
|
|
10
|
+
* It also settles a correctness split. `selfTest.ts` used to carry its own copy
|
|
11
|
+
* that checked the PRINCIPAL alone, so it reported a failure on every machine
|
|
12
|
+
* whose tasks are conhost-wrapped — which is all MSI installs, because current
|
|
13
|
+
* Windows builds deny S4U registration even to SYSTEM. The flash condition is
|
|
14
|
+
* the pair of facts, never the principal by itself.
|
|
15
|
+
*
|
|
16
|
+
* Pure apart from the `schtasks /Query` read; never throws.
|
|
17
|
+
*/
|
|
18
|
+
export interface WindowsTaskState {
|
|
19
|
+
/** Task exists and declares an InteractiveToken principal. */
|
|
20
|
+
interactive: boolean;
|
|
21
|
+
/** Task action runs through `conhost.exe --headless` (windowless regardless of principal). */
|
|
22
|
+
headless: boolean;
|
|
23
|
+
/** Task is registered at all — absent tasks can't flash. */
|
|
24
|
+
exists: boolean;
|
|
25
|
+
}
|
|
26
|
+
export declare function windowsTaskXmlState(taskName: string): WindowsTaskState;
|
|
27
|
+
/**
|
|
28
|
+
* True when an existing task still runs with an InteractiveToken principal —
|
|
29
|
+
* the 1.22.1 window-flash bug's principal half. Migration trigger for the
|
|
30
|
+
* windowless S4U principal. NOT the flash condition on its own: see
|
|
31
|
+
* `windowsTaskRunsVisibly`.
|
|
32
|
+
*/
|
|
33
|
+
export declare function windowsTaskRunsInteractive(taskName: string): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* True when triggering the task would open a VISIBLE console window:
|
|
36
|
+
* InteractiveToken principal AND a bare console action (not wrapped in
|
|
37
|
+
* `conhost --headless`). This — not the principal alone — is the flash
|
|
38
|
+
* condition; a conhost-wrapped InteractiveToken task is fully windowless.
|
|
39
|
+
*/
|
|
40
|
+
export declare function windowsTaskRunsVisibly(taskName: string): boolean;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.windowsTaskXmlState = windowsTaskXmlState;
|
|
4
|
+
exports.windowsTaskRunsInteractive = windowsTaskRunsInteractive;
|
|
5
|
+
exports.windowsTaskRunsVisibly = windowsTaskRunsVisibly;
|
|
6
|
+
const child_process_1 = require("child_process");
|
|
7
|
+
function windowsTaskXmlState(taskName) {
|
|
8
|
+
try {
|
|
9
|
+
const query = (0, child_process_1.spawnSync)('schtasks', ['/Query', '/TN', taskName, '/XML'], {
|
|
10
|
+
encoding: 'utf8', windowsHide: true, timeout: 10_000,
|
|
11
|
+
});
|
|
12
|
+
if (query.status !== 0 || typeof query.stdout !== 'string') {
|
|
13
|
+
return { interactive: false, headless: false, exists: false };
|
|
14
|
+
}
|
|
15
|
+
return {
|
|
16
|
+
exists: true,
|
|
17
|
+
interactive: /<LogonType>\s*InteractiveToken\s*<\/LogonType>/i.test(query.stdout),
|
|
18
|
+
headless: /conhost(\.exe)?<\/Command>[\s\S]*?--headless/i.test(query.stdout),
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return { interactive: false, headless: false, exists: false };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* True when an existing task still runs with an InteractiveToken principal —
|
|
27
|
+
* the 1.22.1 window-flash bug's principal half. Migration trigger for the
|
|
28
|
+
* windowless S4U principal. NOT the flash condition on its own: see
|
|
29
|
+
* `windowsTaskRunsVisibly`.
|
|
30
|
+
*/
|
|
31
|
+
function windowsTaskRunsInteractive(taskName) {
|
|
32
|
+
return windowsTaskXmlState(taskName).interactive;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* True when triggering the task would open a VISIBLE console window:
|
|
36
|
+
* InteractiveToken principal AND a bare console action (not wrapped in
|
|
37
|
+
* `conhost --headless`). This — not the principal alone — is the flash
|
|
38
|
+
* condition; a conhost-wrapped InteractiveToken task is fully windowless.
|
|
39
|
+
*/
|
|
40
|
+
function windowsTaskRunsVisibly(taskName) {
|
|
41
|
+
const state = windowsTaskXmlState(taskName);
|
|
42
|
+
return state.interactive && !state.headless;
|
|
43
|
+
}
|