mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:30:43 +00:00
refactor: centralize restart log conventions
This commit is contained in:
@@ -28,11 +28,13 @@ vi.mock("../../daemon/inspect.js", () => ({
|
||||
renderGatewayServiceCleanupHints: () => [],
|
||||
}));
|
||||
|
||||
vi.mock("../../daemon/launchd.js", () => ({
|
||||
vi.mock("../../daemon/restart-logs.js", () => ({
|
||||
resolveGatewayLogPaths: () => ({
|
||||
logDir: "/tmp",
|
||||
stdoutPath: "/tmp/gateway.out.log",
|
||||
stderrPath: "/tmp/gateway.err.log",
|
||||
}),
|
||||
resolveGatewayRestartLogPath: () => "/tmp/gateway-restart.log",
|
||||
}));
|
||||
|
||||
vi.mock("../../daemon/systemd-hints.js", () => ({
|
||||
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
resolveGatewaySystemdServiceName,
|
||||
} from "../../daemon/constants.js";
|
||||
import { renderGatewayServiceCleanupHints } from "../../daemon/inspect.js";
|
||||
import { resolveGatewayLogPaths } from "../../daemon/launchd.js";
|
||||
import { resolveGatewayLogPaths, resolveGatewayRestartLogPath } from "../../daemon/restart-logs.js";
|
||||
import {
|
||||
isSystemdUnavailableDetail,
|
||||
renderSystemdUnavailableHints,
|
||||
@@ -288,20 +288,23 @@ export function printDaemonStatus(status: DaemonStatus, opts: { json: boolean })
|
||||
defaultRuntime.error(
|
||||
errorText(`Gateway port ${status.port.port} is not listening (service appears running).`),
|
||||
);
|
||||
const serviceEnv = { ...process.env, ...service.command?.environment };
|
||||
if (status.lastError) {
|
||||
defaultRuntime.error(`${errorText("Last gateway error:")} ${status.lastError}`);
|
||||
}
|
||||
if (process.platform === "linux") {
|
||||
const env = service.command?.environment ?? process.env;
|
||||
const unit = resolveGatewaySystemdServiceName(env.OPENCLAW_PROFILE);
|
||||
const unit = resolveGatewaySystemdServiceName(serviceEnv.OPENCLAW_PROFILE);
|
||||
defaultRuntime.error(
|
||||
errorText(`Logs: journalctl --user -u ${unit}.service -n 200 --no-pager`),
|
||||
);
|
||||
} else if (process.platform === "darwin") {
|
||||
const logs = resolveGatewayLogPaths(service.command?.environment ?? process.env);
|
||||
const logs = resolveGatewayLogPaths(serviceEnv);
|
||||
defaultRuntime.error(`${errorText("Logs:")} ${shortenHomePath(logs.stdoutPath)}`);
|
||||
defaultRuntime.error(`${errorText("Errors:")} ${shortenHomePath(logs.stderrPath)}`);
|
||||
}
|
||||
defaultRuntime.error(
|
||||
`${errorText("Restart log:")} ${shortenHomePath(resolveGatewayRestartLogPath(serviceEnv))}`,
|
||||
);
|
||||
spacer();
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
import { formatConfigIssueLines } from "../../config/issue-format.js";
|
||||
import { asResolvedSourceConfig, asRuntimeConfig } from "../../config/materialize.js";
|
||||
import { resolveGatewayInstallEntrypoint } from "../../daemon/gateway-entrypoint.js";
|
||||
import { resolveGatewayRestartLogPath } from "../../daemon/restart-logs.js";
|
||||
import { resolveGatewayService } from "../../daemon/service.js";
|
||||
import { nodeVersionSatisfiesEngine } from "../../infra/runtime-guard.js";
|
||||
import {
|
||||
@@ -715,6 +716,9 @@ async function maybeRestartService(params: {
|
||||
for (const line of renderRestartDiagnostics(health)) {
|
||||
defaultRuntime.log(theme.muted(line));
|
||||
}
|
||||
defaultRuntime.log(
|
||||
theme.muted(`Restart log: ${resolveGatewayRestartLogPath(process.env)}`),
|
||||
);
|
||||
defaultRuntime.log(
|
||||
theme.muted(
|
||||
`Run \`${replaceCliName(formatCliCommand("openclaw gateway status --deep"), CLI_NAME)}\` for details.`,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { ProgressReporter } from "../../cli/progress.js";
|
||||
|
||||
vi.mock("../../daemon/launchd.js", () => ({
|
||||
vi.mock("../../daemon/restart-logs.js", () => ({
|
||||
resolveGatewayLogPaths: () => {
|
||||
throw new Error("skip log tail");
|
||||
},
|
||||
resolveGatewayRestartLogPath: () => "/tmp/gateway-restart.log",
|
||||
}));
|
||||
|
||||
vi.mock("./gateway.js", () => ({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ProgressReporter } from "../../cli/progress.js";
|
||||
import { formatConfigIssueLine } from "../../config/issue-format.js";
|
||||
import { resolveGatewayLogPaths } from "../../daemon/launchd.js";
|
||||
import { resolveGatewayLogPaths, resolveGatewayRestartLogPath } from "../../daemon/restart-logs.js";
|
||||
import {
|
||||
formatPortDiagnostics,
|
||||
isDualStackLoopbackGatewayListeners,
|
||||
@@ -218,9 +218,11 @@ export async function appendStatusAllDiagnosis(params: {
|
||||
})();
|
||||
if (logPaths) {
|
||||
params.progress.setLabel("Reading logs…");
|
||||
const [stderrTail, stdoutTail] = await Promise.all([
|
||||
const restartLogPath = resolveGatewayRestartLogPath(process.env);
|
||||
const [stderrTail, stdoutTail, restartTail] = await Promise.all([
|
||||
readFileTailLines(logPaths.stderrPath, 40).catch(() => []),
|
||||
readFileTailLines(logPaths.stdoutPath, 40).catch(() => []),
|
||||
readFileTailLines(restartLogPath, 30).catch(() => []),
|
||||
]);
|
||||
if (stderrTail.length > 0 || stdoutTail.length > 0) {
|
||||
lines.push("");
|
||||
@@ -234,6 +236,13 @@ export async function appendStatusAllDiagnosis(params: {
|
||||
lines.push(` ${muted(line)}`);
|
||||
}
|
||||
}
|
||||
if (restartTail.length > 0) {
|
||||
lines.push("");
|
||||
lines.push(muted(`Gateway restart attempts (tail): ${restartLogPath}`));
|
||||
for (const line of summarizeLogTail(restartTail, { maxLines: 16 }).map(redactSecrets)) {
|
||||
lines.push(` ${muted(line)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
params.progress.tick();
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { resolveGatewayLogPaths } from "./launchd.js";
|
||||
import { resolveGatewayLogPaths } from "./restart-logs.js";
|
||||
|
||||
const GATEWAY_LOG_ERROR_PATTERNS = [
|
||||
/refusing to bind gateway/i,
|
||||
|
||||
@@ -20,7 +20,8 @@ import {
|
||||
scheduleDetachedLaunchdRestartHandoff,
|
||||
} from "./launchd-restart-handoff.js";
|
||||
import { formatLine, toPosixPath, writeFormattedLines } from "./output.js";
|
||||
import { resolveGatewayStateDir, resolveHomeDir } from "./paths.js";
|
||||
import { resolveHomeDir } from "./paths.js";
|
||||
import { resolveGatewayLogPaths } from "./restart-logs.js";
|
||||
import { parseKeyValueOutput } from "./runtime-parse.js";
|
||||
import type { GatewayServiceRuntime } from "./service-runtime.js";
|
||||
import type {
|
||||
@@ -65,21 +66,6 @@ export function resolveLaunchAgentPlistPath(env: GatewayServiceEnv): string {
|
||||
return resolveLaunchAgentPlistPathForLabel(env, label);
|
||||
}
|
||||
|
||||
export function resolveGatewayLogPaths(env: GatewayServiceEnv): {
|
||||
logDir: string;
|
||||
stdoutPath: string;
|
||||
stderrPath: string;
|
||||
} {
|
||||
const stateDir = resolveGatewayStateDir(env);
|
||||
const logDir = path.join(stateDir, "logs");
|
||||
const prefix = env.OPENCLAW_LOG_PREFIX?.trim() || "gateway";
|
||||
return {
|
||||
logDir,
|
||||
stdoutPath: path.join(logDir, `${prefix}.log`),
|
||||
stderrPath: path.join(logDir, `${prefix}.err.log`),
|
||||
};
|
||||
}
|
||||
|
||||
export async function readLaunchAgentProgramArguments(
|
||||
env: GatewayServiceEnv,
|
||||
): Promise<GatewayServiceCommandConfig | null> {
|
||||
|
||||
59
src/daemon/restart-logs.test.ts
Normal file
59
src/daemon/restart-logs.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
GATEWAY_RESTART_LOG_FILENAME,
|
||||
renderCmdRestartLogSetup,
|
||||
renderPosixRestartLogSetup,
|
||||
resolveGatewayLogPaths,
|
||||
resolveGatewayRestartLogPath,
|
||||
} from "./restart-logs.js";
|
||||
|
||||
describe("restart log conventions", () => {
|
||||
it("resolves profile-aware gateway logs and restart attempts together", () => {
|
||||
const env = {
|
||||
HOME: "/Users/test",
|
||||
OPENCLAW_PROFILE: "work",
|
||||
};
|
||||
|
||||
expect(resolveGatewayLogPaths(env)).toEqual({
|
||||
logDir: "/Users/test/.openclaw-work/logs",
|
||||
stdoutPath: "/Users/test/.openclaw-work/logs/gateway.log",
|
||||
stderrPath: "/Users/test/.openclaw-work/logs/gateway.err.log",
|
||||
});
|
||||
expect(resolveGatewayRestartLogPath(env)).toBe(
|
||||
`/Users/test/.openclaw-work/logs/${GATEWAY_RESTART_LOG_FILENAME}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("honors OPENCLAW_STATE_DIR for restart attempts", () => {
|
||||
const env = {
|
||||
HOME: "/Users/test",
|
||||
OPENCLAW_STATE_DIR: "/tmp/openclaw-state",
|
||||
};
|
||||
|
||||
expect(resolveGatewayRestartLogPath(env)).toBe(
|
||||
`/tmp/openclaw-state/logs/${GATEWAY_RESTART_LOG_FILENAME}`,
|
||||
);
|
||||
});
|
||||
|
||||
it("renders best-effort POSIX log setup with escaped paths", () => {
|
||||
const setup = renderPosixRestartLogSetup({
|
||||
HOME: "/Users/test's",
|
||||
});
|
||||
|
||||
expect(setup).toContain("mkdir -p '/Users/test'\\''s/.openclaw/logs' 2>/dev/null || true");
|
||||
expect(setup).toContain(
|
||||
"exec >>'/Users/test'\\''s/.openclaw/logs/gateway-restart.log' 2>&1 || true",
|
||||
);
|
||||
});
|
||||
|
||||
it("renders CMD log setup with quoted paths", () => {
|
||||
const setup = renderCmdRestartLogSetup({
|
||||
USERPROFILE: "C:\\Users\\Test User",
|
||||
});
|
||||
|
||||
expect(setup.quotedLogPath).toBe('"C:\\Users\\Test User/.openclaw/logs/gateway-restart.log"');
|
||||
expect(setup.lines).toContain(
|
||||
'if not exist "C:\\Users\\Test User/.openclaw/logs" mkdir "C:\\Users\\Test User/.openclaw/logs" >nul 2>&1',
|
||||
);
|
||||
});
|
||||
});
|
||||
53
src/daemon/restart-logs.ts
Normal file
53
src/daemon/restart-logs.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import path from "node:path";
|
||||
import { quoteCmdScriptArg } from "./cmd-argv.js";
|
||||
import { resolveGatewayStateDir } from "./paths.js";
|
||||
import type { GatewayServiceEnv } from "./service-types.js";
|
||||
|
||||
export const GATEWAY_RESTART_LOG_FILENAME = "gateway-restart.log";
|
||||
|
||||
export function resolveGatewayLogPaths(env: GatewayServiceEnv): {
|
||||
logDir: string;
|
||||
stdoutPath: string;
|
||||
stderrPath: string;
|
||||
} {
|
||||
const stateDir = resolveGatewayStateDir(env);
|
||||
const logDir = path.join(stateDir, "logs");
|
||||
const prefix = env.OPENCLAW_LOG_PREFIX?.trim() || "gateway";
|
||||
return {
|
||||
logDir,
|
||||
stdoutPath: path.join(logDir, `${prefix}.log`),
|
||||
stderrPath: path.join(logDir, `${prefix}.err.log`),
|
||||
};
|
||||
}
|
||||
|
||||
export function resolveGatewayRestartLogPath(env: GatewayServiceEnv): string {
|
||||
return path.join(resolveGatewayLogPaths(env).logDir, GATEWAY_RESTART_LOG_FILENAME);
|
||||
}
|
||||
|
||||
export function shellEscapeRestartLogValue(value: string): string {
|
||||
return value.replace(/'/g, "'\\''");
|
||||
}
|
||||
|
||||
export function renderPosixRestartLogSetup(env: GatewayServiceEnv): string {
|
||||
const logDir = path.dirname(resolveGatewayRestartLogPath(env));
|
||||
const logPath = resolveGatewayRestartLogPath(env);
|
||||
return `mkdir -p '${shellEscapeRestartLogValue(logDir)}' 2>/dev/null || true
|
||||
exec >>'${shellEscapeRestartLogValue(logPath)}' 2>&1 || true`;
|
||||
}
|
||||
|
||||
export function renderCmdRestartLogSetup(env: GatewayServiceEnv): {
|
||||
lines: string[];
|
||||
quotedLogPath: string;
|
||||
} {
|
||||
const logPath = resolveGatewayRestartLogPath(env);
|
||||
const logDir = path.dirname(logPath);
|
||||
const quotedLogDir = quoteCmdScriptArg(logDir);
|
||||
const quotedLogPath = quoteCmdScriptArg(logPath);
|
||||
return {
|
||||
quotedLogPath,
|
||||
lines: [
|
||||
`if not exist ${quotedLogDir} mkdir ${quotedLogDir} >nul 2>&1`,
|
||||
`>> ${quotedLogPath} 2>&1 echo [%DATE% %TIME%] openclaw restart log initialized`,
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -16,6 +16,7 @@ describe("buildPlatformRuntimeLogHints", () => {
|
||||
).toEqual([
|
||||
"Launchd stdout (if installed): /tmp/openclaw-state/logs/gateway.log",
|
||||
"Launchd stderr (if installed): /tmp/openclaw-state/logs/gateway.err.log",
|
||||
"Restart attempts: /tmp/openclaw-state/logs/gateway-restart.log",
|
||||
]);
|
||||
});
|
||||
|
||||
@@ -23,17 +24,29 @@ describe("buildPlatformRuntimeLogHints", () => {
|
||||
expect(
|
||||
buildPlatformRuntimeLogHints({
|
||||
platform: "linux",
|
||||
env: {
|
||||
OPENCLAW_STATE_DIR: "/tmp/openclaw-state",
|
||||
},
|
||||
systemdServiceName: "openclaw-gateway",
|
||||
windowsTaskName: "OpenClaw Gateway",
|
||||
}),
|
||||
).toEqual(["Logs: journalctl --user -u openclaw-gateway.service -n 200 --no-pager"]);
|
||||
).toEqual([
|
||||
"Logs: journalctl --user -u openclaw-gateway.service -n 200 --no-pager",
|
||||
"Restart attempts: /tmp/openclaw-state/logs/gateway-restart.log",
|
||||
]);
|
||||
expect(
|
||||
buildPlatformRuntimeLogHints({
|
||||
platform: "win32",
|
||||
env: {
|
||||
OPENCLAW_STATE_DIR: "/tmp/openclaw-state",
|
||||
},
|
||||
systemdServiceName: "openclaw-gateway",
|
||||
windowsTaskName: "OpenClaw Gateway",
|
||||
}),
|
||||
).toEqual(['Logs: schtasks /Query /TN "OpenClaw Gateway" /V /FO LIST']);
|
||||
).toEqual([
|
||||
'Logs: schtasks /Query /TN "OpenClaw Gateway" /V /FO LIST',
|
||||
"Restart attempts: /tmp/openclaw-state/logs/gateway-restart.log",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { resolveGatewayLogPaths } from "./launchd.js";
|
||||
import { toPosixPath } from "./output.js";
|
||||
import { resolveGatewayLogPaths, resolveGatewayRestartLogPath } from "./restart-logs.js";
|
||||
|
||||
function toDarwinDisplayPath(value: string): string {
|
||||
return toPosixPath(value).replace(/^[A-Za-z]:/, "");
|
||||
@@ -12,19 +12,26 @@ export function buildPlatformRuntimeLogHints(params: {
|
||||
windowsTaskName: string;
|
||||
}): string[] {
|
||||
const platform = params.platform ?? process.platform;
|
||||
const env = params.env ?? process.env;
|
||||
const env = { ...process.env, ...params.env };
|
||||
if (platform === "darwin") {
|
||||
const logs = resolveGatewayLogPaths(env);
|
||||
return [
|
||||
`Launchd stdout (if installed): ${toDarwinDisplayPath(logs.stdoutPath)}`,
|
||||
`Launchd stderr (if installed): ${toDarwinDisplayPath(logs.stderrPath)}`,
|
||||
`Restart attempts: ${toDarwinDisplayPath(resolveGatewayRestartLogPath(env))}`,
|
||||
];
|
||||
}
|
||||
if (platform === "linux") {
|
||||
return [`Logs: journalctl --user -u ${params.systemdServiceName}.service -n 200 --no-pager`];
|
||||
return [
|
||||
`Logs: journalctl --user -u ${params.systemdServiceName}.service -n 200 --no-pager`,
|
||||
`Restart attempts: ${resolveGatewayRestartLogPath(env)}`,
|
||||
];
|
||||
}
|
||||
if (platform === "win32") {
|
||||
return [`Logs: schtasks /Query /TN "${params.windowsTaskName}" /V /FO LIST`];
|
||||
return [
|
||||
`Logs: schtasks /Query /TN "${params.windowsTaskName}" /V /FO LIST`,
|
||||
`Restart attempts: ${resolveGatewayRestartLogPath(env)}`,
|
||||
];
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -1,12 +1,17 @@
|
||||
import { beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const resolveGatewayLogPathsMock = vi.fn(() => ({
|
||||
logDir: "C:\\tmp\\openclaw-state\\logs",
|
||||
stdoutPath: "C:\\tmp\\openclaw-state\\logs\\gateway.log",
|
||||
stderrPath: "C:\\tmp\\openclaw-state\\logs\\gateway.err.log",
|
||||
}));
|
||||
const resolveGatewayRestartLogPathMock = vi.fn(
|
||||
() => "C:\\tmp\\openclaw-state\\logs\\gateway-restart.log",
|
||||
);
|
||||
|
||||
vi.mock("./launchd.js", () => ({
|
||||
vi.mock("./restart-logs.js", () => ({
|
||||
resolveGatewayLogPaths: resolveGatewayLogPathsMock,
|
||||
resolveGatewayRestartLogPath: resolveGatewayRestartLogPathMock,
|
||||
}));
|
||||
|
||||
let buildPlatformRuntimeLogHints: typeof import("./runtime-hints.js").buildPlatformRuntimeLogHints;
|
||||
@@ -26,6 +31,7 @@ describe("buildPlatformRuntimeLogHints", () => {
|
||||
).toEqual([
|
||||
"Launchd stdout (if installed): /tmp/openclaw-state/logs/gateway.log",
|
||||
"Launchd stderr (if installed): /tmp/openclaw-state/logs/gateway.err.log",
|
||||
"Restart attempts: /tmp/openclaw-state/logs/gateway-restart.log",
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user