fix(opencode): prevent Windows live runtime hangs
This commit is contained in:
parent
69572150c9
commit
678d12219a
3 changed files with 56 additions and 15 deletions
|
|
@ -97,6 +97,7 @@ async function canStartOpenCodeHost(opencodeBin, cwd, env) {
|
||||||
cwd,
|
cwd,
|
||||||
env,
|
env,
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
windowsHide: true,
|
||||||
});
|
});
|
||||||
let output = '';
|
let output = '';
|
||||||
let spawnError = '';
|
let spawnError = '';
|
||||||
|
|
@ -138,12 +139,17 @@ async function canStartOpenCodeHost(opencodeBin, cwd, env) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function stopChild(child) {
|
async function stopChild(child) {
|
||||||
return new Promise((resolve) => {
|
|
||||||
if (child.exitCode != null || child.killed) {
|
if (child.exitCode != null || child.killed) {
|
||||||
resolve();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'win32' && child.pid) {
|
||||||
|
await taskkillProcessTree(child.pid);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
if (child.exitCode == null) {
|
if (child.exitCode == null) {
|
||||||
child.kill('SIGKILL');
|
child.kill('SIGKILL');
|
||||||
|
|
@ -158,6 +164,34 @@ function stopChild(child) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function taskkillProcessTree(pid) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let done = false;
|
||||||
|
const finish = () => {
|
||||||
|
if (done) return;
|
||||||
|
done = true;
|
||||||
|
clearTimeout(timeout);
|
||||||
|
resolve();
|
||||||
|
};
|
||||||
|
const timeout = setTimeout(finish, 5_000);
|
||||||
|
timeout.unref?.();
|
||||||
|
try {
|
||||||
|
const taskkill = spawn(
|
||||||
|
path.join(process.env.SystemRoot ?? 'C:\\Windows', 'System32', 'taskkill.exe'),
|
||||||
|
['/T', '/F', '/PID', String(pid)],
|
||||||
|
{
|
||||||
|
stdio: 'ignore',
|
||||||
|
windowsHide: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
taskkill.once('error', finish);
|
||||||
|
taskkill.once('close', finish);
|
||||||
|
} catch {
|
||||||
|
finish();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function allocateLoopbackPort() {
|
function allocateLoopbackPort() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const server = net.createServer();
|
const server = net.createServer();
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,10 @@ import * as path from 'path';
|
||||||
import pidusage from 'pidusage';
|
import pidusage from 'pidusage';
|
||||||
import * as readline from 'readline';
|
import * as readline from 'readline';
|
||||||
|
|
||||||
|
// pidusage's Windows gwmi fallback needs a non-zero cache window to finish its
|
||||||
|
// initial two-sample pass. maxage: 0 can recurse forever on Windows.
|
||||||
|
const RUNTIME_PIDUSAGE_OPTIONS = process.platform === 'win32' ? { maxage: 1_000 } : { maxage: 0 };
|
||||||
|
|
||||||
import {
|
import {
|
||||||
ANTHROPIC_HELPER_MODE_COMPETING_AUTH_ENV_KEYS,
|
ANTHROPIC_HELPER_MODE_COMPETING_AUTH_ENV_KEYS,
|
||||||
type AnthropicTeamApiKeyHelperMaterial,
|
type AnthropicTeamApiKeyHelperMaterial,
|
||||||
|
|
@ -15298,7 +15302,7 @@ export class TeamProvisioningService {
|
||||||
let rssBytes = rssPid ? rssBytesByPid.get(rssPid) : undefined;
|
let rssBytes = rssPid ? rssBytesByPid.get(rssPid) : undefined;
|
||||||
if (rssBytes == null && isSharedOpenCodeHost && typeof rssPid === 'number' && rssPid > 0) {
|
if (rssBytes == null && isSharedOpenCodeHost && typeof rssPid === 'number' && rssPid > 0) {
|
||||||
try {
|
try {
|
||||||
const refreshedStat = await pidusage(rssPid, { maxage: 0 });
|
const refreshedStat = await pidusage(rssPid, RUNTIME_PIDUSAGE_OPTIONS);
|
||||||
if (Number.isFinite(refreshedStat.memory) && refreshedStat.memory >= 0) {
|
if (Number.isFinite(refreshedStat.memory) && refreshedStat.memory >= 0) {
|
||||||
rssBytesByPid.set(rssPid, refreshedStat.memory);
|
rssBytesByPid.set(rssPid, refreshedStat.memory);
|
||||||
rssBytes = refreshedStat.memory;
|
rssBytes = refreshedStat.memory;
|
||||||
|
|
@ -25558,7 +25562,7 @@ export class TeamProvisioningService {
|
||||||
}
|
}
|
||||||
|
|
||||||
const rssBytesByPid = new Map<number, number>();
|
const rssBytesByPid = new Map<number, number>();
|
||||||
const options = { maxage: 0 };
|
const options = RUNTIME_PIDUSAGE_OPTIONS;
|
||||||
try {
|
try {
|
||||||
const statsByPid = await pidusage(uniquePids, options);
|
const statsByPid = await pidusage(uniquePids, options);
|
||||||
for (const [rawPid, stat] of Object.entries(statsByPid)) {
|
for (const [rawPid, stat] of Object.entries(statsByPid)) {
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,9 @@ import {
|
||||||
} from '@features/tmux-installer/main';
|
} from '@features/tmux-installer/main';
|
||||||
import pidusage from 'pidusage';
|
import pidusage from 'pidusage';
|
||||||
|
|
||||||
|
const EXPECTED_RUNTIME_PIDUSAGE_OPTIONS =
|
||||||
|
process.platform === 'win32' ? { maxage: 1_000 } : { maxage: 0 };
|
||||||
|
|
||||||
function allowConsoleLogs() {
|
function allowConsoleLogs() {
|
||||||
vi.spyOn(console, 'error').mockImplementation(() => {});
|
vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
vi.spyOn(console, 'warn').mockImplementation(() => {});
|
||||||
|
|
@ -2490,7 +2493,7 @@ describe('TeamProvisioningService', () => {
|
||||||
|
|
||||||
const snapshot = await svc.getTeamAgentRuntimeSnapshot('runtime-team');
|
const snapshot = await svc.getTeamAgentRuntimeSnapshot('runtime-team');
|
||||||
|
|
||||||
expect(pidusage).toHaveBeenCalledWith([111, 222], { maxage: 0 });
|
expect(pidusage).toHaveBeenCalledWith([111, 222], EXPECTED_RUNTIME_PIDUSAGE_OPTIONS);
|
||||||
expect(snapshot.members['team-lead']).toMatchObject({
|
expect(snapshot.members['team-lead']).toMatchObject({
|
||||||
pid: 111,
|
pid: 111,
|
||||||
rssBytes: 123_000_000,
|
rssBytes: 123_000_000,
|
||||||
|
|
@ -2630,9 +2633,9 @@ describe('TeamProvisioningService', () => {
|
||||||
|
|
||||||
const snapshot = await svc.getTeamAgentRuntimeSnapshot('runtime-team');
|
const snapshot = await svc.getTeamAgentRuntimeSnapshot('runtime-team');
|
||||||
|
|
||||||
expect(pidusage).toHaveBeenNthCalledWith(1, [111, 222], { maxage: 0 });
|
expect(pidusage).toHaveBeenNthCalledWith(1, [111, 222], EXPECTED_RUNTIME_PIDUSAGE_OPTIONS);
|
||||||
expect(pidusage).toHaveBeenNthCalledWith(2, 111, { maxage: 0 });
|
expect(pidusage).toHaveBeenNthCalledWith(2, 111, EXPECTED_RUNTIME_PIDUSAGE_OPTIONS);
|
||||||
expect(pidusage).toHaveBeenNthCalledWith(3, 222, { maxage: 0 });
|
expect(pidusage).toHaveBeenNthCalledWith(3, 222, EXPECTED_RUNTIME_PIDUSAGE_OPTIONS);
|
||||||
expect(snapshot.members['team-lead']?.rssBytes).toBe(123_000_000);
|
expect(snapshot.members['team-lead']?.rssBytes).toBe(123_000_000);
|
||||||
expect(snapshot.members.alice?.rssBytes).toBe(456_000_000);
|
expect(snapshot.members.alice?.rssBytes).toBe(456_000_000);
|
||||||
});
|
});
|
||||||
|
|
@ -2744,7 +2747,7 @@ describe('TeamProvisioningService', () => {
|
||||||
|
|
||||||
const snapshot = await svc.getTeamAgentRuntimeSnapshot('nice-team');
|
const snapshot = await svc.getTeamAgentRuntimeSnapshot('nice-team');
|
||||||
|
|
||||||
expect(pidusage).toHaveBeenCalledWith([111, 333], { maxage: 0 });
|
expect(pidusage).toHaveBeenCalledWith([111, 333], EXPECTED_RUNTIME_PIDUSAGE_OPTIONS);
|
||||||
expect(snapshot.members.alice).toMatchObject({
|
expect(snapshot.members.alice).toMatchObject({
|
||||||
alive: true,
|
alive: true,
|
||||||
providerId: 'anthropic',
|
providerId: 'anthropic',
|
||||||
|
|
@ -3256,8 +3259,8 @@ describe('TeamProvisioningService', () => {
|
||||||
|
|
||||||
const snapshot = await svc.getTeamAgentRuntimeSnapshot('runtime-team');
|
const snapshot = await svc.getTeamAgentRuntimeSnapshot('runtime-team');
|
||||||
|
|
||||||
expect(pidusage).toHaveBeenCalledWith([111, 333], { maxage: 0 });
|
expect(pidusage).toHaveBeenCalledWith([111, 333], EXPECTED_RUNTIME_PIDUSAGE_OPTIONS);
|
||||||
expect(pidusage).toHaveBeenCalledWith(333, { maxage: 0 });
|
expect(pidusage).toHaveBeenCalledWith(333, EXPECTED_RUNTIME_PIDUSAGE_OPTIONS);
|
||||||
expect(snapshot.members.bob).toMatchObject({
|
expect(snapshot.members.bob).toMatchObject({
|
||||||
memberName: 'bob',
|
memberName: 'bob',
|
||||||
alive: false,
|
alive: false,
|
||||||
|
|
@ -3332,7 +3335,7 @@ describe('TeamProvisioningService', () => {
|
||||||
|
|
||||||
const snapshot = await svc.getTeamAgentRuntimeSnapshot('runtime-team');
|
const snapshot = await svc.getTeamAgentRuntimeSnapshot('runtime-team');
|
||||||
|
|
||||||
expect(pidusage).toHaveBeenCalledWith([333], { maxage: 0 });
|
expect(pidusage).toHaveBeenCalledWith([333], EXPECTED_RUNTIME_PIDUSAGE_OPTIONS);
|
||||||
expect(snapshot.members.bob).toMatchObject({
|
expect(snapshot.members.bob).toMatchObject({
|
||||||
memberName: 'bob',
|
memberName: 'bob',
|
||||||
alive: false,
|
alive: false,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue