Skip to content

Commit 3fa4dce

Browse files
committed
Merge remote-tracking branch 'origin/dev' into dev
2 parents bfebdff + cb97b97 commit 3fa4dce

5 files changed

Lines changed: 275 additions & 36 deletions

File tree

package.json

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -34,17 +34,17 @@
3434
"build": "node --max-old-space-size=8192 ./node_modules/electron-vite/bin/electron-vite.js build",
3535
"stage-runtime": "node ./scripts/stage-runtime.mjs",
3636
"clean:runtime": "node ./scripts/stage-runtime.mjs --clean",
37-
"pack:mac": "electron-builder --mac",
38-
"pack:mac:arm64": "electron-builder --mac --arm64",
39-
"pack:mac:x64": "electron-builder --mac --x64",
40-
"pack:win": "electron-builder --win",
41-
"pack:linux": "electron-builder --linux",
42-
"dist": "pnpm build && node ./scripts/stage-runtime.mjs && electron-builder",
43-
"dist:mac": "pnpm build && node ./scripts/stage-runtime.mjs && electron-builder --mac",
44-
"dist:mac:arm64": "pnpm build && node ./scripts/stage-runtime.mjs --platform darwin-arm64 && electron-builder --mac --arm64",
45-
"dist:mac:x64": "pnpm build && node ./scripts/stage-runtime.mjs --platform darwin-x64 && electron-builder --mac --x64",
46-
"dist:win": "pnpm build && node ./scripts/stage-runtime.mjs --platform win32-x64 && electron-builder --win",
47-
"dist:linux": "pnpm build && node ./scripts/stage-runtime.mjs --platform linux-x64 && electron-builder --linux",
37+
"pack:mac": "node ./scripts/electron-builder/dist.mjs --mac",
38+
"pack:mac:arm64": "node ./scripts/electron-builder/dist.mjs --mac --arm64",
39+
"pack:mac:x64": "node ./scripts/electron-builder/dist.mjs --mac --x64",
40+
"pack:win": "node ./scripts/electron-builder/dist.mjs --win",
41+
"pack:linux": "node ./scripts/electron-builder/dist.mjs --linux",
42+
"dist": "pnpm build && node ./scripts/stage-runtime.mjs && node ./scripts/electron-builder/dist.mjs",
43+
"dist:mac": "pnpm build && node ./scripts/stage-runtime.mjs && node ./scripts/electron-builder/dist.mjs --mac",
44+
"dist:mac:arm64": "pnpm build && node ./scripts/stage-runtime.mjs --platform darwin-arm64 && node ./scripts/electron-builder/dist.mjs --mac --arm64",
45+
"dist:mac:x64": "pnpm build && node ./scripts/stage-runtime.mjs --platform darwin-x64 && node ./scripts/electron-builder/dist.mjs --mac --x64",
46+
"dist:win": "pnpm build && node ./scripts/stage-runtime.mjs --platform win32-x64 && node ./scripts/electron-builder/dist.mjs --win",
47+
"dist:linux": "pnpm build && node ./scripts/stage-runtime.mjs --platform linux-x64 && node ./scripts/electron-builder/dist.mjs --linux",
4848
"smoke:packaged": "node ./scripts/electron-builder/smokePackagedApp.cjs",
4949
"preview": "electron-vite preview",
5050
"typecheck": "tsc --noEmit",

scripts/lib/opencode-live-preflight.mjs

Lines changed: 96 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ import net from 'node:net';
44
import os from 'node:os';
55
import path from 'node:path';
66

7+
const CHILD_CLOSE_GRACE_MS = 3_000;
8+
const CHILD_FORCE_CLOSE_GRACE_MS = 1_000;
9+
const TASKKILL_TIMEOUT_MS = 5_000;
10+
711
export async function preflightOpenCodeLiveEnvironment(input) {
812
const repoRoot = input.repoRoot;
913
const opencodeBin = process.env.OPENCODE_BIN?.trim() || '/opt/homebrew/bin/opencode';
@@ -97,6 +101,7 @@ async function canStartOpenCodeHost(opencodeBin, cwd, env) {
97101
cwd,
98102
env,
99103
stdio: ['ignore', 'pipe', 'pipe'],
104+
windowsHide: true,
100105
});
101106
let output = '';
102107
let spawnError = '';
@@ -138,26 +143,100 @@ async function canStartOpenCodeHost(opencodeBin, cwd, env) {
138143
}
139144
}
140145

