mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 21:07:01 -06:00
fix: close stale TUI clients before updates
This commit is contained in:
@@ -1,3 +1,9 @@
|
||||
import { theme } from "../../../packages/terminal-core/src/theme.js";
|
||||
import {
|
||||
formatLocalTuiPidList,
|
||||
listLocalTuiProcesses,
|
||||
terminateLocalTuiProcesses,
|
||||
} from "../../infra/local-tui-processes.js";
|
||||
import type { DevUpdateTarget } from "../../infra/update-dev-target.js";
|
||||
import type { ResolvedGlobalInstallTarget } from "../../infra/update-global.js";
|
||||
import type { UpdateRunResult } from "../../infra/update-runner.js";
|
||||
@@ -33,6 +39,38 @@ import {
|
||||
|
||||
const CLI_NAME = resolveCliName();
|
||||
|
||||
async function stopLocalTuiClientsBeforeMutableUpdate(params: { jsonMode: boolean }) {
|
||||
if (
|
||||
(process.env.VITEST || process.env.NODE_ENV === "test") &&
|
||||
process.env.OPENCLAW_ALLOW_TEST_LOCAL_TUI_PROCESS_MUTATION !== "1"
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const tuiProcesses = listLocalTuiProcesses();
|
||||
if (tuiProcesses.length === 0) {
|
||||
return;
|
||||
}
|
||||
const pids = formatLocalTuiPidList(tuiProcesses);
|
||||
if (!params.jsonMode) {
|
||||
defaultRuntime.log(
|
||||
theme.muted(
|
||||
`Closing local TUI clients before update so they do not load stale runtime chunks: ${pids}`,
|
||||
),
|
||||
);
|
||||
}
|
||||
const stopped = await terminateLocalTuiProcesses({ processes: tuiProcesses });
|
||||
if (!params.jsonMode) {
|
||||
if (stopped.stopped.length > 0) {
|
||||
defaultRuntime.log(theme.muted(`Stopped local TUI clients: ${stopped.stopped.join(", ")}`));
|
||||
}
|
||||
if (stopped.failed.length > 0) {
|
||||
defaultRuntime.log(
|
||||
theme.warn(`Could not stop local TUI clients: ${stopped.failed.join(", ")}`),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type MutableUpdateExecutionResult = {
|
||||
result: UpdateRunResult;
|
||||
preManagedServiceStop: PreManagedServiceStop | undefined;
|
||||
@@ -153,6 +191,8 @@ export async function executeMutableUpdate(params: {
|
||||
}
|
||||
};
|
||||
|
||||
await stopLocalTuiClientsBeforeMutableUpdate({ jsonMode: Boolean(params.opts.json) });
|
||||
|
||||
if (params.updateInstallKind === "package") {
|
||||
try {
|
||||
await stopManagedServiceBeforeMutableUpdate();
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
import "./doctor-whatsapp-responsiveness.js";
|
||||
|
||||
type LocalTuiProcess = { pid: number; command: string };
|
||||
type ProcessSignal = "SIGTERM" | "SIGKILL";
|
||||
type ProcessController = { kill(pid: number, signal: ProcessSignal | 0): boolean };
|
||||
|
||||
type TestApi = {
|
||||
listLocalTuiProcesses(): LocalTuiProcess[];
|
||||
terminateLocalTuiProcesses(params: {
|
||||
processes: LocalTuiProcess[];
|
||||
controller?: ProcessController;
|
||||
graceMs?: number;
|
||||
}): Promise<{ stopped: number[]; failed: number[] }>;
|
||||
};
|
||||
|
||||
function getTestApi(): TestApi {
|
||||
return (globalThis as Record<PropertyKey, unknown>)[
|
||||
Symbol.for("openclaw.doctorWhatsappResponsivenessTestApi")
|
||||
] as TestApi;
|
||||
}
|
||||
|
||||
export const listLocalTuiProcesses: TestApi["listLocalTuiProcesses"] = () =>
|
||||
getTestApi().listLocalTuiProcesses();
|
||||
|
||||
export const terminateLocalTuiProcesses: TestApi["terminateLocalTuiProcesses"] = (params) =>
|
||||
getTestApi().terminateLocalTuiProcesses(params);
|
||||
@@ -3,14 +3,6 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
|
||||
const noteMock = vi.hoisted(() => vi.fn());
|
||||
const spawnSyncMock = vi.hoisted(() => vi.fn());
|
||||
|
||||
vi.mock("node:child_process", async () => {
|
||||
const { mockNodeChildProcessSpawnSync } = await import("openclaw/plugin-sdk/test-node-mocks");
|
||||
return mockNodeChildProcessSpawnSync(spawnSyncMock, () =>
|
||||
vi.importActual<typeof import("node:child_process")>("node:child_process"),
|
||||
);
|
||||
});
|
||||
|
||||
vi.mock("../../packages/terminal-core/src/note.js", () => ({
|
||||
note: noteMock,
|
||||
@@ -18,81 +10,12 @@ vi.mock("../../packages/terminal-core/src/note.js", () => ({
|
||||
|
||||
const { collectWhatsappResponsivenessHealthFindings, noteWhatsappResponsivenessHealth } =
|
||||
await import("./doctor-whatsapp-responsiveness.js");
|
||||
const { listLocalTuiProcesses, terminateLocalTuiProcesses } =
|
||||
await import("./doctor-whatsapp-responsiveness.test-support.js");
|
||||
|
||||
describe("doctor WhatsApp responsiveness", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("lists only verified local TUI processes", () => {
|
||||
spawnSyncMock.mockReturnValue({
|
||||
status: 0,
|
||||
stdout: [
|
||||
" 101 openclaw-tui",
|
||||
" 102 /usr/bin/node /usr/lib/node_modules/openclaw/dist/index.js gateway --port 18789",
|
||||
" 103 openclaw channels",
|
||||
" 104 openclaw tui --local",
|
||||
" 105 /usr/bin/openclaw chat",
|
||||
" 106 helper --note 'openclaw tui'",
|
||||
" 107 openclaw-helper openclaw terminal",
|
||||
" 108 openclaw --flag tui",
|
||||
].join("\n"),
|
||||
});
|
||||
|
||||
if (process.platform === "win32") {
|
||||
expect(listLocalTuiProcesses()).toEqual([]);
|
||||
expect(spawnSyncMock).not.toHaveBeenCalled();
|
||||
} else {
|
||||
expect(listLocalTuiProcesses()).toEqual([
|
||||
{ pid: 101, command: "openclaw-tui" },
|
||||
{ pid: 104, command: "openclaw tui --local" },
|
||||
{ pid: 105, command: "/usr/bin/openclaw chat" },
|
||||
]);
|
||||
expect(spawnSyncMock).toHaveBeenCalledWith("ps", ["-axo", "pid=,command="], {
|
||||
encoding: "utf8",
|
||||
killSignal: "SIGKILL",
|
||||
timeout: 1_000,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("terminates stale local TUI processes with a kill fallback", async () => {
|
||||
const alive = new Set([101]);
|
||||
const signals: Array<[number, string | number]> = [];
|
||||
const controller = {
|
||||
kill: vi.fn((pid: number, signal: string | number) => {
|
||||
signals.push([pid, signal]);
|
||||
if (signal === "SIGKILL") {
|
||||
alive.delete(pid);
|
||||
return true;
|
||||
}
|
||||
if (signal === 0) {
|
||||
if (alive.has(pid)) {
|
||||
return true;
|
||||
}
|
||||
throw new Error("gone");
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
};
|
||||
|
||||
await expect(
|
||||
terminateLocalTuiProcesses({
|
||||
processes: [{ pid: 101, command: "openclaw-tui" }],
|
||||
controller,
|
||||
graceMs: 0,
|
||||
}),
|
||||
).resolves.toEqual({ stopped: [101], failed: [] });
|
||||
expect(signals).toEqual([
|
||||
[101, "SIGTERM"],
|
||||
[101, 0],
|
||||
[101, "SIGKILL"],
|
||||
[101, 0],
|
||||
]);
|
||||
});
|
||||
|
||||
it("warns and repairs local TUI pressure when WhatsApp is enabled and the gateway is degraded", async () => {
|
||||
const terminate = vi.fn().mockResolvedValue({ stopped: [101], failed: [] });
|
||||
const cfg = { channels: { whatsapp: { enabled: true } } } as OpenClawConfig;
|
||||
|
||||
@@ -1,86 +1,17 @@
|
||||
/** Doctor hints for WhatsApp responsiveness when local TUI clients block gateway work. */
|
||||
import { spawnSync } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { note } from "../../packages/terminal-core/src/note.js";
|
||||
import { formatCliCommand } from "../cli/command-format.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import type { HealthFinding } from "../flows/health-checks.js";
|
||||
import {
|
||||
formatLocalTuiPidList,
|
||||
listLocalTuiProcesses,
|
||||
terminateLocalTuiProcesses,
|
||||
type LocalTuiProcess,
|
||||
} from "../infra/local-tui-processes.js";
|
||||
import type { StatusSummary } from "../status/types.js";
|
||||
import { sleep } from "../utils/sleep.js";
|
||||
|
||||
type LocalTuiProcess = {
|
||||
pid: number;
|
||||
command: string;
|
||||
};
|
||||
|
||||
type ProcessSignal = "SIGTERM" | "SIGKILL";
|
||||
|
||||
type ProcessController = {
|
||||
kill: (pid: number, signal: ProcessSignal | 0) => boolean;
|
||||
};
|
||||
|
||||
const LOCAL_TUI_SUBCOMMANDS = new Set(["chat", "terminal", "tui"]);
|
||||
const WHATSAPP_RESPONSIVENESS_CHECK_ID = "core/doctor/whatsapp-responsiveness";
|
||||
const LOCAL_TUI_PROCESS_PROBE_TIMEOUT_MS = 1_000;
|
||||
|
||||
function tokenizeCommandLine(command: string): string[] {
|
||||
return command.trim().split(/\s+/u).filter(Boolean);
|
||||
}
|
||||
|
||||
function normalizeExecutableName(value: string | undefined): string {
|
||||
return path.basename(value ?? "").replace(/\.exe$/iu, "");
|
||||
}
|
||||
|
||||
function isLocalTuiCommand(command: string): boolean {
|
||||
const argv = tokenizeCommandLine(command);
|
||||
const executable = normalizeExecutableName(argv[0]);
|
||||
if (executable === "openclaw-tui") {
|
||||
return true;
|
||||
}
|
||||
return executable === "openclaw" && LOCAL_TUI_SUBCOMMANDS.has(argv[1] ?? "");
|
||||
}
|
||||
|
||||
function parsePsPidLine(line: string): LocalTuiProcess | null {
|
||||
const match = line.match(/^\s*(\d+)\s+(.+)$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const pid = Number(match[1]);
|
||||
if (!Number.isFinite(pid) || pid <= 0 || pid === process.pid) {
|
||||
return null;
|
||||
}
|
||||
const command = match[2]?.trim() ?? "";
|
||||
if (!isLocalTuiCommand(command)) {
|
||||
return null;
|
||||
}
|
||||
return { pid, command };
|
||||
}
|
||||
|
||||
/** Lists local OpenClaw TUI processes that can contend with gateway responsiveness. */
|
||||
function listLocalTuiProcesses(): LocalTuiProcess[] {
|
||||
if (process.platform === "win32") {
|
||||
return [];
|
||||
}
|
||||
const ps = spawnSync("ps", ["-axo", "pid=,command="], {
|
||||
encoding: "utf8",
|
||||
killSignal: "SIGKILL",
|
||||
timeout: LOCAL_TUI_PROCESS_PROBE_TIMEOUT_MS,
|
||||
});
|
||||
if (ps.error || ps.status !== 0 || typeof ps.stdout !== "string") {
|
||||
return [];
|
||||
}
|
||||
const seen = new Set<number>();
|
||||
const processes: LocalTuiProcess[] = [];
|
||||
for (const line of ps.stdout.split(/\r?\n/)) {
|
||||
const proc = parsePsPidLine(line);
|
||||
if (!proc || seen.has(proc.pid)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(proc.pid);
|
||||
processes.push(proc);
|
||||
}
|
||||
return processes;
|
||||
}
|
||||
|
||||
function hasWhatsappEnabled(cfg: OpenClawConfig): boolean {
|
||||
const whatsapp = cfg.channels?.whatsapp;
|
||||
@@ -94,10 +25,6 @@ function hasWhatsappEnabled(cfg: OpenClawConfig): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
function formatPidList(processes: LocalTuiProcess[]): string {
|
||||
return processes.map((proc) => String(proc.pid)).join(", ");
|
||||
}
|
||||
|
||||
/** Collects read-only structured findings for WhatsApp responsiveness pressure. */
|
||||
export function collectWhatsappResponsivenessHealthFindings(params: {
|
||||
cfg: OpenClawConfig;
|
||||
@@ -118,7 +45,7 @@ export function collectWhatsappResponsivenessHealthFindings(params: {
|
||||
return [];
|
||||
}
|
||||
|
||||
const pids = formatPidList(tuiProcesses);
|
||||
const pids = formatLocalTuiPidList(tuiProcesses);
|
||||
return [
|
||||
{
|
||||
checkId: WHATSAPP_RESPONSIVENESS_CHECK_ID,
|
||||
@@ -135,64 +62,6 @@ export function collectWhatsappResponsivenessHealthFindings(params: {
|
||||
];
|
||||
}
|
||||
|
||||
function isProcessAlive(controller: ProcessController, pid: number): boolean {
|
||||
try {
|
||||
controller.kill(pid, 0);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Terminates local TUI processes with SIGTERM, then SIGKILL for remaining pids. */
|
||||
async function terminateLocalTuiProcesses(params: {
|
||||
processes: LocalTuiProcess[];
|
||||
controller?: ProcessController;
|
||||
graceMs?: number;
|
||||
}): Promise<{ stopped: number[]; failed: number[] }> {
|
||||
const controller = params.controller ?? process;
|
||||
const graceMs = Math.max(0, params.graceMs ?? 500);
|
||||
const stopped: number[] = [];
|
||||
const failed: number[] = [];
|
||||
|
||||
for (const proc of params.processes) {
|
||||
try {
|
||||
controller.kill(proc.pid, "SIGTERM");
|
||||
} catch {
|
||||
// Already gone is success for this repair.
|
||||
}
|
||||
}
|
||||
if (graceMs > 0) {
|
||||
await sleep(graceMs);
|
||||
}
|
||||
for (const proc of params.processes) {
|
||||
if (!isProcessAlive(controller, proc.pid)) {
|
||||
stopped.push(proc.pid);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
controller.kill(proc.pid, "SIGKILL");
|
||||
} catch {
|
||||
// Already gone is still success.
|
||||
}
|
||||
if (isProcessAlive(controller, proc.pid)) {
|
||||
failed.push(proc.pid);
|
||||
} else {
|
||||
stopped.push(proc.pid);
|
||||
}
|
||||
}
|
||||
return { stopped, failed };
|
||||
}
|
||||
|
||||
if (process.env.VITEST || process.env.NODE_ENV === "test") {
|
||||
(globalThis as Record<PropertyKey, unknown>)[
|
||||
Symbol.for("openclaw.doctorWhatsappResponsivenessTestApi")
|
||||
] = {
|
||||
listLocalTuiProcesses,
|
||||
terminateLocalTuiProcesses,
|
||||
};
|
||||
}
|
||||
|
||||
/** Emits WhatsApp responsiveness warnings and optionally stops contending local TUI clients. */
|
||||
export async function noteWhatsappResponsivenessHealth(params: {
|
||||
cfg: OpenClawConfig;
|
||||
@@ -215,7 +84,7 @@ export async function noteWhatsappResponsivenessHealth(params: {
|
||||
[
|
||||
"Gateway event loop is degraded while local TUI clients are running.",
|
||||
"WhatsApp replies can queue behind TUI startup/session refresh work.",
|
||||
`Local TUI pids: ${formatPidList(tuiProcesses)}`,
|
||||
`Local TUI pids: ${formatLocalTuiPidList(tuiProcesses)}`,
|
||||
].join("\n"),
|
||||
);
|
||||
if (params.shouldRepair) {
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
listLocalTuiProcesses,
|
||||
parseLocalTuiProcessLine,
|
||||
terminateLocalTuiProcesses,
|
||||
} from "./local-tui-processes.js";
|
||||
|
||||
describe("local TUI processes", () => {
|
||||
it("parses only verified local TUI command lines", () => {
|
||||
expect(parseLocalTuiProcessLine(" 101 openclaw-tui", 999)).toEqual({
|
||||
pid: 101,
|
||||
command: "openclaw-tui",
|
||||
});
|
||||
expect(parseLocalTuiProcessLine(" 104 openclaw tui --local", 999)).toEqual({
|
||||
pid: 104,
|
||||
command: "openclaw tui --local",
|
||||
});
|
||||
expect(parseLocalTuiProcessLine(" 105 /usr/bin/openclaw chat", 999)).toEqual({
|
||||
pid: 105,
|
||||
command: "/usr/bin/openclaw chat",
|
||||
});
|
||||
expect(parseLocalTuiProcessLine(" 102 openclaw gateway --port 18789", 999)).toBeNull();
|
||||
expect(parseLocalTuiProcessLine(" 106 helper --note 'openclaw tui'", 999)).toBeNull();
|
||||
expect(parseLocalTuiProcessLine(" 107 openclaw-helper openclaw terminal", 999)).toBeNull();
|
||||
expect(parseLocalTuiProcessLine(" 108 openclaw --flag tui", 999)).toBeNull();
|
||||
expect(parseLocalTuiProcessLine(" 999 openclaw tui", 999)).toBeNull();
|
||||
});
|
||||
|
||||
it("lists local TUI processes from ps output", () => {
|
||||
const spawnSync = vi.fn().mockReturnValue({
|
||||
status: 0,
|
||||
stdout: [
|
||||
" 101 openclaw-tui",
|
||||
" 101 openclaw-tui",
|
||||
" 102 /usr/bin/node /usr/lib/node_modules/openclaw/dist/index.js gateway --port 18789",
|
||||
" 104 openclaw tui --local",
|
||||
" 105 /usr/bin/openclaw chat",
|
||||
].join("\n"),
|
||||
});
|
||||
|
||||
expect(
|
||||
listLocalTuiProcesses({
|
||||
platform: "darwin",
|
||||
currentPid: 999,
|
||||
spawnSync,
|
||||
}),
|
||||
).toEqual([
|
||||
{ pid: 101, command: "openclaw-tui" },
|
||||
{ pid: 104, command: "openclaw tui --local" },
|
||||
{ pid: 105, command: "/usr/bin/openclaw chat" },
|
||||
]);
|
||||
expect(spawnSync).toHaveBeenCalledWith("ps", ["-axo", "pid=,command="], {
|
||||
encoding: "utf8",
|
||||
killSignal: "SIGKILL",
|
||||
timeout: 1_000,
|
||||
});
|
||||
});
|
||||
|
||||
it("skips process probing on Windows", () => {
|
||||
const spawnSync = vi.fn();
|
||||
|
||||
expect(listLocalTuiProcesses({ platform: "win32", spawnSync })).toEqual([]);
|
||||
expect(spawnSync).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("terminates stale local TUI processes with a kill fallback", async () => {
|
||||
const alive = new Set([101]);
|
||||
const signals: Array<[number, string | number]> = [];
|
||||
const controller = {
|
||||
kill: vi.fn((pid: number, signal: string | number) => {
|
||||
signals.push([pid, signal]);
|
||||
if (signal === "SIGKILL") {
|
||||
alive.delete(pid);
|
||||
return true;
|
||||
}
|
||||
if (signal === 0) {
|
||||
if (alive.has(pid)) {
|
||||
return true;
|
||||
}
|
||||
throw new Error("gone");
|
||||
}
|
||||
return true;
|
||||
}),
|
||||
};
|
||||
|
||||
await expect(
|
||||
terminateLocalTuiProcesses({
|
||||
processes: [{ pid: 101, command: "openclaw-tui" }],
|
||||
controller,
|
||||
graceMs: 0,
|
||||
}),
|
||||
).resolves.toEqual({ stopped: [101], failed: [] });
|
||||
expect(signals).toEqual([
|
||||
[101, "SIGTERM"],
|
||||
[101, 0],
|
||||
[101, "SIGKILL"],
|
||||
[101, 0],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,146 @@
|
||||
import { spawnSync, type SpawnSyncOptionsWithStringEncoding } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { sleep } from "../utils/sleep.js";
|
||||
|
||||
export type LocalTuiProcess = {
|
||||
pid: number;
|
||||
command: string;
|
||||
};
|
||||
|
||||
type ProcessSignal = "SIGTERM" | "SIGKILL";
|
||||
|
||||
type ProcessController = {
|
||||
kill: (pid: number, signal: ProcessSignal | 0) => boolean;
|
||||
};
|
||||
|
||||
type PsResult = {
|
||||
error?: Error;
|
||||
status: number | null;
|
||||
stdout?: string;
|
||||
};
|
||||
|
||||
const LOCAL_TUI_SUBCOMMANDS = new Set(["chat", "terminal", "tui"]);
|
||||
const LOCAL_TUI_PROCESS_PROBE_TIMEOUT_MS = 1_000;
|
||||
|
||||
function tokenizeCommandLine(command: string): string[] {
|
||||
return command.trim().split(/\s+/u).filter(Boolean);
|
||||
}
|
||||
|
||||
function normalizeExecutableName(value: string | undefined): string {
|
||||
return path.basename(value ?? "").replace(/\.exe$/iu, "");
|
||||
}
|
||||
|
||||
export function isLocalTuiCommand(command: string): boolean {
|
||||
const argv = tokenizeCommandLine(command);
|
||||
const executable = normalizeExecutableName(argv[0]);
|
||||
if (executable === "openclaw-tui") {
|
||||
return true;
|
||||
}
|
||||
return executable === "openclaw" && LOCAL_TUI_SUBCOMMANDS.has(argv[1] ?? "");
|
||||
}
|
||||
|
||||
export function parseLocalTuiProcessLine(line: string, currentPid = process.pid) {
|
||||
const match = line.match(/^\s*(\d+)\s+(.+)$/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
const pid = Number(match[1]);
|
||||
if (!Number.isFinite(pid) || pid <= 0 || pid === currentPid) {
|
||||
return null;
|
||||
}
|
||||
const command = match[2]?.trim() ?? "";
|
||||
if (!isLocalTuiCommand(command)) {
|
||||
return null;
|
||||
}
|
||||
return { pid, command };
|
||||
}
|
||||
|
||||
/** Lists local OpenClaw TUI processes whose in-memory chunk graph may outlive an update. */
|
||||
export function listLocalTuiProcesses(
|
||||
params: {
|
||||
platform?: NodeJS.Platform;
|
||||
currentPid?: number;
|
||||
spawnSync?: (
|
||||
command: string,
|
||||
args: string[],
|
||||
options: SpawnSyncOptionsWithStringEncoding,
|
||||
) => PsResult;
|
||||
} = {},
|
||||
): LocalTuiProcess[] {
|
||||
if ((params.platform ?? process.platform) === "win32") {
|
||||
return [];
|
||||
}
|
||||
const spawnSyncImpl = params.spawnSync ?? spawnSync;
|
||||
const ps = spawnSyncImpl("ps", ["-axo", "pid=,command="], {
|
||||
encoding: "utf8",
|
||||
killSignal: "SIGKILL",
|
||||
timeout: LOCAL_TUI_PROCESS_PROBE_TIMEOUT_MS,
|
||||
});
|
||||
if (ps.error || ps.status !== 0 || typeof ps.stdout !== "string") {
|
||||
return [];
|
||||
}
|
||||
const seen = new Set<number>();
|
||||
const processes: LocalTuiProcess[] = [];
|
||||
for (const line of ps.stdout.split(/\r?\n/)) {
|
||||
const proc = parseLocalTuiProcessLine(line, params.currentPid);
|
||||
if (!proc || seen.has(proc.pid)) {
|
||||
continue;
|
||||
}
|
||||
seen.add(proc.pid);
|
||||
processes.push(proc);
|
||||
}
|
||||
return processes;
|
||||
}
|
||||
|
||||
function isProcessAlive(controller: ProcessController, pid: number): boolean {
|
||||
try {
|
||||
controller.kill(pid, 0);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/** Terminates local TUI processes with SIGTERM, then SIGKILL for remaining pids. */
|
||||
export async function terminateLocalTuiProcesses(params: {
|
||||
processes: LocalTuiProcess[];
|
||||
controller?: ProcessController;
|
||||
graceMs?: number;
|
||||
}): Promise<{ stopped: number[]; failed: number[] }> {
|
||||
const controller = params.controller ?? process;
|
||||
const graceMs = Math.max(0, params.graceMs ?? 500);
|
||||
const stopped: number[] = [];
|
||||
const failed: number[] = [];
|
||||
|
||||
for (const proc of params.processes) {
|
||||
try {
|
||||
controller.kill(proc.pid, "SIGTERM");
|
||||
} catch {
|
||||
// Already gone is success for this repair.
|
||||
}
|
||||
}
|
||||
if (graceMs > 0) {
|
||||
await sleep(graceMs);
|
||||
}
|
||||
for (const proc of params.processes) {
|
||||
if (!isProcessAlive(controller, proc.pid)) {
|
||||
stopped.push(proc.pid);
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
controller.kill(proc.pid, "SIGKILL");
|
||||
} catch {
|
||||
// Already gone is still success.
|
||||
}
|
||||
if (isProcessAlive(controller, proc.pid)) {
|
||||
failed.push(proc.pid);
|
||||
} else {
|
||||
stopped.push(proc.pid);
|
||||
}
|
||||
}
|
||||
return { stopped, failed };
|
||||
}
|
||||
|
||||
export function formatLocalTuiPidList(processes: readonly LocalTuiProcess[]) {
|
||||
return processes.map((proc) => String(proc.pid)).join(", ");
|
||||
}
|
||||
Reference in New Issue
Block a user