farming-code 2.2.8 → 2.2.11
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 +36 -23
- package/README.zh_cn.md +36 -23
- package/THIRD_PARTY_NOTICES.md +11 -2
- package/backend/acp-checkpoint-store.js +198 -0
- package/backend/acp-runtime.js +306 -83
- package/backend/acp-session-state.js +202 -6
- package/backend/acp-transcript.js +112 -0
- package/backend/agent-activity.js +6 -157
- package/backend/agent-manager.js +1482 -730
- package/backend/agent-provider-session.js +17 -242
- package/backend/agent-runtime-binding.js +219 -0
- package/backend/agent-session-history.js +66 -1
- package/backend/auth.js +79 -6
- package/backend/codex-models.js +81 -84
- package/backend/codex-session-archive.js +45 -0
- package/backend/codex-terminal-profile.js +500 -0
- package/backend/codex-transcript-sanitizer.js +12 -0
- package/backend/codex-transcript.js +230 -8
- package/backend/config-manager.js +30 -0
- package/backend/control-api.js +192 -17
- package/backend/farming-net-pass.js +285 -0
- package/backend/farming-net-registry.js +112 -0
- package/backend/farming-net-server.js +298 -0
- package/backend/farming-session-store.js +5 -13
- package/backend/git-worktree-info.js +181 -0
- package/backend/local-session-engine.js +411 -186
- package/backend/main-page-session.js +5 -2
- package/backend/native-pty-controller-generation.js +106 -0
- package/backend/native-pty-host-client.js +275 -7
- package/backend/native-pty-host-identity.js +86 -0
- package/backend/native-pty-host.js +813 -114
- package/backend/native-session-engine.js +100 -28
- package/backend/packaged-node-pty.js +22 -2
- package/backend/provider-adapters.js +253 -0
- package/backend/provider-session-service.js +241 -0
- package/backend/runtime-observation.js +81 -0
- package/backend/server.js +294 -86
- package/backend/session-engine-bridge.js +21 -2
- package/backend/session-engine-router.js +1 -1
- package/backend/session-engine.js +1 -1
- package/backend/session-stream-protocol.js +185 -0
- package/backend/storage-layout.js +55 -0
- package/backend/terminal-attach-checkpoint.js +74 -0
- package/backend/terminal-exit-quiescence.js +39 -0
- package/backend/terminal-reducer-flow-control.js +97 -0
- package/backend/terminal-screen-state.js +11 -2
- package/backend/terminal-screen-worker-pool.js +59 -6
- package/backend/terminal-screen-worker-thread.js +97 -57
- package/backend/terminal-screen-worker.js +133 -51
- package/backend/terminal-state-serialization.js +127 -0
- package/backend/terminal-status.js +23 -4
- package/backend/usage-monitor.js +81 -4
- package/backend/workspace-directory.js +152 -0
- package/backend/workspace-file-router.js +182 -76
- package/backend/workspace-file-service.js +319 -4
- package/backend/workspace-root-registry.js +164 -0
- package/dist/assets/App-CHYpgECZ.js +208 -0
- package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview--Cri80nQ.js} +92 -92
- package/dist/assets/FileEditorPane-DGJlEKdf.js +2 -0
- package/dist/assets/IconGlyphs-Cc14sepw.js +1 -0
- package/dist/assets/ProjectFilesSection-Cz1wG4NT.js +12 -0
- package/dist/assets/{ReviewPage-BaXu1ZdX.js → ReviewPage-LonRc6Fz.js} +1 -1
- package/dist/assets/code-dark-B1pAtO_P.css +1 -0
- package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
- package/dist/assets/{index-BrbljRqn.js → index-miO2r9IZ.js} +3 -3
- package/dist/assets/main-DNe0jPw-.css +1 -0
- package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
- package/dist/assets/workspace-editor-monaco-5gFbeDPF.js +1 -0
- package/dist/assets/workspace-editor-monaco-xPv8JzHH.js +4 -0
- package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
- package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
- package/dist/index.html +1 -1
- package/frontend/farming-net/app.css +625 -0
- package/frontend/farming-net/app.js +268 -0
- package/frontend/farming-net/index.html +86 -0
- package/frontend/reading-anchor.js +198 -0
- package/frontend/session-bridge.js +12 -3
- package/frontend/session-modal-bridge.js +5 -12
- package/frontend/skins/crt/app.js +1976 -793
- package/frontend/skins/crt/index.html +313 -23
- package/frontend/skins/crt/styles/billing.css +294 -223
- package/frontend/skins/crt/styles/monochrome-green.css +7 -2
- package/frontend/terminal-replay.js +372 -0
- package/package.json +10 -3
- package/shared/browser-protocol.d.ts +5 -0
- package/shared/browser-protocol.js +130 -0
- package/dist/assets/App-8dYAM6ql.js +0 -124
- package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
- package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
- package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
- package/dist/assets/code-dark-CDkOQAtK.css +0 -1
- package/dist/assets/file-icons-EFUGSSwf.js +0 -1
- package/dist/assets/main-D073SnW4.css +0 -1
- package/dist/assets/qoder-C9LmmOSf.svg +0 -1
- package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
- package/dist/assets/qoder-gHCinseV.svg +0 -1
- package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
- package/dist/assets/zsh-CLpveKlF.svg +0 -1
- package/dist/assets/zsh-FxSpMPbz.svg +0 -1
- /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
- /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
- /package/dist/assets/{useWorkspaceMenuKeyboard-CneKAZUJ.js → useWorkspaceMenuKeyboard-Brws6Ar9.js} +0 -0
|
@@ -14,9 +14,12 @@ function isSafeSessionId(sessionId) {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
function mainPageAgentSessionKey(provider, sessionId, providerHomeId = '') {
|
|
17
|
+
const normalizedProvider = normalizeMainPageSessionProvider(provider);
|
|
18
|
+
const normalizedSessionId = String(sessionId || '').trim();
|
|
19
|
+
if (!normalizedProvider || !normalizedSessionId) return '';
|
|
17
20
|
const homeId = String(providerHomeId || '').trim();
|
|
18
|
-
if (homeId && homeId !== 'default') return `agent-session:${
|
|
19
|
-
return `agent-session:${
|
|
21
|
+
if (homeId && homeId !== 'default') return `agent-session:${normalizedProvider}:home:${homeId}:${normalizedSessionId}`;
|
|
22
|
+
return `agent-session:${normalizedProvider}:${normalizedSessionId}`;
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
function mainPageAgentSessionFromKey(key) {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const storageLayout = require('./storage-layout');
|
|
5
|
+
|
|
6
|
+
const LOCK_RETRY_MS = 10;
|
|
7
|
+
const LOCK_TIMEOUT_MS = 5000;
|
|
8
|
+
const STALE_LOCK_MS = 30000;
|
|
9
|
+
|
|
10
|
+
function delay(ms) {
|
|
11
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function positiveGeneration(value) {
|
|
15
|
+
const parsed = Math.floor(Number(value));
|
|
16
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function acquireGenerationLock(lockDir) {
|
|
20
|
+
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
21
|
+
while (Date.now() < deadline) {
|
|
22
|
+
try {
|
|
23
|
+
await fs.promises.mkdir(lockDir);
|
|
24
|
+
return;
|
|
25
|
+
} catch (error) {
|
|
26
|
+
if (!error || error.code !== 'EEXIST') throw error;
|
|
27
|
+
try {
|
|
28
|
+
const stat = await fs.promises.stat(lockDir);
|
|
29
|
+
if (Date.now() - stat.mtimeMs > STALE_LOCK_MS) {
|
|
30
|
+
await fs.promises.rm(lockDir, { recursive: true, force: true });
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
} catch (statError) {
|
|
34
|
+
if (!statError || statError.code !== 'ENOENT') throw statError;
|
|
35
|
+
}
|
|
36
|
+
await delay(LOCK_RETRY_MS);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
throw new Error('Timed out allocating native PTY controller generation');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function allocateGeneration(generationFile, lockDir) {
|
|
43
|
+
const root = path.dirname(generationFile);
|
|
44
|
+
await fs.promises.mkdir(root, { recursive: true });
|
|
45
|
+
await acquireGenerationLock(lockDir);
|
|
46
|
+
try {
|
|
47
|
+
let current = 0;
|
|
48
|
+
try {
|
|
49
|
+
current = positiveGeneration(await fs.promises.readFile(generationFile, 'utf8'));
|
|
50
|
+
} catch (error) {
|
|
51
|
+
if (!error || error.code !== 'ENOENT') throw error;
|
|
52
|
+
}
|
|
53
|
+
const generation = current + 1;
|
|
54
|
+
const temporary = `${generationFile}.${process.pid}.${crypto.randomUUID()}.tmp`;
|
|
55
|
+
await fs.promises.writeFile(temporary, `${generation}\n`, { mode: 0o600 });
|
|
56
|
+
await fs.promises.rename(temporary, generationFile);
|
|
57
|
+
return generation;
|
|
58
|
+
} finally {
|
|
59
|
+
await fs.promises.rm(lockDir, { recursive: true, force: true });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function allocateNativePtyControllerGeneration(configDir) {
|
|
64
|
+
const root = path.resolve(configDir);
|
|
65
|
+
const generationFile = storageLayout.nativePtyControllerGenerationFile(root);
|
|
66
|
+
const lockDir = storageLayout.nativePtyControllerGenerationLockDir(root);
|
|
67
|
+
return allocateGeneration(generationFile, lockDir);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function allocateNativePtyRuntimeGeneration(configDir) {
|
|
71
|
+
const root = path.resolve(configDir);
|
|
72
|
+
return allocateGeneration(
|
|
73
|
+
storageLayout.nativePtyRuntimeGenerationFile(root),
|
|
74
|
+
storageLayout.nativePtyRuntimeGenerationLockDir(root),
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function formatNativePtyRuntimeEpoch(generation, id = crypto.randomUUID()) {
|
|
79
|
+
const normalized = positiveGeneration(generation);
|
|
80
|
+
if (!normalized) throw new Error('Native PTY runtime generation must be positive');
|
|
81
|
+
return `farming-runtime-v1:${String(normalized).padStart(20, '0')}:${id}`;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function nativePtyRuntimeEpochGeneration(runtimeEpoch) {
|
|
85
|
+
const match = /^farming-runtime-v1:(\d{20}):/.exec(String(runtimeEpoch || ''));
|
|
86
|
+
if (!match) return null;
|
|
87
|
+
const generation = Number(match[1]);
|
|
88
|
+
return Number.isSafeInteger(generation) && generation > 0 ? generation : null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function compareNativePtyRuntimeEpochs(left, right) {
|
|
92
|
+
if (left === right) return 0;
|
|
93
|
+
const leftGeneration = nativePtyRuntimeEpochGeneration(left);
|
|
94
|
+
const rightGeneration = nativePtyRuntimeEpochGeneration(right);
|
|
95
|
+
if (leftGeneration === null || rightGeneration === null || leftGeneration === rightGeneration) return null;
|
|
96
|
+
return leftGeneration < rightGeneration ? -1 : 1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
module.exports = {
|
|
100
|
+
allocateNativePtyControllerGeneration,
|
|
101
|
+
allocateNativePtyRuntimeGeneration,
|
|
102
|
+
compareNativePtyRuntimeEpochs,
|
|
103
|
+
formatNativePtyRuntimeEpoch,
|
|
104
|
+
nativePtyRuntimeEpochGeneration,
|
|
105
|
+
positiveGeneration,
|
|
106
|
+
};
|
|
@@ -1,24 +1,38 @@
|
|
|
1
1
|
const EventEmitter = require('events');
|
|
2
2
|
const { spawn } = require('child_process');
|
|
3
|
+
const crypto = require('crypto');
|
|
3
4
|
const fs = require('fs');
|
|
4
5
|
const net = require('net');
|
|
5
6
|
const path = require('path');
|
|
6
7
|
const { nativePtyHostSocketPath } = require('./native-pty-host-path');
|
|
8
|
+
const {
|
|
9
|
+
nativePtyHostRuntimeIdentity,
|
|
10
|
+
nativePtyHostRuntimeIdentityMatches,
|
|
11
|
+
normalizeNativePtyHostRuntimeIdentity,
|
|
12
|
+
} = require('./native-pty-host-identity');
|
|
13
|
+
const {
|
|
14
|
+
allocateNativePtyControllerGeneration,
|
|
15
|
+
positiveGeneration,
|
|
16
|
+
} = require('./native-pty-controller-generation');
|
|
7
17
|
const storageLayout = require('./storage-layout');
|
|
8
18
|
|
|
9
19
|
const DEFAULT_CONNECT_RETRIES = 300;
|
|
10
20
|
const DEFAULT_CONNECT_RETRY_MS = 50;
|
|
11
21
|
const DEFAULT_REQUEST_TIMEOUT_MS = 30000;
|
|
22
|
+
const DEFAULT_HOST_ROTATION_TIMEOUT_MS = 10000;
|
|
12
23
|
const PACKAGED_NATIVE_PTY_HOST_ENV = 'FARMING_RUN_NATIVE_PTY_HOST';
|
|
13
24
|
const RECONNECT_RETRYABLE_METHODS = new Set([
|
|
14
25
|
'ping',
|
|
15
26
|
'createSession',
|
|
27
|
+
'claimSessionController',
|
|
28
|
+
'renewSessionController',
|
|
16
29
|
'resizeSession',
|
|
17
|
-
'clearBuffer',
|
|
18
30
|
'killSession',
|
|
31
|
+
'getSessionAttachCheckpoint',
|
|
19
32
|
'getSessionState',
|
|
20
33
|
'getSessionPreview',
|
|
21
34
|
'recoverSessions',
|
|
35
|
+
'serializeTerminalState',
|
|
22
36
|
'updateSessionMetadata',
|
|
23
37
|
]);
|
|
24
38
|
const PACKAGED_NATIVE_HOST_ENV_KEYS = new Set([
|
|
@@ -111,6 +125,13 @@ function hostConnectErrorMessage(error, spawned, logPath) {
|
|
|
111
125
|
return `Native PTY host is not reachable${code}.${logHint}`;
|
|
112
126
|
}
|
|
113
127
|
|
|
128
|
+
function runtimeIdentityLabel(value) {
|
|
129
|
+
const identity = normalizeNativePtyHostRuntimeIdentity(value);
|
|
130
|
+
if (!identity) return 'legacy/unknown';
|
|
131
|
+
const version = identity.version ? `v${identity.version} ` : '';
|
|
132
|
+
return `${version}protocol ${identity.protocolVersion} build ${identity.buildId.slice(0, 12)}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
114
135
|
function nativeHostSpawnCommand(hostScript, env) {
|
|
115
136
|
const nodeBin = env.FARMING_NODE_BIN || process.execPath;
|
|
116
137
|
const ldPath = env.FARMING_NODE_LD || '';
|
|
@@ -148,7 +169,22 @@ class NativePtyHostClient extends EventEmitter {
|
|
|
148
169
|
this.connectRetryMs = options.connectRetryMs || DEFAULT_CONNECT_RETRY_MS;
|
|
149
170
|
this.requestTimeoutMs = options.requestTimeoutMs || DEFAULT_REQUEST_TIMEOUT_MS;
|
|
150
171
|
this.hostLogPath = options.hostLogPath || this.defaultHostLogPath();
|
|
172
|
+
this.expectedRuntimeIdentity = options.expectedRuntimeIdentity || nativePtyHostRuntimeIdentity();
|
|
173
|
+
this.controllerIdentity = options.controllerIdentity
|
|
174
|
+
? {
|
|
175
|
+
id: String(options.controllerIdentity.id || ''),
|
|
176
|
+
generation: positiveGeneration(options.controllerIdentity.generation),
|
|
177
|
+
}
|
|
178
|
+
: {
|
|
179
|
+
id: crypto.randomUUID(),
|
|
180
|
+
generation: 0,
|
|
181
|
+
};
|
|
182
|
+
this.controllerIdentityReady = null;
|
|
183
|
+
this.hostRotationTimeoutMs = options.hostRotationTimeoutMs || DEFAULT_HOST_ROTATION_TIMEOUT_MS;
|
|
184
|
+
this.connectedHostInfo = null;
|
|
185
|
+
this.runtimeRotationInfo = null;
|
|
151
186
|
this.socket = null;
|
|
187
|
+
this.socketGeneration = 0;
|
|
152
188
|
this.buffer = '';
|
|
153
189
|
this.nextRequestId = 1;
|
|
154
190
|
this.pending = new Map();
|
|
@@ -281,8 +317,11 @@ class NativePtyHostClient extends EventEmitter {
|
|
|
281
317
|
}
|
|
282
318
|
|
|
283
319
|
async connectWithRetries(options = {}) {
|
|
320
|
+
await this.ensureControllerIdentity();
|
|
284
321
|
const allowHostStart = options.startHost !== false;
|
|
285
322
|
let spawned = false;
|
|
323
|
+
let rotatedMismatchedHost = false;
|
|
324
|
+
let pendingRotationInfo = null;
|
|
286
325
|
let lastError = null;
|
|
287
326
|
|
|
288
327
|
for (let attempt = 0; attempt < this.connectRetries; attempt += 1) {
|
|
@@ -291,14 +330,51 @@ class NativePtyHostClient extends EventEmitter {
|
|
|
291
330
|
}
|
|
292
331
|
try {
|
|
293
332
|
await this.connectOnce();
|
|
294
|
-
await this.request('ping', {}, { ensureConnected: false, timeoutMs: 3000 });
|
|
333
|
+
const hostInfo = await this.request('ping', {}, { ensureConnected: false, timeoutMs: 3000 });
|
|
334
|
+
if (!nativePtyHostRuntimeIdentityMatches(this.expectedRuntimeIdentity, hostInfo?.runtimeIdentity)) {
|
|
335
|
+
if (rotatedMismatchedHost) {
|
|
336
|
+
const error = new Error(
|
|
337
|
+
`Native PTY host runtime still mismatches after rotation: expected ` +
|
|
338
|
+
`${runtimeIdentityLabel(this.expectedRuntimeIdentity)}, got ${runtimeIdentityLabel(hostInfo?.runtimeIdentity)}`
|
|
339
|
+
);
|
|
340
|
+
error.code = 'FARMING_NATIVE_HOST_RUNTIME_MISMATCH';
|
|
341
|
+
throw error;
|
|
342
|
+
}
|
|
343
|
+
const serializedTerminalState = await this.rotateMismatchedHost(hostInfo);
|
|
344
|
+
pendingRotationInfo = {
|
|
345
|
+
rotatedAt: Date.now(),
|
|
346
|
+
previous: normalizeNativePtyHostRuntimeIdentity(hostInfo?.runtimeIdentity),
|
|
347
|
+
current: normalizeNativePtyHostRuntimeIdentity(this.expectedRuntimeIdentity),
|
|
348
|
+
previousPid: Number(hostInfo?.pid) || null,
|
|
349
|
+
serializedTerminalState: typeof serializedTerminalState === 'string'
|
|
350
|
+
? serializedTerminalState
|
|
351
|
+
: '',
|
|
352
|
+
};
|
|
353
|
+
rotatedMismatchedHost = true;
|
|
354
|
+
spawned = true;
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
await this.requestOnce('registerController', {
|
|
358
|
+
identity: this.controllerIdentity,
|
|
359
|
+
}, {
|
|
360
|
+
ensureConnected: false,
|
|
361
|
+
retryOnDisconnect: false,
|
|
362
|
+
timeoutMs: 3000,
|
|
363
|
+
});
|
|
364
|
+
this.connectedHostInfo = hostInfo || null;
|
|
365
|
+
if (pendingRotationInfo) {
|
|
366
|
+
this.runtimeRotationInfo = pendingRotationInfo;
|
|
367
|
+
}
|
|
295
368
|
return;
|
|
296
369
|
} catch (error) {
|
|
297
370
|
lastError = error;
|
|
371
|
+
if (error && error.code === 'FARMING_NATIVE_HOST_RUNTIME_MISMATCH') {
|
|
372
|
+
throw error;
|
|
373
|
+
}
|
|
298
374
|
if (this.hostStartError) {
|
|
299
375
|
lastError = this.hostStartError;
|
|
300
376
|
}
|
|
301
|
-
if (!allowHostStart && isConnectRetryable(error)) {
|
|
377
|
+
if (!allowHostStart && !rotatedMismatchedHost && isConnectRetryable(error)) {
|
|
302
378
|
throw error;
|
|
303
379
|
}
|
|
304
380
|
if (allowHostStart && !spawned && isConnectRetryable(error)) {
|
|
@@ -330,26 +406,217 @@ class NativePtyHostClient extends EventEmitter {
|
|
|
330
406
|
throw lastError || new Error('Failed to connect to native pty host');
|
|
331
407
|
}
|
|
332
408
|
|
|
409
|
+
async ensureControllerIdentity() {
|
|
410
|
+
if (this.controllerIdentity.generation > 0) {
|
|
411
|
+
return this.controllerIdentity;
|
|
412
|
+
}
|
|
413
|
+
if (!this.controllerIdentityReady) {
|
|
414
|
+
const configRoot = this.configDir || path.dirname(this.socketPath);
|
|
415
|
+
this.controllerIdentityReady = allocateNativePtyControllerGeneration(configRoot)
|
|
416
|
+
.then((generation) => {
|
|
417
|
+
this.controllerIdentity.generation = generation;
|
|
418
|
+
return this.controllerIdentity;
|
|
419
|
+
})
|
|
420
|
+
.catch((error) => {
|
|
421
|
+
this.controllerIdentityReady = null;
|
|
422
|
+
throw error;
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
return this.controllerIdentityReady;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
async waitForHostRelease() {
|
|
429
|
+
const deadline = Date.now() + this.hostRotationTimeoutMs;
|
|
430
|
+
if (process.platform === 'win32') {
|
|
431
|
+
await delay(Math.min(250, this.hostRotationTimeoutMs));
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
while (Date.now() < deadline) {
|
|
435
|
+
const childReleased = !this.hostChild ||
|
|
436
|
+
this.hostChild.exitCode !== null ||
|
|
437
|
+
this.hostChild.signalCode !== null;
|
|
438
|
+
if (!fs.existsSync(this.socketPath) && childReleased) return;
|
|
439
|
+
await delay(50);
|
|
440
|
+
}
|
|
441
|
+
const error = new Error('Timed out waiting for the previous native PTY host to stop');
|
|
442
|
+
error.code = 'FARMING_NATIVE_HOST_RUNTIME_MISMATCH';
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async resumePreparedHostRotation(preparationToken = '') {
|
|
447
|
+
if (!this.socket || this.socket.destroyed) {
|
|
448
|
+
await this.connectOnce();
|
|
449
|
+
await this.requestOnce('registerController', {
|
|
450
|
+
identity: this.controllerIdentity,
|
|
451
|
+
}, {
|
|
452
|
+
ensureConnected: false,
|
|
453
|
+
retryOnDisconnect: false,
|
|
454
|
+
timeoutMs: 1000,
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
return this.requestOnce('resumeTerminalState', {
|
|
458
|
+
preparationToken,
|
|
459
|
+
}, {
|
|
460
|
+
ensureConnected: false,
|
|
461
|
+
retryOnDisconnect: false,
|
|
462
|
+
timeoutMs: 1000,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
async rotateMismatchedHost(hostInfo) {
|
|
467
|
+
const expected = runtimeIdentityLabel(this.expectedRuntimeIdentity);
|
|
468
|
+
const actual = runtimeIdentityLabel(hostInfo && hostInfo.runtimeIdentity);
|
|
469
|
+
console.warn(`Rotating native PTY host runtime: expected ${expected}, connected to ${actual}`);
|
|
470
|
+
this.emit('host-runtime-mismatch', {
|
|
471
|
+
expected: this.expectedRuntimeIdentity,
|
|
472
|
+
actual: hostInfo && hostInfo.runtimeIdentity || null,
|
|
473
|
+
pid: Number(hostInfo && hostInfo.pid) || null,
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
await this.requestOnce('registerController', {
|
|
477
|
+
identity: this.controllerIdentity,
|
|
478
|
+
}, {
|
|
479
|
+
ensureConnected: false,
|
|
480
|
+
retryOnDisconnect: false,
|
|
481
|
+
timeoutMs: 3000,
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
let serializedTerminalState = '';
|
|
485
|
+
let preparationToken = '';
|
|
486
|
+
try {
|
|
487
|
+
const preparation = await this.requestOnce('serializeTerminalState', {}, {
|
|
488
|
+
ensureConnected: false,
|
|
489
|
+
retryOnDisconnect: false,
|
|
490
|
+
timeoutMs: Math.min(5000, this.hostRotationTimeoutMs),
|
|
491
|
+
});
|
|
492
|
+
if (
|
|
493
|
+
preparation &&
|
|
494
|
+
typeof preparation === 'object' &&
|
|
495
|
+
typeof preparation.preparationToken === 'string' &&
|
|
496
|
+
preparation.preparationToken &&
|
|
497
|
+
typeof preparation.serializedTerminalState === 'string'
|
|
498
|
+
) {
|
|
499
|
+
preparationToken = preparation.preparationToken;
|
|
500
|
+
serializedTerminalState = preparation.serializedTerminalState;
|
|
501
|
+
} else if (typeof preparation === 'string') {
|
|
502
|
+
const recovered = await this.requestOnce('recoverSessions', {}, {
|
|
503
|
+
ensureConnected: false,
|
|
504
|
+
retryOnDisconnect: false,
|
|
505
|
+
timeoutMs: Math.min(3000, this.hostRotationTimeoutMs),
|
|
506
|
+
});
|
|
507
|
+
if (Array.isArray(recovered) && recovered.length === 0) {
|
|
508
|
+
serializedTerminalState = preparation;
|
|
509
|
+
} else {
|
|
510
|
+
throw new Error('The old native PTY host cannot commit a transactional terminal checkpoint');
|
|
511
|
+
}
|
|
512
|
+
} else {
|
|
513
|
+
throw new Error('The native PTY host returned an invalid rotation checkpoint');
|
|
514
|
+
}
|
|
515
|
+
} catch (error) {
|
|
516
|
+
await this.resumePreparedHostRotation(preparationToken).catch(() => {});
|
|
517
|
+
const recovered = await this.requestOnce('recoverSessions', {}, {
|
|
518
|
+
ensureConnected: false,
|
|
519
|
+
retryOnDisconnect: false,
|
|
520
|
+
timeoutMs: Math.min(3000, this.hostRotationTimeoutMs),
|
|
521
|
+
}).catch(() => null);
|
|
522
|
+
if (Array.isArray(recovered) && recovered.length === 0) {
|
|
523
|
+
serializedTerminalState = '';
|
|
524
|
+
preparationToken = '';
|
|
525
|
+
} else {
|
|
526
|
+
const mismatchError = new Error(
|
|
527
|
+
`Cannot rotate incompatible native PTY host (${actual}) without a committed terminal checkpoint`
|
|
528
|
+
);
|
|
529
|
+
mismatchError.code = 'FARMING_NATIVE_HOST_RUNTIME_MISMATCH';
|
|
530
|
+
mismatchError.cause = error;
|
|
531
|
+
throw mismatchError;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
const socket = this.socket;
|
|
536
|
+
if (socket) this.suppressedDisconnectSockets.add(socket);
|
|
537
|
+
let shutdownUncertain = false;
|
|
538
|
+
try {
|
|
539
|
+
await this.requestOnce('shutdownHost', {
|
|
540
|
+
controller: this.controllerIdentity,
|
|
541
|
+
preparationToken,
|
|
542
|
+
}, {
|
|
543
|
+
ensureConnected: false,
|
|
544
|
+
retryOnDisconnect: false,
|
|
545
|
+
timeoutMs: Math.min(5000, this.hostRotationTimeoutMs),
|
|
546
|
+
});
|
|
547
|
+
} catch (error) {
|
|
548
|
+
if (!isConnectRetryable(error)) {
|
|
549
|
+
await this.resumePreparedHostRotation(preparationToken).catch(() => {});
|
|
550
|
+
const mismatchError = new Error(
|
|
551
|
+
`Cannot rotate incompatible native PTY host (${actual}); stop the old host and restart Farming`
|
|
552
|
+
);
|
|
553
|
+
mismatchError.code = 'FARMING_NATIVE_HOST_RUNTIME_MISMATCH';
|
|
554
|
+
mismatchError.cause = error;
|
|
555
|
+
throw mismatchError;
|
|
556
|
+
}
|
|
557
|
+
shutdownUncertain = true;
|
|
558
|
+
} finally {
|
|
559
|
+
if (this.socket === socket) {
|
|
560
|
+
this.socket = null;
|
|
561
|
+
this.buffer = '';
|
|
562
|
+
}
|
|
563
|
+
if (socket && !socket.destroyed) socket.destroy();
|
|
564
|
+
this.connectedHostInfo = null;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
try {
|
|
568
|
+
await this.waitForHostRelease();
|
|
569
|
+
} catch (error) {
|
|
570
|
+
if (shutdownUncertain) {
|
|
571
|
+
try {
|
|
572
|
+
await this.resumePreparedHostRotation(preparationToken);
|
|
573
|
+
} catch {
|
|
574
|
+
// The old host may have exited after the release timeout. Do not
|
|
575
|
+
// start a second host until the socket and child are actually gone.
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
const mismatchError = new Error(
|
|
579
|
+
`Cannot confirm shutdown of incompatible native PTY host (${actual}); restart Farming after the old host exits`
|
|
580
|
+
);
|
|
581
|
+
mismatchError.code = 'FARMING_NATIVE_HOST_RUNTIME_MISMATCH';
|
|
582
|
+
mismatchError.cause = error;
|
|
583
|
+
throw mismatchError;
|
|
584
|
+
}
|
|
585
|
+
this.spawnHost();
|
|
586
|
+
return serializedTerminalState;
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
consumeRuntimeRotation() {
|
|
590
|
+
const rotation = this.runtimeRotationInfo;
|
|
591
|
+
this.runtimeRotationInfo = null;
|
|
592
|
+
return rotation;
|
|
593
|
+
}
|
|
594
|
+
|
|
333
595
|
attachSocket(socket) {
|
|
334
596
|
if (this.socket && this.socket !== socket) {
|
|
335
597
|
this.socket.destroy();
|
|
336
598
|
}
|
|
599
|
+
const generation = this.socketGeneration + 1;
|
|
600
|
+
this.socketGeneration = generation;
|
|
337
601
|
this.socket = socket;
|
|
338
602
|
this.buffer = '';
|
|
339
603
|
|
|
340
|
-
socket.on('data', chunk => this.handleData(chunk));
|
|
341
|
-
socket.on('close', () => this.handleDisconnect(socket));
|
|
604
|
+
socket.on('data', chunk => this.handleData(chunk, socket, generation));
|
|
605
|
+
socket.on('close', () => this.handleDisconnect(socket, generation));
|
|
342
606
|
socket.on('error', error => {
|
|
607
|
+
if (this.socket !== socket || this.socketGeneration !== generation) return;
|
|
343
608
|
this.emit('host-error', error);
|
|
344
609
|
});
|
|
345
610
|
}
|
|
346
611
|
|
|
347
|
-
handleDisconnect(socket) {
|
|
612
|
+
handleDisconnect(socket, generation = this.socketGeneration) {
|
|
348
613
|
if (socket && this.socket && this.socket !== socket) {
|
|
349
614
|
return;
|
|
350
615
|
}
|
|
616
|
+
if (generation !== this.socketGeneration) return;
|
|
351
617
|
this.socket = null;
|
|
352
618
|
this.buffer = '';
|
|
619
|
+
this.connectedHostInfo = null;
|
|
353
620
|
if (this.disposed) return;
|
|
354
621
|
const error = new Error('Native pty host disconnected');
|
|
355
622
|
error.code = 'ECONNRESET';
|
|
@@ -374,7 +641,8 @@ class NativePtyHostClient extends EventEmitter {
|
|
|
374
641
|
}
|
|
375
642
|
}
|
|
376
643
|
|
|
377
|
-
handleData(chunk) {
|
|
644
|
+
handleData(chunk, socket = this.socket, generation = this.socketGeneration) {
|
|
645
|
+
if (!socket || socket !== this.socket || generation !== this.socketGeneration) return;
|
|
378
646
|
this.buffer += chunk.toString('utf8');
|
|
379
647
|
let newline = this.buffer.indexOf('\n');
|
|
380
648
|
while (newline >= 0) {
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
const NATIVE_PTY_HOST_PROTOCOL_VERSION = 8;
|
|
6
|
+
const NATIVE_PTY_HOST_RUNTIME_FILES = Object.freeze([
|
|
7
|
+
'agent-env.js',
|
|
8
|
+
'input-parts.js',
|
|
9
|
+
'local-session-engine.js',
|
|
10
|
+
'native-pty-host.js',
|
|
11
|
+
'shell-busy-integration.js',
|
|
12
|
+
'terminal-screen-worker-pool.js',
|
|
13
|
+
'terminal-screen-worker-thread.js',
|
|
14
|
+
'terminal-screen-worker.js',
|
|
15
|
+
'terminal-reducer-flow-control.js',
|
|
16
|
+
'terminal-state-serialization.js',
|
|
17
|
+
'native-pty-controller-generation.js',
|
|
18
|
+
'terminal-status.js',
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
function readFileForIdentity(filePath) {
|
|
22
|
+
try {
|
|
23
|
+
return fs.readFileSync(filePath);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
return Buffer.from(`missing:${error && error.code ? error.code : 'unknown'}`, 'utf8');
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function packageVersion(backendDir) {
|
|
30
|
+
try {
|
|
31
|
+
const packageJson = JSON.parse(fs.readFileSync(path.join(backendDir, '..', 'package.json'), 'utf8'));
|
|
32
|
+
return typeof packageJson.version === 'string' ? packageJson.version : '';
|
|
33
|
+
} catch {
|
|
34
|
+
return '';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function nativePtyHostRuntimeIdentity(backendDir = __dirname) {
|
|
39
|
+
const hash = crypto.createHash('sha256');
|
|
40
|
+
const version = packageVersion(backendDir);
|
|
41
|
+
hash.update(`protocol:${NATIVE_PTY_HOST_PROTOCOL_VERSION}\n`);
|
|
42
|
+
hash.update(`version:${version}\n`);
|
|
43
|
+
for (const filename of NATIVE_PTY_HOST_RUNTIME_FILES) {
|
|
44
|
+
hash.update(`file:${filename}\n`);
|
|
45
|
+
hash.update(readFileForIdentity(path.join(backendDir, filename)));
|
|
46
|
+
hash.update('\n');
|
|
47
|
+
}
|
|
48
|
+
return Object.freeze({
|
|
49
|
+
protocolVersion: NATIVE_PTY_HOST_PROTOCOL_VERSION,
|
|
50
|
+
buildId: hash.digest('hex'),
|
|
51
|
+
version,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function normalizeNativePtyHostRuntimeIdentity(value) {
|
|
56
|
+
if (!value || typeof value !== 'object') return null;
|
|
57
|
+
const protocolVersion = Number(value.protocolVersion);
|
|
58
|
+
const buildId = typeof value.buildId === 'string' ? value.buildId.trim() : '';
|
|
59
|
+
if (!Number.isInteger(protocolVersion) || protocolVersion <= 0 || !/^[a-f0-9]{64}$/i.test(buildId)) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
protocolVersion,
|
|
64
|
+
buildId: buildId.toLowerCase(),
|
|
65
|
+
version: typeof value.version === 'string' ? value.version : '',
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function nativePtyHostRuntimeIdentityMatches(expected, actual) {
|
|
70
|
+
const normalizedExpected = normalizeNativePtyHostRuntimeIdentity(expected);
|
|
71
|
+
const normalizedActual = normalizeNativePtyHostRuntimeIdentity(actual);
|
|
72
|
+
return Boolean(
|
|
73
|
+
normalizedExpected &&
|
|
74
|
+
normalizedActual &&
|
|
75
|
+
normalizedExpected.protocolVersion === normalizedActual.protocolVersion &&
|
|
76
|
+
normalizedExpected.buildId === normalizedActual.buildId
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
NATIVE_PTY_HOST_PROTOCOL_VERSION,
|
|
82
|
+
NATIVE_PTY_HOST_RUNTIME_FILES,
|
|
83
|
+
nativePtyHostRuntimeIdentity,
|
|
84
|
+
nativePtyHostRuntimeIdentityMatches,
|
|
85
|
+
normalizeNativePtyHostRuntimeIdentity,
|
|
86
|
+
};
|