141-
function stopChild(child) {
146+
async function stopChild(child, options = {}) {
147+
const platform = options.platform ?? process.platform;
148+
const killProcessTree = options.killProcessTree ?? taskkillProcessTree;
149+
const closeGraceMs = options.closeGraceMs ?? CHILD_CLOSE_GRACE_MS;
150+
const forceCloseGraceMs = options.forceCloseGraceMs ?? CHILD_FORCE_CLOSE_GRACE_MS;
151+
152+
if (hasChildExited(child)) {
153+
return;
154+
}
155+
156+
if (platform === 'win32' && child.pid) {
157+
await killProcessTree(child.pid);
158+
} else if (!child.killed) {
159+
sendChildSignal(child, 'SIGTERM');
160+
}
161+
162+
if (await waitForChildClose(child, closeGraceMs)) {
163+
return;
164+
}
165+
166+
if (!hasChildExited(child)) {
167+
sendChildSignal(child, 'SIGKILL');
168+
if (!(await waitForChildClose(child, forceCloseGraceMs))) {
169+
child.stdout?.destroy();
170+
child.stderr?.destroy();
171+
child.unref?.();
172+
}
173+
}
174+
}
175+
176+
function taskkillProcessTree(pid) {
142177
return new Promise((resolve) => {
143-
if (child.exitCode != null || child.killed) {
178+
let done = false;
179+
let taskkill = null;
180+
const finish = () => {
181+
if (done) return;
182+
done = true;
183+
clearTimeout(timeout);
144184
resolve();
145-
return;
146-
}
185+
};
147186
const timeout = setTimeout(() => {
148-
if (child.exitCode == null) {
149-
child.kill('SIGKILL');
187+
if (taskkill) {
188+
sendChildSignal(taskkill, 'SIGTERM');
150189
}
151-
resolve();
152-
}, 3_000);
153-
child.once('close', () => {
190+
finish();
191+
}, TASKKILL_TIMEOUT_MS);
192+
try {
193+
taskkill = spawn(
194+
path.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'taskkill.exe'),
195+
['/T', '/F', '/PID', String(pid)],
196+
{
197+
stdio: 'ignore',
198+
windowsHide: true,
199+
}
200+
);
201+
taskkill.unref?.();
202+
taskkill.once('error', finish);
203+
taskkill.once('close', finish);
204+
} catch {
205+
finish();
206+
}
207+
});
208+
}
209+
210+
function waitForChildClose(child, timeoutMs) {
211+
if (hasChildExited(child)) {
212+
return Promise.resolve(true);
213+
}
214+
215+
return new Promise((resolve) => {
216+
let done = false;
217+
const finish = (closed) => {
218+
if (done) return;
219+
done = true;
154220
clearTimeout(timeout);
155-
resolve();
156-
});
157-
child.kill('SIGTERM');
221+
resolve(closed);
222+
};
223+
const timeout = setTimeout(() => finish(false), timeoutMs);
224+
child.once('close', () => finish(true));
158225
});
159226
}
160227

