nterminal 1.3.8 → 1.3.9
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 +1 -1
- package/dist/client/assets/MarkdownPreview-OPnFTPId.js +1 -0
- package/dist/client/assets/{TranscriptMarkdownBody-Dd6cp7t2.js → TranscriptMarkdownBody-CwtPlXXA.js} +1 -1
- package/dist/client/assets/{index-DJwOCZv_.js → index-BeEACkVn.js} +1 -1
- package/dist/client/assets/{index-CWRDvfyP.css → index-CFEvEJOV.css} +1 -1
- package/dist/client/assets/index-D7fYqfyY.js +59 -0
- package/dist/client/index.html +2 -2
- package/dist/server/accounts/accountService.d.ts +16 -5
- package/dist/server/accounts/accountService.js +157 -20
- package/dist/server/accounts/accountService.js.map +1 -1
- package/dist/server/accounts/loginSession.d.ts +67 -0
- package/dist/server/accounts/loginSession.js +333 -0
- package/dist/server/accounts/loginSession.js.map +1 -0
- package/dist/server/agent/agentRoutes.js +11 -1
- package/dist/server/agent/agentRoutes.js.map +1 -1
- package/dist/server/routes/accountRoutes.js +15 -0
- package/dist/server/routes/accountRoutes.js.map +1 -1
- package/dist/server/routes/agentManagementRoutes.js +13 -5
- package/dist/server/routes/agentManagementRoutes.js.map +1 -1
- package/dist/shared/accountLogin.d.ts +3 -0
- package/dist/shared/accountLogin.js +47 -0
- package/dist/shared/accountLogin.js.map +1 -0
- package/dist/shared/protocol.d.ts +20 -5
- package/dist/shared/protocol.js.map +1 -1
- package/docs/features.md +2 -0
- package/package.json +1 -1
- package/dist/client/assets/MarkdownPreview-D3Oa7_1H.js +0 -1
- package/dist/client/assets/index-BOvp6zV9.js +0 -58
package/dist/client/index.html
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
<link rel="icon" type="image/png" sizes="64x64" href="/icons/favicon-64.png" />
|
|
13
13
|
<link rel="icon" type="image/png" sizes="96x96" href="/icons/favicon-96.png" />
|
|
14
14
|
<title>NTerminal</title>
|
|
15
|
-
<script type="module" crossorigin src="/assets/index-
|
|
16
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
15
|
+
<script type="module" crossorigin src="/assets/index-D7fYqfyY.js"></script>
|
|
16
|
+
<link rel="stylesheet" crossorigin href="/assets/index-CFEvEJOV.css">
|
|
17
17
|
</head>
|
|
18
18
|
<body>
|
|
19
19
|
<div id="root"></div>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { AppConfig } from '../config.js';
|
|
2
2
|
import type { TerminalManager } from '../terminal/TerminalManager.js';
|
|
3
|
-
import type { TerminalSummary } from '../../shared/types.js';
|
|
4
3
|
import type { AccountProvider, AccountStatusResponse, AccountRunningProcess, AccountRefreshStatus, AccountUsageResponse } from '../../shared/protocol.js';
|
|
4
|
+
import { type LoginLauncher } from './loginSession.js';
|
|
5
5
|
export type AccountDiagnostic = {
|
|
6
6
|
event: 'account.refresh' | 'account.refresh_scan';
|
|
7
7
|
provider?: AccountProvider;
|
|
@@ -49,6 +49,11 @@ export declare class AccountService {
|
|
|
49
49
|
private readonly paneProcessResolver;
|
|
50
50
|
private readonly processTerminator;
|
|
51
51
|
private readonly processSnapshotReader;
|
|
52
|
+
private readonly loginSessions;
|
|
53
|
+
private readonly loginNotices;
|
|
54
|
+
private readonly loginLauncher;
|
|
55
|
+
private readonly loginTimeoutMs;
|
|
56
|
+
private readonly loginExitWaitMs;
|
|
52
57
|
constructor(config: AppConfig, terminalManager: TerminalManager, options?: {
|
|
53
58
|
homeDir?: string;
|
|
54
59
|
now?: () => Date;
|
|
@@ -59,15 +64,16 @@ export declare class AccountService {
|
|
|
59
64
|
processSnapshot?: () => Promise<ProcessSnapshotEntry[]>;
|
|
60
65
|
fetcher?: typeof fetch;
|
|
61
66
|
onDiagnostic?: (event: AccountDiagnostic) => void;
|
|
67
|
+
loginLauncher?: LoginLauncher;
|
|
68
|
+
loginTimeoutMs?: number;
|
|
69
|
+
loginExitWaitMs?: number;
|
|
62
70
|
});
|
|
63
71
|
private mutate;
|
|
64
72
|
synchronize(): Promise<void>;
|
|
65
73
|
select(provider: AccountProvider, profileId: string): Promise<AccountStatusResponse>;
|
|
66
74
|
remove(provider: AccountProvider, profileId: string): Promise<AccountStatusResponse>;
|
|
67
|
-
beginLogin(provider: AccountProvider): Promise<
|
|
68
|
-
|
|
69
|
-
command: string;
|
|
70
|
-
}>;
|
|
75
|
+
beginLogin(provider: AccountProvider): Promise<AccountStatusResponse>;
|
|
76
|
+
submitLoginCode(provider: AccountProvider, code: unknown): Promise<AccountStatusResponse>;
|
|
71
77
|
completeLogin(provider: AccountProvider, label: string): Promise<AccountStatusResponse>;
|
|
72
78
|
cancelLogin(provider: AccountProvider): Promise<AccountStatusResponse>;
|
|
73
79
|
refresh(provider: AccountProvider, profileId?: string): Promise<AccountStatusResponse>;
|
|
@@ -85,6 +91,11 @@ export declare class AccountService {
|
|
|
85
91
|
private refreshUnlocked;
|
|
86
92
|
start(): void;
|
|
87
93
|
dispose(): void;
|
|
94
|
+
private loginProcessIds;
|
|
95
|
+
private stopLoginSession;
|
|
96
|
+
private expireLogin;
|
|
97
|
+
private expireLoginNotices;
|
|
98
|
+
private loginState;
|
|
88
99
|
private refreshExpiringProfiles;
|
|
89
100
|
private refreshAndRecord;
|
|
90
101
|
private reportDiagnostic;
|
|
@@ -6,6 +6,7 @@ import os from 'node:os';
|
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { accountReloginReason } from '../../shared/accountRelogin.js';
|
|
8
8
|
import { ProviderRequestError, requestAccountUsage, requestTokenRefresh, unavailableUsage } from './providerRequests.js';
|
|
9
|
+
import { createPtyLoginLauncher, LOGIN_TIMEOUT_MS, LoginSessionError, ProviderLoginSession, validateLoginCode } from './loginSession.js';
|
|
9
10
|
const VAULT_DIRECTORY = 'credential-vault';
|
|
10
11
|
const VAULT_INDEX = 'index.json';
|
|
11
12
|
const VAULT_KEY = 'key';
|
|
@@ -23,6 +24,9 @@ const PROVIDER_TERMINATE_GRACE_MS = 2_000;
|
|
|
23
24
|
const TERMINATION_ACKNOWLEDGEMENT_TTL_MS = 5 * 60_000;
|
|
24
25
|
const USAGE_CACHE_TTL_MS = 2 * 60_000;
|
|
25
26
|
const USAGE_FAILURE_TTL_MS = 30_000;
|
|
27
|
+
const LOGIN_COMPLETE_EXIT_WAIT_MS = 5_000;
|
|
28
|
+
const LOGIN_NOTICE_TTL_MS = 10 * 60_000;
|
|
29
|
+
const LOGIN_EXPIRY_RETRY_MS = 500;
|
|
26
30
|
export class AccountServiceError extends Error {
|
|
27
31
|
code;
|
|
28
32
|
constructor(code, message = code) {
|
|
@@ -33,7 +37,7 @@ export class AccountServiceError extends Error {
|
|
|
33
37
|
get statusCode() {
|
|
34
38
|
if (this.code === 'preview_mode')
|
|
35
39
|
return 403;
|
|
36
|
-
if (['provider_panes_running', 'account_operation_busy', 'account_credential_changed', 'account_login_already_pending', 'active_cli_refresh_deferred'].includes(this.code))
|
|
40
|
+
if (['provider_panes_running', 'account_operation_busy', 'account_credential_changed', 'account_login_already_pending', 'active_cli_refresh_deferred', 'account_login_code_not_expected', 'account_login_process_unavailable', 'account_login_code_attempts_exceeded'].includes(this.code))
|
|
37
41
|
return 409;
|
|
38
42
|
return 400;
|
|
39
43
|
}
|
|
@@ -64,6 +68,12 @@ export class AccountService {
|
|
|
64
68
|
paneProcessResolver;
|
|
65
69
|
processTerminator;
|
|
66
70
|
processSnapshotReader;
|
|
71
|
+
// In-popup sign-ins: one private PTY per provider, never a workspace terminal.
|
|
72
|
+
loginSessions = new Map();
|
|
73
|
+
loginNotices = new Map();
|
|
74
|
+
loginLauncher;
|
|
75
|
+
loginTimeoutMs;
|
|
76
|
+
loginExitWaitMs;
|
|
67
77
|
constructor(config, terminalManager, options = {}) {
|
|
68
78
|
this.config = config;
|
|
69
79
|
this.terminalManager = terminalManager;
|
|
@@ -75,13 +85,17 @@ export class AccountService {
|
|
|
75
85
|
this.now = options.now ?? (() => new Date());
|
|
76
86
|
this.fetcher = options.fetcher ?? ((...args) => fetch(...args));
|
|
77
87
|
this.onDiagnostic = options.onDiagnostic ?? (() => undefined);
|
|
78
|
-
|
|
88
|
+
// The in-popup login CLI is NTerminal's own child, not a user session.
|
|
89
|
+
this.processLister = options.processLister ?? (async (provider) => providerProcesses(provider, withoutProcessTrees(await this.listProcessSnapshot(), this.loginProcessIds())));
|
|
79
90
|
this.paneProcessResolver = options.paneProcessResolver ?? (options.processLister
|
|
80
91
|
? async () => new Map()
|
|
81
92
|
: async () => resolvePaneProcesses(this.config.tmuxSocket, await this.listProcessSnapshot()));
|
|
82
93
|
this.processProbe = options.processProbe ?? (async (provider) => (await this.processLister(provider)).length > 0);
|
|
83
|
-
this.processTerminator = options.processTerminator ?? terminateProviderProcesses;
|
|
94
|
+
this.processTerminator = options.processTerminator ?? ((provider, processes) => terminateProviderProcesses(provider, processes, this.loginProcessIds()));
|
|
84
95
|
this.processSnapshotReader = options.processSnapshot ?? listUserProcesses;
|
|
96
|
+
this.loginLauncher = options.loginLauncher ?? createPtyLoginLauncher();
|
|
97
|
+
this.loginTimeoutMs = options.loginTimeoutMs ?? LOGIN_TIMEOUT_MS;
|
|
98
|
+
this.loginExitWaitMs = options.loginExitWaitMs ?? LOGIN_COMPLETE_EXIT_WAIT_MS;
|
|
85
99
|
}
|
|
86
100
|
// Reject overlapping mutations instead of accumulating requests behind OAuth I/O.
|
|
87
101
|
async mutate(operation) {
|
|
@@ -112,6 +126,32 @@ export class AccountService {
|
|
|
112
126
|
beginLogin(provider) {
|
|
113
127
|
return this.mutate(() => this.beginLoginUnlocked(provider));
|
|
114
128
|
}
|
|
129
|
+
// Forwards a pasted Claude authorization code to the login CLI. Only a
|
|
130
|
+
// strictly shaped `code#state` value is written, followed by Enter.
|
|
131
|
+
async submitLoginCode(provider, code) {
|
|
132
|
+
if (this.config.previewMode)
|
|
133
|
+
throw new AccountServiceError('preview_mode');
|
|
134
|
+
if (this.disposed)
|
|
135
|
+
throw new AccountServiceError('account_operation_busy');
|
|
136
|
+
const valid = validateLoginCode(provider, code);
|
|
137
|
+
if (!valid)
|
|
138
|
+
throw new AccountServiceError(provider === 'claude' ? 'account_login_code_invalid' : 'account_login_code_not_expected');
|
|
139
|
+
const index = await this.readIndex();
|
|
140
|
+
if (!index.pending[provider])
|
|
141
|
+
throw new AccountServiceError('account_login_not_pending');
|
|
142
|
+
const session = this.loginSessions.get(provider);
|
|
143
|
+
if (!session)
|
|
144
|
+
throw new AccountServiceError('account_login_process_unavailable');
|
|
145
|
+
try {
|
|
146
|
+
session.submitCode(valid);
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (error instanceof LoginSessionError)
|
|
150
|
+
throw new AccountServiceError(error.code);
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
return this.status();
|
|
154
|
+
}
|
|
115
155
|
completeLogin(provider, label) {
|
|
116
156
|
return this.mutate(() => this.completeLoginUnlocked(provider, label));
|
|
117
157
|
}
|
|
@@ -229,6 +269,7 @@ export class AccountService {
|
|
|
229
269
|
await Promise.all(['codex', 'claude'].map(async (provider) => {
|
|
230
270
|
pendingReady.set(provider, Boolean(refreshed.pending[provider] && await this.readCredentialFile(provider).catch(() => null)));
|
|
231
271
|
}));
|
|
272
|
+
this.expireLoginNotices();
|
|
232
273
|
return {
|
|
233
274
|
providers: ['codex', 'claude'].map((provider) => this.providerStatus(refreshed, provider, running, processes.get(provider) ?? [], processCheckUnavailable, pendingReady.get(provider) === true))
|
|
234
275
|
};
|
|
@@ -305,28 +346,36 @@ export class AccountService {
|
|
|
305
346
|
index.pending[provider] = {
|
|
306
347
|
provider,
|
|
307
348
|
previousProfileId: index.active[provider] ?? null,
|
|
308
|
-
startedAt: this.now().toISOString()
|
|
309
|
-
terminalId: null
|
|
349
|
+
startedAt: this.now().toISOString()
|
|
310
350
|
};
|
|
311
351
|
delete index.active[provider];
|
|
312
352
|
await this.writeIndex(index);
|
|
313
|
-
let
|
|
353
|
+
let session;
|
|
314
354
|
try {
|
|
355
|
+
// `codex login` revokes the tokens of an auth.json it finds, so the CLI
|
|
356
|
+
// must start only after the archived live file is gone.
|
|
315
357
|
if (current)
|
|
316
358
|
await this.removeCredentialFile(provider);
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
359
|
+
await this.stopLoginSession(provider);
|
|
360
|
+
let launched;
|
|
361
|
+
try {
|
|
362
|
+
launched = this.loginLauncher(provider, { homeDir: this.homeDir });
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
throw new AccountServiceError('account_login_process_unavailable');
|
|
366
|
+
}
|
|
367
|
+
session = new ProviderLoginSession(provider, launched, () => this.now().getTime(), this.loginTimeoutMs, (expired) => void this.expireLogin(provider, expired));
|
|
368
|
+
this.loginSessions.set(provider, session);
|
|
369
|
+
this.loginNotices.delete(provider);
|
|
324
370
|
this.clearTerminationAcknowledgement(provider);
|
|
325
|
-
return
|
|
371
|
+
return this.status();
|
|
326
372
|
}
|
|
327
373
|
catch (error) {
|
|
328
|
-
if (
|
|
329
|
-
|
|
374
|
+
if (session) {
|
|
375
|
+
if (this.loginSessions.get(provider) === session)
|
|
376
|
+
this.loginSessions.delete(provider);
|
|
377
|
+
await session.terminate();
|
|
378
|
+
}
|
|
330
379
|
if (current)
|
|
331
380
|
await this.writeCredentialFile(provider, current);
|
|
332
381
|
if (index.active[provider] === undefined && index.pending[provider]?.previousProfileId) {
|
|
@@ -342,6 +391,12 @@ export class AccountService {
|
|
|
342
391
|
const pending = index.pending[provider];
|
|
343
392
|
if (!pending)
|
|
344
393
|
throw new AccountServiceError('account_login_not_pending');
|
|
394
|
+
if (!await this.readCredentialFile(provider))
|
|
395
|
+
throw new AccountServiceError('account_login_not_complete');
|
|
396
|
+
// Let the CLI finish its own post-login writes before the file is captured.
|
|
397
|
+
const session = this.loginSessions.get(provider);
|
|
398
|
+
if (session?.running && !await session.waitForExit(this.loginExitWaitMs))
|
|
399
|
+
await session.terminate();
|
|
345
400
|
const credentials = await this.readCredentialFile(provider);
|
|
346
401
|
if (!credentials)
|
|
347
402
|
throw new AccountServiceError('account_login_not_complete');
|
|
@@ -352,10 +407,14 @@ export class AccountService {
|
|
|
352
407
|
delete index.pending[provider];
|
|
353
408
|
clearExternalSwitch(index, provider);
|
|
354
409
|
await this.writeIndex(index);
|
|
410
|
+
if (this.loginSessions.get(provider) === session)
|
|
411
|
+
this.loginSessions.delete(provider);
|
|
355
412
|
this.clearTerminationAcknowledgement(provider);
|
|
356
413
|
return this.status();
|
|
357
414
|
}
|
|
358
415
|
async cancelLoginUnlocked(provider) {
|
|
416
|
+
// Stop the CLI first so it cannot write a credential after the restore.
|
|
417
|
+
await this.stopLoginSession(provider);
|
|
359
418
|
const index = await this.readIndex();
|
|
360
419
|
const pending = index.pending[provider];
|
|
361
420
|
if (!pending)
|
|
@@ -395,6 +454,68 @@ export class AccountService {
|
|
|
395
454
|
if (this.refreshTimer)
|
|
396
455
|
clearInterval(this.refreshTimer);
|
|
397
456
|
this.refreshTimer = null;
|
|
457
|
+
// The pending entry survives in the index; after a restart the dialog
|
|
458
|
+
// reports the missing login process and offers to cancel and restore.
|
|
459
|
+
for (const session of this.loginSessions.values())
|
|
460
|
+
session.dispose();
|
|
461
|
+
this.loginSessions.clear();
|
|
462
|
+
}
|
|
463
|
+
loginProcessIds() {
|
|
464
|
+
return [...this.loginSessions.values()].filter((session) => session.running).map((session) => session.pid);
|
|
465
|
+
}
|
|
466
|
+
async stopLoginSession(provider) {
|
|
467
|
+
const session = this.loginSessions.get(provider);
|
|
468
|
+
if (!session)
|
|
469
|
+
return;
|
|
470
|
+
this.loginSessions.delete(provider);
|
|
471
|
+
await session.terminate();
|
|
472
|
+
}
|
|
473
|
+
// Timeout: stop the CLI and restore the previous credential through the
|
|
474
|
+
// normal cancel path, retrying while another account operation holds the lock.
|
|
475
|
+
async expireLogin(provider, session) {
|
|
476
|
+
await session.terminate();
|
|
477
|
+
for (let attempt = 0; attempt < 120 && !this.disposed; attempt += 1) {
|
|
478
|
+
if (this.loginSessions.get(provider) !== session)
|
|
479
|
+
return;
|
|
480
|
+
try {
|
|
481
|
+
await this.cancelLogin(provider);
|
|
482
|
+
this.loginNotices.set(provider, { code: 'login_timed_out', at: this.now().toISOString() });
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
if (!(error instanceof AccountServiceError) || error.code !== 'account_operation_busy')
|
|
487
|
+
return;
|
|
488
|
+
await sleep(LOGIN_EXPIRY_RETRY_MS);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
expireLoginNotices() {
|
|
493
|
+
for (const [provider, notice] of this.loginNotices) {
|
|
494
|
+
if (this.now().getTime() - Date.parse(notice.at) > LOGIN_NOTICE_TTL_MS)
|
|
495
|
+
this.loginNotices.delete(provider);
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
loginState(provider, ready) {
|
|
499
|
+
const session = this.loginSessions.get(provider);
|
|
500
|
+
if (!session) {
|
|
501
|
+
return ready ? { phase: 'done' } : {
|
|
502
|
+
phase: 'failed',
|
|
503
|
+
message: 'The sign-in process is no longer running (NTerminal may have restarted). Cancel to restore the previous account, then try again.'
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
const state = session.state();
|
|
507
|
+
if (ready) {
|
|
508
|
+
// The credential exists; wait for the CLI to report success or exit.
|
|
509
|
+
if (session.succeeded || !session.running)
|
|
510
|
+
return { phase: 'done' };
|
|
511
|
+
return { ...state, phase: 'completing' };
|
|
512
|
+
}
|
|
513
|
+
if (state.phase === 'done') {
|
|
514
|
+
return session.running ? { ...state, phase: 'completing' } : {
|
|
515
|
+
...state, phase: 'failed', message: 'The CLI reported success, but no credential file was written. Cancel and try again.'
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
return state;
|
|
398
519
|
}
|
|
399
520
|
async refreshExpiringProfiles() {
|
|
400
521
|
if (this.refreshInFlight || this.disposed || this.config.previewMode || this.operationInFlight)
|
|
@@ -495,7 +616,8 @@ export class AccountService {
|
|
|
495
616
|
runningProcesses,
|
|
496
617
|
processCheckUnavailable,
|
|
497
618
|
terminationAcknowledged: this.hasTerminationAcknowledgement(provider),
|
|
498
|
-
pendingLogin: pending ? { startedAt: pending.startedAt, ready: pendingReady,
|
|
619
|
+
pendingLogin: pending ? { startedAt: pending.startedAt, ready: pendingReady, login: this.loginState(provider, pendingReady) } : null,
|
|
620
|
+
loginNotice: pending ? null : this.loginNotices.get(provider) ?? null,
|
|
499
621
|
lastExternalSwitch: index.externalSwitch?.[provider] ?? null,
|
|
500
622
|
credentialPresent: Boolean(profile || pending || false)
|
|
501
623
|
};
|
|
@@ -1043,8 +1165,8 @@ function displayProcessCommand(command) {
|
|
|
1043
1165
|
const basename = path.basename(command);
|
|
1044
1166
|
return basename.length <= 80 ? basename : basename.slice(0, 80);
|
|
1045
1167
|
}
|
|
1046
|
-
async function terminateProviderProcesses(provider, _processes) {
|
|
1047
|
-
const snapshot = await listUserProcesses();
|
|
1168
|
+
async function terminateProviderProcesses(provider, _processes, excludedRoots = []) {
|
|
1169
|
+
const snapshot = withoutProcessTrees(await listUserProcesses(), excludedRoots);
|
|
1048
1170
|
const daemons = provider === 'codex' ? snapshot.filter(isCodexDaemon) : [];
|
|
1049
1171
|
// Shared daemon descendants include work for multiple TUI clients. Stop the
|
|
1050
1172
|
// interactive clients first, then let the daemon's lifecycle command stop it.
|
|
@@ -1062,7 +1184,7 @@ async function terminateProviderProcesses(provider, _processes) {
|
|
|
1062
1184
|
signalProcess(pid, 'SIGTERM');
|
|
1063
1185
|
if (targetPids.length > 0) {
|
|
1064
1186
|
await sleep(PROVIDER_TERMINATE_GRACE_MS);
|
|
1065
|
-
const survivors = new Set(providerProcessTree(provider, await listUserProcesses()));
|
|
1187
|
+
const survivors = new Set(providerProcessTree(provider, withoutProcessTrees(await listUserProcesses(), excludedRoots)));
|
|
1066
1188
|
for (const pid of targetPids)
|
|
1067
1189
|
if (survivors.has(pid))
|
|
1068
1190
|
signalProcess(pid, 'SIGKILL');
|
|
@@ -1085,6 +1207,21 @@ async function stopCodexDaemon(daemons) {
|
|
|
1085
1207
|
}
|
|
1086
1208
|
throw new AccountServiceError('codex_daemon_stop_failed');
|
|
1087
1209
|
}
|
|
1210
|
+
// Drops the given processes and all their descendants from a snapshot.
|
|
1211
|
+
function withoutProcessTrees(entries, roots) {
|
|
1212
|
+
if (roots.length === 0)
|
|
1213
|
+
return entries;
|
|
1214
|
+
const excluded = new Set(roots);
|
|
1215
|
+
for (let changed = true; changed;) {
|
|
1216
|
+
changed = false;
|
|
1217
|
+
for (const entry of entries)
|
|
1218
|
+
if (excluded.has(entry.ppid) && !excluded.has(entry.pid)) {
|
|
1219
|
+
excluded.add(entry.pid);
|
|
1220
|
+
changed = true;
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
return entries.filter((entry) => !excluded.has(entry.pid));
|
|
1224
|
+
}
|
|
1088
1225
|
function daemonDescendants(entries) {
|
|
1089
1226
|
const parents = new Map(entries.map((entry) => [entry.pid, entry.ppid]));
|
|
1090
1227
|
const daemons = new Set(entries.filter(isCodexDaemon).map((entry) => entry.pid));
|