@yeaft/webchat-agent 1.0.413 → 1.0.415
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/browser-runtime/browser-install.js +497 -0
- package/browser-runtime/cli.js +88 -0
- package/browser-runtime/config.js +116 -0
- package/browser-runtime/errors.js +8 -0
- package/browser-runtime/extension/manifest.json +18 -0
- package/browser-runtime/extension/offscreen.html +5 -0
- package/browser-runtime/extension/offscreen.js +101 -0
- package/browser-runtime/extension/popup.html +5 -0
- package/browser-runtime/extension/popup.js +1 -0
- package/browser-runtime/extension/service-worker.js +48 -0
- package/browser-runtime/extension.js +45 -0
- package/browser-runtime/index.js +5 -0
- package/browser-runtime/probe.js +427 -0
- package/browser-runtime/protocol.js +71 -0
- package/browser-runtime/service.js +132 -0
- package/browser-runtime/windows-version-job.ps1 +233 -0
- package/browser-runtime/windows-version-worker.js +75 -0
- package/browser-runtime/windows-version.js +85 -0
- package/cli.js +24 -7
- package/connection/index.js +12 -0
- package/context.js +1 -0
- package/index.js +19 -2
- package/llm-config-cli.js +24 -21
- package/local-runtime/server/client-protocol.js +14 -0
- package/local-runtime/server/context.js +3 -2
- package/local-runtime/server/handlers/agent-file-terminal.js +185 -115
- package/local-runtime/server/handlers/agent-output.js +3 -0
- package/local-runtime/server/handlers/client-misc.js +21 -4
- package/local-runtime/server/handlers/client-workbench.js +222 -41
- package/local-runtime/server/workbench-correlation.js +184 -0
- package/local-runtime/server/workbench-route.js +180 -0
- package/local-runtime/server/ws-agent.js +4 -0
- package/local-runtime/server/ws-client.js +25 -3
- package/local-runtime/version.json +1 -1
- package/local-runtime/web/app.bundle.js +191 -135
- package/local-runtime/web/app.bundle.js.gz +0 -0
- package/local-runtime/web/index.html +2 -2
- package/local-runtime/web/style.bundle.css +1 -1
- package/local-runtime/web/style.bundle.css.gz +0 -0
- package/package.json +5 -1
- package/service/config.js +23 -2
- package/service/index.js +1 -0
- package/service/linux.js +3 -2
- package/terminal.js +167 -30
- package/workbench/file-ops.js +21 -20
- package/workbench/file-search.js +4 -3
- package/workbench/git-ops.js +23 -22
- package/workbench/request-routing.js +16 -0
- package/yeaft/cli.js +57 -1
- package/yeaft/config-api.js +138 -192
- package/yeaft/config-store.js +192 -0
- package/yeaft/config.js +3 -0
- package/yeaft/init.js +20 -7
- package/yeaft/sessions/feature-flag.js +15 -33
- package/yeaft/sessions/session-manifest.js +114 -10
- package/yeaft/stdio-protocol.js +57 -0
- package/yeaft/storage/atomic.js +43 -17
- package/yeaft/tools/process-runner.js +86 -13
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export const BROWSER_PROTOCOL_VERSION = 1;
|
|
2
|
+
export const BROWSER_CONTROL_CHANNEL = 'browser.control.v1';
|
|
3
|
+
export const BROWSER_POINTER_CHANNEL = 'browser.pointer.v1';
|
|
4
|
+
export const BROWSER_STATE_CHANNEL = 'browser.state.v1';
|
|
5
|
+
|
|
6
|
+
function positiveInteger(value) {
|
|
7
|
+
return Number.isSafeInteger(value) && value > 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Per-producer sequence fencing. Reliable control and lossy pointer traffic
|
|
12
|
+
* intentionally have independent sequence spaces: pointer loss or reordering
|
|
13
|
+
* must never advance or block reliable keyboard/click/navigation actions.
|
|
14
|
+
*/
|
|
15
|
+
export class ProducerSequenceState {
|
|
16
|
+
/** @param {{producerId:string, producerGeneration:number}} identity */
|
|
17
|
+
constructor({ producerId, producerGeneration }) {
|
|
18
|
+
if (typeof producerId !== 'string' || !producerId) throw new Error('producerId required');
|
|
19
|
+
if (!positiveInteger(producerGeneration)) throw new Error('producerGeneration must be a positive integer');
|
|
20
|
+
this.producerId = producerId;
|
|
21
|
+
this.producerGeneration = producerGeneration;
|
|
22
|
+
this.lastAcceptedControlSeq = 0;
|
|
23
|
+
this.lastAcceptedPointerSeq = 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
#matches(envelope) {
|
|
27
|
+
return envelope?.producerId === this.producerId
|
|
28
|
+
&& envelope?.producerGeneration === this.producerGeneration;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Accept only the next gap-free reliable sequence number.
|
|
33
|
+
* @returns {{accepted:boolean, code:string, expectedControlSeq:number}}
|
|
34
|
+
*/
|
|
35
|
+
acceptControl(envelope) {
|
|
36
|
+
const expectedControlSeq = this.lastAcceptedControlSeq + 1;
|
|
37
|
+
if (!this.#matches(envelope)) return { accepted: false, code: 'producer_stale', expectedControlSeq };
|
|
38
|
+
if (!positiveInteger(envelope.controlSeq)) return { accepted: false, code: 'control_seq_invalid', expectedControlSeq };
|
|
39
|
+
if (envelope.controlSeq < expectedControlSeq) return { accepted: false, code: 'control_duplicate', expectedControlSeq };
|
|
40
|
+
if (envelope.controlSeq > expectedControlSeq) return { accepted: false, code: 'control_gap', expectedControlSeq };
|
|
41
|
+
this.lastAcceptedControlSeq = envelope.controlSeq;
|
|
42
|
+
return { accepted: true, code: 'accepted', expectedControlSeq: this.lastAcceptedControlSeq + 1 };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Accept any strictly newer lossy pointer sequence number. Gaps are expected.
|
|
47
|
+
* @returns {{accepted:boolean, code:string, pointerHighWater:number}}
|
|
48
|
+
*/
|
|
49
|
+
acceptPointer(envelope) {
|
|
50
|
+
if (!this.#matches(envelope)) {
|
|
51
|
+
return { accepted: false, code: 'producer_stale', pointerHighWater: this.lastAcceptedPointerSeq };
|
|
52
|
+
}
|
|
53
|
+
if (!positiveInteger(envelope.pointerSeq)) {
|
|
54
|
+
return { accepted: false, code: 'pointer_seq_invalid', pointerHighWater: this.lastAcceptedPointerSeq };
|
|
55
|
+
}
|
|
56
|
+
if (envelope.pointerSeq <= this.lastAcceptedPointerSeq) {
|
|
57
|
+
return { accepted: false, code: 'pointer_stale', pointerHighWater: this.lastAcceptedPointerSeq };
|
|
58
|
+
}
|
|
59
|
+
this.lastAcceptedPointerSeq = envelope.pointerSeq;
|
|
60
|
+
return { accepted: true, code: 'accepted', pointerHighWater: this.lastAcceptedPointerSeq };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
snapshot() {
|
|
64
|
+
return Object.freeze({
|
|
65
|
+
producerId: this.producerId,
|
|
66
|
+
producerGeneration: this.producerGeneration,
|
|
67
|
+
lastAcceptedControlSeq: this.lastAcceptedControlSeq,
|
|
68
|
+
lastAcceptedPointerSeq: this.lastAcceptedPointerSeq,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { normaliseBrowserRuntimeSection } from './config.js';
|
|
3
|
+
import { defaultBrowserCacheDir } from './browser-install.js';
|
|
4
|
+
import { probeBrowserRuntime } from './probe.js';
|
|
5
|
+
import { BrowserRuntimeError } from './errors.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Agent-local Browser Runtime lifecycle owner. Phase 0 exposes startup probe and
|
|
9
|
+
* capacity semantics only; Browser Sessions remain unavailable until the later
|
|
10
|
+
* owner-checked control-plane phase lands.
|
|
11
|
+
*/
|
|
12
|
+
export class BrowserRuntimeService {
|
|
13
|
+
constructor({ yeaftDir, config, probe = probeBrowserRuntime } = {}) {
|
|
14
|
+
if (!yeaftDir) throw new Error('yeaftDir required');
|
|
15
|
+
this.yeaftDir = yeaftDir;
|
|
16
|
+
this.config = normaliseBrowserRuntimeSection(config);
|
|
17
|
+
this.config.cacheDir ||= defaultBrowserCacheDir(yeaftDir);
|
|
18
|
+
this.probe = probe;
|
|
19
|
+
this.sessions = new Map();
|
|
20
|
+
this.probeResult = null;
|
|
21
|
+
this.state = this.config.enabled ? 'unprobed' : 'disabled';
|
|
22
|
+
this.#probePromise = null;
|
|
23
|
+
this.#probeAbort = null;
|
|
24
|
+
this.#shutdownPromise = null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#probePromise;
|
|
28
|
+
#probeAbort;
|
|
29
|
+
#shutdownPromise;
|
|
30
|
+
|
|
31
|
+
get enabled() { return this.config.enabled === true; }
|
|
32
|
+
get ready() { return this.state === 'ready' && this.probeResult?.ok === true; }
|
|
33
|
+
|
|
34
|
+
capabilities() {
|
|
35
|
+
// Phase 0 is a local spike only. Advertising the Phase 1 feature before
|
|
36
|
+
// owner-checked create/attach/signaling exists would expose a dead route.
|
|
37
|
+
return [];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async startupProbe() {
|
|
41
|
+
if (!this.enabled) return { ok: false, code: 'browser_runtime_disabled' };
|
|
42
|
+
if (this.#probePromise) return this.#probePromise;
|
|
43
|
+
this.state = 'probing';
|
|
44
|
+
this.#probeAbort = new AbortController();
|
|
45
|
+
this.#probePromise = this.probe({
|
|
46
|
+
executablePath: this.config.executablePath,
|
|
47
|
+
cacheDir: this.config.cacheDir,
|
|
48
|
+
headless: this.config.headless,
|
|
49
|
+
timeoutMs: this.config.startupProbeTimeoutMs,
|
|
50
|
+
profileParent: `${this.config.cacheDir}-profiles`,
|
|
51
|
+
signal: this.#probeAbort.signal,
|
|
52
|
+
}).then(result => {
|
|
53
|
+
this.probeResult = Object.freeze({ ...result });
|
|
54
|
+
this.state = result.ok ? 'ready' : 'unavailable';
|
|
55
|
+
return this.probeResult;
|
|
56
|
+
}).catch(error => {
|
|
57
|
+
this.probeResult = Object.freeze({
|
|
58
|
+
ok: false,
|
|
59
|
+
code: error?.code || 'browser_probe_failed',
|
|
60
|
+
safeError: String(error?.message || error).slice(0, 500),
|
|
61
|
+
});
|
|
62
|
+
this.state = 'unavailable';
|
|
63
|
+
return this.probeResult;
|
|
64
|
+
});
|
|
65
|
+
return this.#probePromise;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
assertCanCreateSession() {
|
|
69
|
+
if (!this.ready) throw new BrowserRuntimeError('browser_runtime_unavailable');
|
|
70
|
+
if (this.sessions.size >= this.config.maxSessions) {
|
|
71
|
+
throw new BrowserRuntimeError('browser_session_limit');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Phase 0 test hook for lifecycle/capacity ownership. */
|
|
76
|
+
reserveSession(ownerUserId) {
|
|
77
|
+
this.assertCanCreateSession();
|
|
78
|
+
if (!ownerUserId) throw new BrowserRuntimeError('browser_owner_required');
|
|
79
|
+
const browserSessionId = randomUUID();
|
|
80
|
+
this.sessions.set(browserSessionId, Object.freeze({
|
|
81
|
+
browserSessionId,
|
|
82
|
+
ownerUserId,
|
|
83
|
+
state: 'reserved',
|
|
84
|
+
createdAt: Date.now(),
|
|
85
|
+
}));
|
|
86
|
+
return this.sessions.get(browserSessionId);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
releaseSession(browserSessionId) {
|
|
90
|
+
return this.sessions.delete(browserSessionId);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
snapshot() {
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
enabled: this.enabled,
|
|
96
|
+
ready: this.ready,
|
|
97
|
+
state: this.state,
|
|
98
|
+
activeSessions: this.sessions.size,
|
|
99
|
+
maxSessions: this.config.maxSessions,
|
|
100
|
+
probe: this.probeResult,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async shutdown() {
|
|
105
|
+
if (this.#shutdownPromise) return this.#shutdownPromise;
|
|
106
|
+
this.#probeAbort?.abort(new BrowserRuntimeError('browser_runtime_shutdown'));
|
|
107
|
+
this.#shutdownPromise = Promise.resolve(this.#probePromise).catch(() => {}).then(() => {
|
|
108
|
+
this.sessions.clear();
|
|
109
|
+
this.state = 'closed';
|
|
110
|
+
});
|
|
111
|
+
return this.#shutdownPromise;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
let runtime = null;
|
|
116
|
+
|
|
117
|
+
export function getBrowserRuntime() {
|
|
118
|
+
return runtime;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export async function bootBrowserRuntime(options) {
|
|
122
|
+
if (runtime) return runtime;
|
|
123
|
+
runtime = new BrowserRuntimeService(options);
|
|
124
|
+
await runtime.startupProbe();
|
|
125
|
+
return runtime;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export async function shutdownBrowserRuntime() {
|
|
129
|
+
const current = runtime;
|
|
130
|
+
runtime = null;
|
|
131
|
+
await current?.shutdown();
|
|
132
|
+
}
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
param(
|
|
2
|
+
[Parameter(Mandatory = $true)][string]$NodePath,
|
|
3
|
+
[Parameter(Mandatory = $true)][string]$WorkerPath,
|
|
4
|
+
[Parameter(Mandatory = $true)][string]$ExecutablePath,
|
|
5
|
+
[int]$CleanupTimeoutMs = 500
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
$ErrorActionPreference = 'Stop'
|
|
9
|
+
|
|
10
|
+
Add-Type -TypeDefinition @'
|
|
11
|
+
using System;
|
|
12
|
+
using System.Runtime.InteropServices;
|
|
13
|
+
using System.Text;
|
|
14
|
+
|
|
15
|
+
public static class YeaftBrowserJob {
|
|
16
|
+
public const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
|
|
17
|
+
public const int JobObjectBasicAccountingInformation = 1;
|
|
18
|
+
public const int JobObjectExtendedLimitInformation = 9;
|
|
19
|
+
public const uint PROCESS_SET_QUOTA = 0x0100;
|
|
20
|
+
public const uint PROCESS_TERMINATE = 0x0001;
|
|
21
|
+
|
|
22
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
23
|
+
public struct IO_COUNTERS {
|
|
24
|
+
public UInt64 ReadOperationCount;
|
|
25
|
+
public UInt64 WriteOperationCount;
|
|
26
|
+
public UInt64 OtherOperationCount;
|
|
27
|
+
public UInt64 ReadTransferCount;
|
|
28
|
+
public UInt64 WriteTransferCount;
|
|
29
|
+
public UInt64 OtherTransferCount;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
33
|
+
public struct JOBOBJECT_BASIC_LIMIT_INFORMATION {
|
|
34
|
+
public Int64 PerProcessUserTimeLimit;
|
|
35
|
+
public Int64 PerJobUserTimeLimit;
|
|
36
|
+
public UInt32 LimitFlags;
|
|
37
|
+
public UIntPtr MinimumWorkingSetSize;
|
|
38
|
+
public UIntPtr MaximumWorkingSetSize;
|
|
39
|
+
public UInt32 ActiveProcessLimit;
|
|
40
|
+
public UIntPtr Affinity;
|
|
41
|
+
public UInt32 PriorityClass;
|
|
42
|
+
public UInt32 SchedulingClass;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
46
|
+
public struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION {
|
|
47
|
+
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
|
|
48
|
+
public IO_COUNTERS IoInfo;
|
|
49
|
+
public UIntPtr ProcessMemoryLimit;
|
|
50
|
+
public UIntPtr JobMemoryLimit;
|
|
51
|
+
public UIntPtr PeakProcessMemoryUsed;
|
|
52
|
+
public UIntPtr PeakJobMemoryUsed;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
56
|
+
public struct JOBOBJECT_BASIC_ACCOUNTING_INFORMATION {
|
|
57
|
+
public Int64 TotalUserTime;
|
|
58
|
+
public Int64 TotalKernelTime;
|
|
59
|
+
public Int64 ThisPeriodTotalUserTime;
|
|
60
|
+
public Int64 ThisPeriodTotalKernelTime;
|
|
61
|
+
public UInt32 TotalPageFaultCount;
|
|
62
|
+
public UInt32 TotalProcesses;
|
|
63
|
+
public UInt32 ActiveProcesses;
|
|
64
|
+
public UInt32 TotalTerminatedProcesses;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
|
68
|
+
public static extern IntPtr CreateJobObject(IntPtr securityAttributes, string name);
|
|
69
|
+
|
|
70
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
71
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
72
|
+
public static extern bool SetInformationJobObject(
|
|
73
|
+
IntPtr job,
|
|
74
|
+
int infoClass,
|
|
75
|
+
IntPtr info,
|
|
76
|
+
UInt32 length
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
80
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
81
|
+
public static extern bool AssignProcessToJobObject(IntPtr job, IntPtr process);
|
|
82
|
+
|
|
83
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
84
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
85
|
+
public static extern bool TerminateJobObject(IntPtr job, UInt32 exitCode);
|
|
86
|
+
|
|
87
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
88
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
89
|
+
public static extern bool QueryInformationJobObject(
|
|
90
|
+
IntPtr job,
|
|
91
|
+
int infoClass,
|
|
92
|
+
IntPtr info,
|
|
93
|
+
UInt32 length,
|
|
94
|
+
out UInt32 returnLength
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
[DllImport("kernel32.dll", SetLastError = true)]
|
|
98
|
+
public static extern IntPtr OpenProcess(UInt32 desiredAccess, bool inheritHandle, UInt32 processId);
|
|
99
|
+
|
|
100
|
+
[DllImport("kernel32.dll")]
|
|
101
|
+
[return: MarshalAs(UnmanagedType.Bool)]
|
|
102
|
+
public static extern bool CloseHandle(IntPtr handle);
|
|
103
|
+
|
|
104
|
+
public static string QuoteArgument(string value) {
|
|
105
|
+
if (value.Length > 0 && value.IndexOfAny(new[] { ' ', '\t', '\n', '\v', '"' }) < 0) {
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
var output = new StringBuilder();
|
|
109
|
+
output.Append('"');
|
|
110
|
+
var slashes = 0;
|
|
111
|
+
foreach (var ch in value) {
|
|
112
|
+
if (ch == '\\') {
|
|
113
|
+
slashes++;
|
|
114
|
+
} else if (ch == '"') {
|
|
115
|
+
output.Append('\\', slashes * 2 + 1);
|
|
116
|
+
output.Append('"');
|
|
117
|
+
slashes = 0;
|
|
118
|
+
} else {
|
|
119
|
+
output.Append('\\', slashes);
|
|
120
|
+
slashes = 0;
|
|
121
|
+
output.Append(ch);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
output.Append('\\', slashes * 2);
|
|
125
|
+
output.Append('"');
|
|
126
|
+
return output.ToString();
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
'@
|
|
130
|
+
|
|
131
|
+
function Throw-Win32Error([string]$Operation) {
|
|
132
|
+
$code = [Runtime.InteropServices.Marshal]::GetLastWin32Error()
|
|
133
|
+
throw "$Operation failed with Win32 error $code"
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
$job = [IntPtr]::Zero
|
|
137
|
+
$workerHandle = [IntPtr]::Zero
|
|
138
|
+
$worker = $null
|
|
139
|
+
$resultLine = $null
|
|
140
|
+
try {
|
|
141
|
+
$job = [YeaftBrowserJob]::CreateJobObject([IntPtr]::Zero, $null)
|
|
142
|
+
if ($job -eq [IntPtr]::Zero) { Throw-Win32Error 'CreateJobObject' }
|
|
143
|
+
|
|
144
|
+
$limit = New-Object YeaftBrowserJob+JOBOBJECT_EXTENDED_LIMIT_INFORMATION
|
|
145
|
+
$basicLimit = $limit.BasicLimitInformation
|
|
146
|
+
$basicLimit.LimitFlags = [YeaftBrowserJob]::JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE
|
|
147
|
+
$limit.BasicLimitInformation = $basicLimit
|
|
148
|
+
$limitSize = [Runtime.InteropServices.Marshal]::SizeOf($limit)
|
|
149
|
+
$limitPtr = [Runtime.InteropServices.Marshal]::AllocHGlobal($limitSize)
|
|
150
|
+
try {
|
|
151
|
+
[Runtime.InteropServices.Marshal]::StructureToPtr($limit, $limitPtr, $false)
|
|
152
|
+
if (-not [YeaftBrowserJob]::SetInformationJobObject(
|
|
153
|
+
$job,
|
|
154
|
+
[YeaftBrowserJob]::JobObjectExtendedLimitInformation,
|
|
155
|
+
$limitPtr,
|
|
156
|
+
[uint32]$limitSize
|
|
157
|
+
)) { Throw-Win32Error 'SetInformationJobObject' }
|
|
158
|
+
} finally {
|
|
159
|
+
[Runtime.InteropServices.Marshal]::FreeHGlobal($limitPtr)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
$start = New-Object System.Diagnostics.ProcessStartInfo
|
|
163
|
+
$start.FileName = $NodePath
|
|
164
|
+
$start.Arguments = [YeaftBrowserJob]::QuoteArgument($WorkerPath) + ' ' + [YeaftBrowserJob]::QuoteArgument($ExecutablePath)
|
|
165
|
+
$start.UseShellExecute = $false
|
|
166
|
+
$start.CreateNoWindow = $true
|
|
167
|
+
$start.RedirectStandardInput = $true
|
|
168
|
+
$start.RedirectStandardOutput = $true
|
|
169
|
+
$start.RedirectStandardError = $true
|
|
170
|
+
|
|
171
|
+
$worker = New-Object System.Diagnostics.Process
|
|
172
|
+
$worker.StartInfo = $start
|
|
173
|
+
if (-not $worker.Start()) { throw 'Windows version worker did not start' }
|
|
174
|
+
$workerHandle = [YeaftBrowserJob]::OpenProcess(
|
|
175
|
+
[YeaftBrowserJob]::PROCESS_SET_QUOTA -bor [YeaftBrowserJob]::PROCESS_TERMINATE,
|
|
176
|
+
$false,
|
|
177
|
+
[uint32]$worker.Id
|
|
178
|
+
)
|
|
179
|
+
if ($workerHandle -eq [IntPtr]::Zero) { Throw-Win32Error 'OpenProcess' }
|
|
180
|
+
if (-not [YeaftBrowserJob]::AssignProcessToJobObject($job, $workerHandle)) {
|
|
181
|
+
Throw-Win32Error 'AssignProcessToJobObject'
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
$worker.StandardInput.WriteLine('go')
|
|
185
|
+
$worker.StandardInput.Close()
|
|
186
|
+
$resultLine = $worker.StandardOutput.ReadLine()
|
|
187
|
+
if ([string]::IsNullOrWhiteSpace($resultLine)) {
|
|
188
|
+
if (-not $worker.HasExited) {
|
|
189
|
+
[void][YeaftBrowserJob]::TerminateJobObject($job, 1)
|
|
190
|
+
$worker.WaitForExit()
|
|
191
|
+
}
|
|
192
|
+
$workerError = $worker.StandardError.ReadToEnd()
|
|
193
|
+
throw "Windows version worker returned no result ($($worker.ExitCode)): $workerError"
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (-not [YeaftBrowserJob]::TerminateJobObject($job, 1)) {
|
|
197
|
+
Throw-Win32Error 'TerminateJobObject'
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
$accounting = New-Object YeaftBrowserJob+JOBOBJECT_BASIC_ACCOUNTING_INFORMATION
|
|
201
|
+
$accountingSize = [Runtime.InteropServices.Marshal]::SizeOf($accounting)
|
|
202
|
+
$accountingPtr = [Runtime.InteropServices.Marshal]::AllocHGlobal($accountingSize)
|
|
203
|
+
try {
|
|
204
|
+
$deadline = [DateTime]::UtcNow.AddMilliseconds([Math]::Max(1, $CleanupTimeoutMs))
|
|
205
|
+
do {
|
|
206
|
+
[uint32]$returned = 0
|
|
207
|
+
if (-not [YeaftBrowserJob]::QueryInformationJobObject(
|
|
208
|
+
$job,
|
|
209
|
+
[YeaftBrowserJob]::JobObjectBasicAccountingInformation,
|
|
210
|
+
$accountingPtr,
|
|
211
|
+
[uint32]$accountingSize,
|
|
212
|
+
[ref]$returned
|
|
213
|
+
)) { Throw-Win32Error 'QueryInformationJobObject' }
|
|
214
|
+
$accounting = [Runtime.InteropServices.Marshal]::PtrToStructure(
|
|
215
|
+
$accountingPtr,
|
|
216
|
+
[type][YeaftBrowserJob+JOBOBJECT_BASIC_ACCOUNTING_INFORMATION]
|
|
217
|
+
)
|
|
218
|
+
if ($accounting.ActiveProcesses -eq 0) { break }
|
|
219
|
+
Start-Sleep -Milliseconds 10
|
|
220
|
+
} while ([DateTime]::UtcNow -lt $deadline)
|
|
221
|
+
if ($accounting.ActiveProcesses -ne 0) {
|
|
222
|
+
throw "Windows Browser Runtime job still has $($accounting.ActiveProcesses) active process(es)"
|
|
223
|
+
}
|
|
224
|
+
} finally {
|
|
225
|
+
[Runtime.InteropServices.Marshal]::FreeHGlobal($accountingPtr)
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
[Console]::Out.WriteLine($resultLine)
|
|
229
|
+
} finally {
|
|
230
|
+
if ($workerHandle -ne [IntPtr]::Zero) { [void][YeaftBrowserJob]::CloseHandle($workerHandle) }
|
|
231
|
+
if ($job -ne [IntPtr]::Zero) { [void][YeaftBrowserJob]::CloseHandle($job) }
|
|
232
|
+
if ($null -ne $worker) { $worker.Dispose() }
|
|
233
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
const MAX_OUTPUT_BYTES = 64 * 1024;
|
|
4
|
+
|
|
5
|
+
function writeResult(result) {
|
|
6
|
+
process.stdout.write(`${JSON.stringify(result)}\n`, () => process.exit(0));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function capture(chunks, chunk, state) {
|
|
10
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
11
|
+
const remaining = MAX_OUTPUT_BYTES - state.bytes;
|
|
12
|
+
if (remaining <= 0) {
|
|
13
|
+
state.truncated = true;
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const bounded = buffer.length > remaining ? buffer.subarray(0, remaining) : buffer;
|
|
17
|
+
chunks.push(bounded);
|
|
18
|
+
state.bytes += bounded.length;
|
|
19
|
+
if (bounded.length !== buffer.length) state.truncated = true;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function run(executablePath) {
|
|
23
|
+
return new Promise(resolve => {
|
|
24
|
+
let child;
|
|
25
|
+
try {
|
|
26
|
+
child = spawn(executablePath, ['--version'], {
|
|
27
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
28
|
+
windowsHide: true,
|
|
29
|
+
});
|
|
30
|
+
} catch (error) {
|
|
31
|
+
resolve({ ok: false, error: String(error?.message || error) });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const stdout = [];
|
|
35
|
+
const stderr = [];
|
|
36
|
+
const stdoutState = { bytes: 0, truncated: false };
|
|
37
|
+
const stderrState = { bytes: 0, truncated: false };
|
|
38
|
+
const stopOnOverflow = state => {
|
|
39
|
+
if (!state.truncated) return;
|
|
40
|
+
try { child.kill('SIGKILL'); } catch {}
|
|
41
|
+
};
|
|
42
|
+
child.stdout.on('data', chunk => {
|
|
43
|
+
capture(stdout, chunk, stdoutState);
|
|
44
|
+
stopOnOverflow(stdoutState);
|
|
45
|
+
});
|
|
46
|
+
child.stderr.on('data', chunk => {
|
|
47
|
+
capture(stderr, chunk, stderrState);
|
|
48
|
+
stopOnOverflow(stderrState);
|
|
49
|
+
});
|
|
50
|
+
let settled = false;
|
|
51
|
+
const finish = result => {
|
|
52
|
+
if (settled) return;
|
|
53
|
+
settled = true;
|
|
54
|
+
resolve(result);
|
|
55
|
+
};
|
|
56
|
+
child.once('error', error => finish({ ok: false, error: String(error?.message || error) }));
|
|
57
|
+
child.once('close', code => finish({
|
|
58
|
+
ok: true,
|
|
59
|
+
code: code ?? 1,
|
|
60
|
+
stdout: Buffer.concat(stdout).toString('utf8').replace(/\r/g, ''),
|
|
61
|
+
stderr: Buffer.concat(stderr).toString('utf8').replace(/\r/g, ''),
|
|
62
|
+
truncated: stdoutState.truncated || stderrState.truncated,
|
|
63
|
+
}));
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let started = false;
|
|
68
|
+
process.stdin.setEncoding('utf8');
|
|
69
|
+
process.stdin.on('data', async chunk => {
|
|
70
|
+
if (started || !String(chunk).split(/\r?\n/).includes('go')) return;
|
|
71
|
+
started = true;
|
|
72
|
+
const result = await run(process.argv[2]);
|
|
73
|
+
writeResult(result);
|
|
74
|
+
});
|
|
75
|
+
process.stdin.resume();
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { dirname, join } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { runProcess } from '../yeaft/tools/process-runner.js';
|
|
5
|
+
|
|
6
|
+
const MODULE_DIR = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const WINDOWS_JOB_SCRIPT = join(MODULE_DIR, 'windows-version-job.ps1');
|
|
8
|
+
const WINDOWS_WORKER = join(MODULE_DIR, 'windows-version-worker.js');
|
|
9
|
+
const DEFAULT_WINDOWS_CLEANUP_MS = 500;
|
|
10
|
+
|
|
11
|
+
export function resolveWindowsPowerShell({ env = process.env, fileExists = existsSync } = {}) {
|
|
12
|
+
const systemRoot = env.SystemRoot || env.WINDIR || 'C:\\Windows';
|
|
13
|
+
const candidates = [
|
|
14
|
+
join(systemRoot, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'),
|
|
15
|
+
'powershell.exe',
|
|
16
|
+
];
|
|
17
|
+
return candidates.find(candidate => candidate === 'powershell.exe' || fileExists(candidate));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function remaining(deadline, fallback) {
|
|
21
|
+
if (!Number.isFinite(deadline)) return fallback;
|
|
22
|
+
return Math.max(1, deadline - Date.now());
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function readWindowsBrowserExecutableVersion(executablePath, {
|
|
26
|
+
signal = null,
|
|
27
|
+
terminationDeadline = null,
|
|
28
|
+
run = runProcess,
|
|
29
|
+
powershellPath = resolveWindowsPowerShell(),
|
|
30
|
+
nodePath = process.execPath,
|
|
31
|
+
workerPath = WINDOWS_WORKER,
|
|
32
|
+
jobScriptPath = WINDOWS_JOB_SCRIPT,
|
|
33
|
+
env = process.env,
|
|
34
|
+
} = {}) {
|
|
35
|
+
const cleanupMs = Number.isFinite(terminationDeadline)
|
|
36
|
+
? Math.min(DEFAULT_WINDOWS_CLEANUP_MS, remaining(terminationDeadline, DEFAULT_WINDOWS_CLEANUP_MS))
|
|
37
|
+
: DEFAULT_WINDOWS_CLEANUP_MS;
|
|
38
|
+
const timeoutMs = remaining(terminationDeadline, 20_000);
|
|
39
|
+
const result = await run(powershellPath, [
|
|
40
|
+
'-NoLogo',
|
|
41
|
+
'-NoProfile',
|
|
42
|
+
'-NonInteractive',
|
|
43
|
+
'-ExecutionPolicy',
|
|
44
|
+
'Bypass',
|
|
45
|
+
'-File',
|
|
46
|
+
jobScriptPath,
|
|
47
|
+
'-NodePath',
|
|
48
|
+
nodePath,
|
|
49
|
+
'-WorkerPath',
|
|
50
|
+
workerPath,
|
|
51
|
+
'-ExecutablePath',
|
|
52
|
+
executablePath,
|
|
53
|
+
'-CleanupTimeoutMs',
|
|
54
|
+
String(cleanupMs),
|
|
55
|
+
], {
|
|
56
|
+
signal,
|
|
57
|
+
timeoutMs,
|
|
58
|
+
maxBytes: 128 * 1024,
|
|
59
|
+
env,
|
|
60
|
+
killGraceMs: 0,
|
|
61
|
+
gracefulTerminationDeadline: terminationDeadline,
|
|
62
|
+
terminationDeadline,
|
|
63
|
+
forceSettleMs: cleanupMs,
|
|
64
|
+
treeKillTimeoutMs: cleanupMs,
|
|
65
|
+
requireExitConfirmation: true,
|
|
66
|
+
});
|
|
67
|
+
if (result.code !== 0) {
|
|
68
|
+
const termination = result.terminationError ? ` ${result.terminationError}` : '';
|
|
69
|
+
throw new Error(`Managed Chrome version job failed (${result.code}): ${result.stderr.slice(0, 500)}${termination}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const line = result.stdout.trim().split(/\r?\n/).at(-1);
|
|
73
|
+
let payload;
|
|
74
|
+
try {
|
|
75
|
+
payload = JSON.parse(line);
|
|
76
|
+
} catch {
|
|
77
|
+
throw new Error(`Managed Chrome version job returned invalid output: ${result.stdout.slice(0, 500)}`);
|
|
78
|
+
}
|
|
79
|
+
if (!payload?.ok) throw new Error(`Managed Chrome version check failed: ${payload?.error || 'unknown worker error'}`);
|
|
80
|
+
if (payload.code !== 0) {
|
|
81
|
+
throw new Error(`Managed Chrome version check failed (${payload.code}): ${String(payload.stderr || '').slice(0, 200)}`);
|
|
82
|
+
}
|
|
83
|
+
if (payload.truncated) throw new Error('Managed Chrome version output exceeded its bounded capture');
|
|
84
|
+
return String(payload.stdout || '').trim();
|
|
85
|
+
}
|