mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 09:18:38 +00:00
Browser/Logging: share default openclaw tmp dir resolver
This commit is contained in:
@@ -1,8 +1,8 @@
|
|||||||
import type { Page } from "playwright-core";
|
import type { Page } from "playwright-core";
|
||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
|
||||||
import {
|
import {
|
||||||
ensurePageState,
|
ensurePageState,
|
||||||
getPageForTargetId,
|
getPageForTargetId,
|
||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
function buildTempDownloadPath(fileName: string): string {
|
function buildTempDownloadPath(fileName: string): string {
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
const safeName = fileName.trim() ? fileName.trim() : "download.bin";
|
const safeName = fileName.trim() ? fileName.trim() : "download.bin";
|
||||||
return path.join(os.tmpdir(), "openclaw", "downloads", `${id}-${safeName}`);
|
return path.join(resolvePreferredOpenClawTmpDir(), "downloads", `${id}-${safeName}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPageDownloadWaiter(page: Page, timeoutMs: number) {
|
function createPageDownloadWaiter(page: Page, timeoutMs: number) {
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ const sessionMocks = vi.hoisted(() => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("./pw-session.js", () => sessionMocks);
|
vi.mock("./pw-session.js", () => sessionMocks);
|
||||||
|
const tmpDirMocks = vi.hoisted(() => ({
|
||||||
|
resolvePreferredOpenClawTmpDir: vi.fn(() => "/tmp/openclaw"),
|
||||||
|
}));
|
||||||
|
vi.mock("../infra/tmp-openclaw-dir.js", () => tmpDirMocks);
|
||||||
|
|
||||||
async function importModule() {
|
async function importModule() {
|
||||||
return await import("./pw-tools-core.js");
|
return await import("./pw-tools-core.js");
|
||||||
@@ -47,6 +51,10 @@ describe("pw-tools-core", () => {
|
|||||||
for (const fn of Object.values(sessionMocks)) {
|
for (const fn of Object.values(sessionMocks)) {
|
||||||
fn.mockClear();
|
fn.mockClear();
|
||||||
}
|
}
|
||||||
|
for (const fn of Object.values(tmpDirMocks)) {
|
||||||
|
fn.mockClear();
|
||||||
|
}
|
||||||
|
tmpDirMocks.resolvePreferredOpenClawTmpDir.mockReturnValue("/tmp/openclaw");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("waits for the next download and saves it", async () => {
|
it("waits for the next download and saves it", async () => {
|
||||||
@@ -125,6 +133,43 @@ describe("pw-tools-core", () => {
|
|||||||
expect(saveAs).toHaveBeenCalledWith(targetPath);
|
expect(saveAs).toHaveBeenCalledWith(targetPath);
|
||||||
expect(res.path).toBe(targetPath);
|
expect(res.path).toBe(targetPath);
|
||||||
});
|
});
|
||||||
|
it("uses preferred tmp dir when waiting for download without explicit path", async () => {
|
||||||
|
let downloadHandler: ((download: unknown) => void) | undefined;
|
||||||
|
const on = vi.fn((event: string, handler: (download: unknown) => void) => {
|
||||||
|
if (event === "download") {
|
||||||
|
downloadHandler = handler;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const off = vi.fn();
|
||||||
|
|
||||||
|
const saveAs = vi.fn(async () => {});
|
||||||
|
const download = {
|
||||||
|
url: () => "https://example.com/file.bin",
|
||||||
|
suggestedFilename: () => "file.bin",
|
||||||
|
saveAs,
|
||||||
|
};
|
||||||
|
|
||||||
|
tmpDirMocks.resolvePreferredOpenClawTmpDir.mockReturnValue("/tmp/openclaw-preferred");
|
||||||
|
currentPage = { on, off };
|
||||||
|
|
||||||
|
const mod = await importModule();
|
||||||
|
const p = mod.waitForDownloadViaPlaywright({
|
||||||
|
cdpUrl: "http://127.0.0.1:18792",
|
||||||
|
targetId: "T1",
|
||||||
|
timeoutMs: 1000,
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.resolve();
|
||||||
|
downloadHandler?.(download);
|
||||||
|
|
||||||
|
const res = await p;
|
||||||
|
const outPath = vi.mocked(saveAs).mock.calls[0]?.[0];
|
||||||
|
expect(typeof outPath).toBe("string");
|
||||||
|
expect(String(outPath)).toContain("/tmp/openclaw-preferred/downloads/");
|
||||||
|
expect(String(outPath)).toContain("-file.bin");
|
||||||
|
expect(res.path).toContain("/tmp/openclaw-preferred/downloads/");
|
||||||
|
expect(tmpDirMocks.resolvePreferredOpenClawTmpDir).toHaveBeenCalled();
|
||||||
|
});
|
||||||
it("waits for a matching response and returns its body", async () => {
|
it("waits for a matching response and returns its body", async () => {
|
||||||
let responseHandler: ((resp: unknown) => void) | undefined;
|
let responseHandler: ((resp: unknown) => void) | undefined;
|
||||||
const on = vi.fn((event: string, handler: (resp: unknown) => void) => {
|
const on = vi.fn((event: string, handler: (resp: unknown) => void) => {
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
import crypto from "node:crypto";
|
import crypto from "node:crypto";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import type { BrowserRouteContext } from "../server-context.js";
|
import type { BrowserRouteContext } from "../server-context.js";
|
||||||
import type { BrowserRouteRegistrar } from "./types.js";
|
import type { BrowserRouteRegistrar } from "./types.js";
|
||||||
|
import { resolvePreferredOpenClawTmpDir } from "../../infra/tmp-openclaw-dir.js";
|
||||||
import { handleRouteError, readBody, requirePwAi, resolveProfileContext } from "./agent.shared.js";
|
import { handleRouteError, readBody, requirePwAi, resolveProfileContext } from "./agent.shared.js";
|
||||||
import { toBoolean, toStringOrEmpty } from "./utils.js";
|
import { toBoolean, toStringOrEmpty } from "./utils.js";
|
||||||
|
|
||||||
|
const DEFAULT_TRACE_DIR = resolvePreferredOpenClawTmpDir();
|
||||||
|
|
||||||
export function registerBrowserAgentDebugRoutes(
|
export function registerBrowserAgentDebugRoutes(
|
||||||
app: BrowserRouteRegistrar,
|
app: BrowserRouteRegistrar,
|
||||||
ctx: BrowserRouteContext,
|
ctx: BrowserRouteContext,
|
||||||
@@ -132,7 +134,7 @@ export function registerBrowserAgentDebugRoutes(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const id = crypto.randomUUID();
|
const id = crypto.randomUUID();
|
||||||
const dir = path.join(os.tmpdir(), "openclaw");
|
const dir = DEFAULT_TRACE_DIR;
|
||||||
await fs.mkdir(dir, { recursive: true });
|
await fs.mkdir(dir, { recursive: true });
|
||||||
const tracePath = out.trim() || path.join(dir, `browser-trace-${id}.zip`);
|
const tracePath = out.trim() || path.join(dir, `browser-trace-${id}.zip`);
|
||||||
await pw.traceStopViaPlaywright({
|
await pw.traceStopViaPlaywright({
|
||||||
|
|||||||
@@ -57,7 +57,10 @@ export function registerBrowserFilesAndDownloadsCommands(
|
|||||||
browser
|
browser
|
||||||
.command("waitfordownload")
|
.command("waitfordownload")
|
||||||
.description("Wait for the next download (and save it)")
|
.description("Wait for the next download (and save it)")
|
||||||
.argument("[path]", "Save path (default: os.tmpdir()/openclaw/downloads/...)")
|
.argument(
|
||||||
|
"[path]",
|
||||||
|
"Save path (default: /tmp/openclaw/downloads/...; fallback: os.tmpdir()/openclaw/downloads/...)",
|
||||||
|
)
|
||||||
.option("--target-id <id>", "CDP target id (or unique prefix)")
|
.option("--target-id <id>", "CDP target id (or unique prefix)")
|
||||||
.option(
|
.option(
|
||||||
"--timeout-ms <ms>",
|
"--timeout-ms <ms>",
|
||||||
|
|||||||
64
src/infra/tmp-openclaw-dir.test.ts
Normal file
64
src/infra/tmp-openclaw-dir.test.ts
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import path from "node:path";
|
||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
import { POSIX_OPENCLAW_TMP_DIR, resolvePreferredOpenClawTmpDir } from "./tmp-openclaw-dir.js";
|
||||||
|
|
||||||
|
describe("resolvePreferredOpenClawTmpDir", () => {
|
||||||
|
it("prefers /tmp/openclaw when it already exists and is writable", () => {
|
||||||
|
const accessSync = vi.fn();
|
||||||
|
const statSync = vi.fn(() => ({ isDirectory: () => true }));
|
||||||
|
const tmpdir = vi.fn(() => "/var/fallback");
|
||||||
|
|
||||||
|
const resolved = resolvePreferredOpenClawTmpDir({ accessSync, statSync, tmpdir });
|
||||||
|
|
||||||
|
expect(statSync).toHaveBeenCalledTimes(1);
|
||||||
|
expect(accessSync).toHaveBeenCalledTimes(1);
|
||||||
|
expect(resolved).toBe(POSIX_OPENCLAW_TMP_DIR);
|
||||||
|
expect(tmpdir).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prefers /tmp/openclaw when it does not exist but /tmp is writable", () => {
|
||||||
|
const accessSync = vi.fn();
|
||||||
|
const statSync = vi.fn(() => {
|
||||||
|
const err = new Error("missing") as Error & { code?: string };
|
||||||
|
err.code = "ENOENT";
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
const tmpdir = vi.fn(() => "/var/fallback");
|
||||||
|
|
||||||
|
const resolved = resolvePreferredOpenClawTmpDir({ accessSync, statSync, tmpdir });
|
||||||
|
|
||||||
|
expect(resolved).toBe(POSIX_OPENCLAW_TMP_DIR);
|
||||||
|
expect(accessSync).toHaveBeenCalledWith("/tmp", expect.any(Number));
|
||||||
|
expect(tmpdir).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to os.tmpdir()/openclaw when /tmp/openclaw is not a directory", () => {
|
||||||
|
const accessSync = vi.fn();
|
||||||
|
const statSync = vi.fn(() => ({ isDirectory: () => false }));
|
||||||
|
const tmpdir = vi.fn(() => "/var/fallback");
|
||||||
|
|
||||||
|
const resolved = resolvePreferredOpenClawTmpDir({ accessSync, statSync, tmpdir });
|
||||||
|
|
||||||
|
expect(resolved).toBe(path.join("/var/fallback", "openclaw"));
|
||||||
|
expect(tmpdir).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to os.tmpdir()/openclaw when /tmp is not writable", () => {
|
||||||
|
const accessSync = vi.fn((target: string) => {
|
||||||
|
if (target === "/tmp") {
|
||||||
|
throw new Error("read-only");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const statSync = vi.fn(() => {
|
||||||
|
const err = new Error("missing") as Error & { code?: string };
|
||||||
|
err.code = "ENOENT";
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
const tmpdir = vi.fn(() => "/var/fallback");
|
||||||
|
|
||||||
|
const resolved = resolvePreferredOpenClawTmpDir({ accessSync, statSync, tmpdir });
|
||||||
|
|
||||||
|
expect(resolved).toBe(path.join("/var/fallback", "openclaw"));
|
||||||
|
expect(tmpdir).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
50
src/infra/tmp-openclaw-dir.ts
Normal file
50
src/infra/tmp-openclaw-dir.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
export const POSIX_OPENCLAW_TMP_DIR = "/tmp/openclaw";
|
||||||
|
|
||||||
|
type ResolvePreferredOpenClawTmpDirOptions = {
|
||||||
|
accessSync?: (path: string, mode?: number) => void;
|
||||||
|
statSync?: (path: string) => { isDirectory(): boolean };
|
||||||
|
tmpdir?: () => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type MaybeNodeError = { code?: string };
|
||||||
|
|
||||||
|
function isNodeErrorWithCode(err: unknown, code: string): err is MaybeNodeError {
|
||||||
|
return (
|
||||||
|
typeof err === "object" &&
|
||||||
|
err !== null &&
|
||||||
|
"code" in err &&
|
||||||
|
(err as MaybeNodeError).code === code
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolvePreferredOpenClawTmpDir(
|
||||||
|
options: ResolvePreferredOpenClawTmpDirOptions = {},
|
||||||
|
): string {
|
||||||
|
const accessSync = options.accessSync ?? fs.accessSync;
|
||||||
|
const statSync = options.statSync ?? fs.statSync;
|
||||||
|
const tmpdir = options.tmpdir ?? os.tmpdir;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const preferred = statSync(POSIX_OPENCLAW_TMP_DIR);
|
||||||
|
if (!preferred.isDirectory()) {
|
||||||
|
return path.join(tmpdir(), "openclaw");
|
||||||
|
}
|
||||||
|
accessSync(POSIX_OPENCLAW_TMP_DIR, fs.constants.W_OK | fs.constants.X_OK);
|
||||||
|
return POSIX_OPENCLAW_TMP_DIR;
|
||||||
|
} catch (err) {
|
||||||
|
if (!isNodeErrorWithCode(err, "ENOENT")) {
|
||||||
|
return path.join(tmpdir(), "openclaw");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
accessSync("/tmp", fs.constants.W_OK | fs.constants.X_OK);
|
||||||
|
return POSIX_OPENCLAW_TMP_DIR;
|
||||||
|
} catch {
|
||||||
|
return path.join(tmpdir(), "openclaw");
|
||||||
|
}
|
||||||
|
}
|
||||||
20
src/logging/logger.import-side-effects.test.ts
Normal file
20
src/logging/logger.import-side-effects.test.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
describe("logger import side effects", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.resetModules();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not mkdir at import time", async () => {
|
||||||
|
const mkdirSpy = vi.spyOn(fs, "mkdirSync");
|
||||||
|
|
||||||
|
await import("./logger.js");
|
||||||
|
|
||||||
|
expect(mkdirSpy).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,26 +1,15 @@
|
|||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import { createRequire } from "node:module";
|
import { createRequire } from "node:module";
|
||||||
import os from "node:os";
|
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { Logger as TsLogger } from "tslog";
|
import { Logger as TsLogger } from "tslog";
|
||||||
import type { OpenClawConfig } from "../config/types.js";
|
import type { OpenClawConfig } from "../config/types.js";
|
||||||
import type { ConsoleStyle } from "./console.js";
|
import type { ConsoleStyle } from "./console.js";
|
||||||
|
import { resolvePreferredOpenClawTmpDir } from "../infra/tmp-openclaw-dir.js";
|
||||||
import { readLoggingConfig } from "./config.js";
|
import { readLoggingConfig } from "./config.js";
|
||||||
import { type LogLevel, levelToMinLevel, normalizeLogLevel } from "./levels.js";
|
import { type LogLevel, levelToMinLevel, normalizeLogLevel } from "./levels.js";
|
||||||
import { loggingState } from "./state.js";
|
import { loggingState } from "./state.js";
|
||||||
|
|
||||||
// Prefer /tmp/openclaw so macOS Debug UI and docs match, but fall back to
|
export const DEFAULT_LOG_DIR = resolvePreferredOpenClawTmpDir();
|
||||||
// os.tmpdir() on platforms where /tmp is read-only (e.g. Termux/Android).
|
|
||||||
function resolveDefaultLogDir(): string {
|
|
||||||
try {
|
|
||||||
fs.mkdirSync("/tmp/openclaw", { recursive: true });
|
|
||||||
return "/tmp/openclaw";
|
|
||||||
} catch {
|
|
||||||
return path.join(os.tmpdir(), "openclaw");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const DEFAULT_LOG_DIR = resolveDefaultLogDir();
|
|
||||||
export const DEFAULT_LOG_FILE = path.join(DEFAULT_LOG_DIR, "openclaw.log"); // legacy single-file path
|
export const DEFAULT_LOG_FILE = path.join(DEFAULT_LOG_DIR, "openclaw.log"); // legacy single-file path
|
||||||
|
|
||||||
const LOG_PREFIX = "openclaw";
|
const LOG_PREFIX = "openclaw";
|
||||||
|
|||||||
Reference in New Issue
Block a user