228+
function hasChildExited(child) {
229+
return child.exitCode != null || child.signalCode != null;
230+
}
231+
232+
function sendChildSignal(child, signal) {
233+
try {
234+
child.kill(signal);
235+
} catch {
236+
// Process may already be gone between liveness checks and the kill call.
237+
}
238+
}
239+
161240
function allocateLoopbackPort() {
162241
return new Promise((resolve, reject) => {
163242
const server = net.createServer();
@@ -190,3 +269,8 @@ function skip(reason) {
190269
function compactOutput(value) {
191270
return value.replace(/\s+/g, ' ').trim().slice(0, 1_200);
192271
}
272+
273+
export const __opencodeLivePreflightTestHooks = {
274+
stopChild,
275+
taskkillProcessTree,
276+
};

src/main/services/team/TeamProvisioningService.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -572,6 +572,11 @@ import type {
572572
ToolCallMeta,
573573
} from '@shared/types';
574574

575+
// pidusage's Windows wmic/gwmi fallback needs a non-zero cache window to finish
576+
// its initial two-sample pass. Keep this above slow PowerShell startup time, or
577+
// the first sample can expire before the recursive second read and loop again.
578+
const RUNTIME_PIDUSAGE_OPTIONS = process.platform === 'win32' ? { maxage: 10_000 } : { maxage: 0 };
579+
575580
const logger = createLogger('Service:TeamProvisioning');
576581
const PREFLIGHT_DEBUG_LOG_PATH = path.join(os.tmpdir(), 'claude-team-preflight-debug.log');
577582

@@ -15298,7 +15303,7 @@ export class TeamProvisioningService {
1529815303
let rssBytes = rssPid ? rssBytesByPid.get(rssPid) : undefined;
1529915304
if (rssBytes == null && isSharedOpenCodeHost && typeof rssPid === 'number' && rssPid > 0) {
1530015305
try {
15301-
const refreshedStat = await pidusage(rssPid, { maxage: 0 });
15306+
const refreshedStat = await pidusage(rssPid, RUNTIME_PIDUSAGE_OPTIONS);
1530215307
if (Number.isFinite(refreshedStat.memory) && refreshedStat.memory >= 0) {
1530315308
rssBytesByPid.set(rssPid, refreshedStat.memory);
1530415309
rssBytes = refreshedStat.memory;
@@ -25558,7 +25563,7 @@ export class TeamProvisioningService {
2555825563
}
2555925564

2556025565
const rssBytesByPid = new Map<number, number>();
25561-
const options = { maxage: 0 };
25566+
const options = RUNTIME_PIDUSAGE_OPTIONS;
2556225567
try {
2556325568
const statsByPid = await pidusage(uniquePids, options);
2556425569
for (const [rawPid, stat] of Object.entries(statsByPid)) {

test/main/services/team/TeamProvisioningService.test.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,9 @@ import {
176176
} from '@features/tmux-installer/main';
177177
import pidusage from 'pidusage';
178178

179+
const EXPECTED_RUNTIME_PIDUSAGE_OPTIONS =
180+
process.platform === 'win32' ? { maxage: 10_000 } : { maxage: 0 };
181+
179182
function allowConsoleLogs() {
180183
vi.spyOn(console, 'error').mockImplementation(() => {});
181184
vi.spyOn(console, 'warn').mockImplementation(() => {});
@@ -2490,7 +2493,7 @@ describe('TeamProvisioningService', () => {
24902493

24912494
const snapshot = await svc.getTeamAgentRuntimeSnapshot('runtime-team');
24922495

2493-
expect(pidusage).toHaveBeenCalledWith([111, 222], { maxage: 0 });
2496+
expect(pidusage).toHaveBeenCalledWith([111, 222], EXPECTED_RUNTIME_PIDUSAGE_OPTIONS);
24942497
expect(snapshot.members['team-lead']).toMatchObject({
24952498
pid: 111,
24962499
rssBytes: 123_000_000,
@@ -2630,9 +2633,9 @@ describe('TeamProvisioningService', () => {
26302633

26312634
const snapshot = await svc.getTeamAgentRuntimeSnapshot('runtime-team');
26322635

2633-
expect(pidusage).toHaveBeenNthCalledWith(1, [111, 222], { maxage: 0 });
2634-
expect(pidusage).toHaveBeenNthCalledWith(2, 111, { maxage: 0 });
2635-
expect(pidusage).toHaveBeenNthCalledWith(3, 222, { maxage: 0 });
2636+
expect(pidusage).toHaveBeenNthCalledWith(1, [111, 222], EXPECTED_RUNTIME_PIDUSAGE_OPTIONS);
2637+
expect(pidusage).toHaveBeenNthCalledWith(2, 111, EXPECTED_RUNTIME_PIDUSAGE_OPTIONS);
2638+
expect(pidusage).toHaveBeenNthCalledWith(3, 222, EXPECTED_RUNTIME_PIDUSAGE_OPTIONS);
26362639
expect(snapshot.members['team-lead']?.rssBytes).toBe(123_000_000);
26372640
expect(snapshot.members.alice?.rssBytes).toBe(456_000_000);
26382641
});
@@ -2744,7 +2747,7 @@ describe('TeamProvisioningService', () => {
27442747

27452748
const snapshot = await svc.getTeamAgentRuntimeSnapshot('nice-team');
27462749

2747-
expect(pidusage).toHaveBeenCalledWith([111, 333], { maxage: 0 });
2750+
expect(pidusage).toHaveBeenCalledWith([111, 333], EXPECTED_RUNTIME_PIDUSAGE_OPTIONS);
27482751
expect(snapshot.members.alice).toMatchObject({
27492752
alive: true,
27502753
providerId: 'anthropic',
@@ -3256,8 +3259,8 @@ describe('TeamProvisioningService', () => {
32563259

32573260
const snapshot = await svc.getTeamAgentRuntimeSnapshot('runtime-team');
32583261

3259-
expect(pidusage).toHaveBeenCalledWith([111, 333], { maxage: 0 });
3260-
expect(pidusage).toHaveBeenCalledWith(333, { maxage: 0 });
3262+
expect(pidusage).toHaveBeenCalledWith([111, 333], EXPECTED_RUNTIME_PIDUSAGE_OPTIONS);
3263+
expect(pidusage).toHaveBeenCalledWith(333, EXPECTED_RUNTIME_PIDUSAGE_OPTIONS);
32613264
expect(snapshot.members.bob).toMatchObject({
32623265
memberName: 'bob',
32633266
alive: false,
@@ -3332,7 +3335,7 @@ describe('TeamProvisioningService', () => {
33323335

33333336
const snapshot = await svc.getTeamAgentRuntimeSnapshot('runtime-team');
33343337

3335-
expect(pidusage).toHaveBeenCalledWith([333], { maxage: 0 });
3338+
expect(pidusage).toHaveBeenCalledWith([333], EXPECTED_RUNTIME_PIDUSAGE_OPTIONS);
33363339
expect(snapshot.members.bob).toMatchObject({
33373340
memberName: 'bob',
33383341
alive: false,
@@ -15450,9 +15453,9 @@ describe('TeamProvisioningService', () => {
1545015453
});
1545115454

1545215455
expect(spawnCli).toHaveBeenCalled();
15453-
expect(progressUpdates[0]?.warnings).toEqual(expect.arrayContaining([
15454-
expect.stringContaining('9 primary teammates'),
15455-
]));
15456+
expect(progressUpdates[0]?.warnings).toEqual(
15457+
expect.arrayContaining([expect.stringContaining('9 primary teammates')])
15458+
);
1545615459
expect(progressUpdates[0]?.warnings?.join('\n')).toContain('Launches above 8 teammates');
1545715460
});
1545815461

0 commit comments

Comments
 (0)