vm-manager, api: one logger and one pathExists, from shared

vm-manager's own Logger, silentLogger and stderrLogger repeated the shared ones; it now takes them from
packages/shared and logs as the rest of the process does (prefixedStderrLogger). vm-manager and the API's VM driver
each had their own exists(): one pathExists in shared replaces both.
This commit is contained in:
feder-cr committed 2026-10-11 17:51:54 +02:00
1 parent ac26cec1bf
commit 528be98890
10 files changed
+29 -61

No files matched your search

+3 -11
View File
@@ -9,10 +9,10 @@
* - every start uses the newest `runtime-<version>.iso`, so a new version of
* our code reaches a Dot with a restart (section 3.3).
*/
import { access, readdir } from "node:fs/promises";
import { readdir } from "node:fs/promises";
import { join } from "node:path";
import { type ComputerDriver, type ComputerSpecInput, type ComputerState, type CreatedComputer, type GuestApi, type GuestEndpoint, type StartedComputer, type WaitForHealthOptions } from "@invisible-dots/scheduler";
import { type HealthAnswer, type HostPaths, type ImageKind, imageLabel, imageVersionOf, type Logger } from "@invisible-dots/shared";
import { type HealthAnswer, type HostPaths, type ImageKind, imageLabel, imageVersionOf, type Logger, pathExists } from "@invisible-dots/shared";
import { VmManager, type StartVmResult, type VmSpec } from "@invisible-dots/vm-manager";
/** Compare versions such as "2026.10.1" and "2026.9.3" numerically, part by part. */
@@ -112,7 +112,7 @@ export class VmManagerDriver implements ComputerDriver {
}
async state(dotId: string): Promise<ComputerState> {
const [info, exists] = await Promise.all([this.#vm.state(dotId), fileExists(this.#vm.paths.diskPath(dotId))]);
const [info, exists] = await Promise.all([this.#vm.state(dotId), pathExists(this.#vm.paths.diskPath(dotId))]);
return {
exists,
state: info.state,
@@ -130,14 +130,6 @@ export class VmManagerDriver implements ComputerDriver {
async close(): Promise<void> {}
}
async function fileExists(path: string): Promise<boolean> {
try {
await access(path);
return true;
} catch {
return false;
}
}
/** The driver `invisible-dots server` uses: VmManager over this host's QEMU and INVISIBLE_DOTS_HOME. */
export function createVmDriver(options: { env: Record<string, string | undefined>; paths: HostPaths; logger: Logger }): VmManagerDriver {
+2 -2
View File
@@ -12,8 +12,8 @@ import { join } from "node:path";
import { createTestDatabase, type TestDatabase } from "@invisible-dots/database/testing";
import { Scheduler } from "@invisible-dots/scheduler";
import { FAKE_MAX_IDENTITIES, FakeGuest, waitFor } from "@invisible-dots/scheduler/testing";
import { computerResources, hostPaths, type HostPaths, parseDotConfig, prefixedStderrLogger } from "@invisible-dots/shared";
import { silentLogger, VmManager } from "@invisible-dots/vm-manager";
import { computerResources, hostPaths, type HostPaths, parseDotConfig, prefixedStderrLogger, silentLogger } from "@invisible-dots/shared";
import { VmManager } from "@invisible-dots/vm-manager";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { FakeQemuHost, FakeRunner, type FakeVm } from "../../vm-manager/test/fakes.js";
import { compareVersions, latestImage, VmManagerDriver } from "../src/index.js";
+1 -2
View File
@@ -6,8 +6,7 @@
* and a stop closes the web client first, then the control plane.
*/
import type { ListenAddress, ListenSetting, StartServerOptions } from "@invisible-dots/api";
import { DEFAULT_WEB_LISTEN, ENV, type HostPaths } from "@invisible-dots/shared";
import type { Logger } from "@invisible-dots/vm-manager";
import { DEFAULT_WEB_LISTEN, ENV, type HostPaths, type Logger } from "@invisible-dots/shared";
import { locateWebBuild, type WebServer, type WebServerOptions } from "./web.js";
/** What `serve` needs from the packages that own the processes; tests pass fakes. */
+2 -1
View File
@@ -41,9 +41,10 @@ import {
type SecretsRequest,
type SkillListAnswer,
type ToolListAnswer,
silentLogger,
type Logger,
} from "@invisible-dots/shared";
import { GuestRequestError } from "./errors.js";
import { silentLogger, type Logger } from "./logger.js";
import { GUEST_PORT_HOST } from "./ports.js";
export interface GuestClientOptions {
-1
View File
@@ -76,7 +76,6 @@ export * from "./errors.js";
export * from "./guest-client.js";
export * from "./host-doctor.js";
export * from "./host.js";
export * from "./logger.js";
export * from "./ports.js";
export * from "./powershell.js";
export * from "./process-file.js";
-27
View File
@@ -1,27 +0,0 @@
/** A minimal structured logger; the API process passes its own (pino-compatible shape). */
export interface Logger {
debug(message: string, fields?: Record<string, unknown>): void;
info(message: string, fields?: Record<string, unknown>): void;
warn(message: string, fields?: Record<string, unknown>): void;
error(message: string, fields?: Record<string, unknown>): void;
}
function write(level: string, message: string, fields?: Record<string, unknown>): void {
const suffix = fields && Object.keys(fields).length > 0 ? ` ${JSON.stringify(fields)}` : "";
process.stderr.write(`[vm-manager] ${level} ${message}${suffix}\n`);
}
/** Logs info and above to stderr, one line each. */
export const stderrLogger: Logger = {
debug: () => {},
info: (message, fields) => write("info", message, fields),
warn: (message, fields) => write("warn", message, fields),
error: (message, fields) => write("error", message, fields),
};
export const silentLogger: Logger = {
debug: () => {},
info: () => {},
warn: () => {},
error: () => {},
};
+8 -15
View File
@@ -14,7 +14,7 @@
* restart the pid file stands in for it, checked as `processIsOurs` says.
*/
import { randomBytes } from "node:crypto";
import { access, appendFile, mkdir, open, readFile, rm, writeFile } from "node:fs/promises";
import { appendFile, mkdir, open, readFile, rm, writeFile } from "node:fs/promises";
import { isAbsolute, join, relative, resolve } from "node:path";
import {
allowlistedEnvironment,
@@ -23,9 +23,12 @@ import {
hostRestartedSince,
hostUptimeSeconds,
isFatalGuestError,
pathExists,
prefixedStderrLogger,
replaceFile,
type DotConfig,
type HostPaths,
type Logger,
type VmState,
} from "@invisible-dots/shared";
import {
@@ -40,7 +43,6 @@ import {
} from "./errors.js";
import { GuestClient, waitForGuestHealth, type GuestClientOptions, type WaitForGuestHealthOptions } from "./guest-client.js";
import { accelerator as hostAccelerator, findQemu, type Accelerator, type QemuInstallation } from "./host.js";
import { stderrLogger, type Logger } from "./logger.js";
import { pickFreePort, portListens } from "./ports.js";
import { readProcessFile, writeProcessFile, type ProcessFile } from "./process-file.js";
import { checkVmResources, CPU_MODEL, qemuArgs, qemuPathArg } from "./qemu-args.js";
@@ -183,15 +185,6 @@ function assertDotId(dotId: string): void {
}
}
async function exists(path: string): Promise<boolean> {
try {
await access(path);
return true;
} catch {
return false;
}
}
/** QEMU wrote this when it could not bind the host side of the port forward. */
const PORT_TAKEN = /Could not set up host forwarding rule/i;
@@ -257,7 +250,7 @@ export class VmManager {
this.options = options;
this.env = options.env ?? process.env;
this.paths = options.paths ?? hostPaths(this.env);
this.logger = options.logger ?? stderrLogger;
this.logger = options.logger ?? prefixedStderrLogger("vm-manager");
this.runner = options.runner ?? new NodeCommandRunner();
this.processes = options.processes ?? new NodeProcessControl();
this.sleep = options.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
@@ -372,7 +365,7 @@ export class VmManager {
["golden image", spec.goldenImage],
["runtime ISO", spec.runtimeImage],
] as const) {
if (!(await exists(path))) throw new VmManagerError(`${label} ${path} does not exist: build it with "invisible-dots image build" first`);
if (!(await pathExists(path))) throw new VmManagerError(`${label} ${path} does not exist: build it with "invisible-dots image build" first`);
}
}
@@ -411,7 +404,7 @@ export class VmManager {
await mkdir(dir, { recursive: true, mode: 0o700 });
const diskPath = this.paths.diskPath(spec.dotId);
let diskCreated = false;
if (await exists(diskPath)) {
if (await pathExists(diskPath)) {
this.logger.warn("overlay already exists, keeping it", { dotId: spec.dotId, disk: diskPath });
} else {
await this.qemuImg(["create", "-f", "qcow2", "-F", "qcow2", "-b", qemuPathArg("golden image", spec.goldenImage), qemuPathArg("disk", diskPath), String(spec.diskBytes)]);
@@ -540,7 +533,7 @@ export class VmManager {
}
await this.forget(dotId);
const diskPath = this.paths.diskPath(dotId);
if (!(await exists(diskPath))) throw new VmStateError(dotId, `${diskPath} does not exist; create the VM first`);
if (!(await pathExists(diskPath))) throw new VmStateError(dotId, `${diskPath} does not exist; create the VM first`);
await this.assertImages(spec);
await this.writeSeed(spec);
const qemu = await this.qemu();
+1 -2
View File
@@ -3,7 +3,7 @@ import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { createServer, type Server } from "node:net";
import { tmpdir, uptime } from "node:os";
import { join } from "node:path";
import { hostPaths, type HostPaths } from "@invisible-dots/shared";
import { hostPaths, type HostPaths, silentLogger } from "@invisible-dots/shared";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import {
AcceleratorUnavailableError,
@@ -18,7 +18,6 @@ import {
VmStateError,
generateToken,
qemuArgs,
silentLogger,
vmSpecFromConfig,
type VmManagerOptions,
type VmSpec,
+1
View File
@@ -1,6 +1,7 @@
export * from "./api-token.js";
export * from "./browser.js";
export * from "./files.js";
export * from "./path-exists.js";
export * from "./paths.js";
export * from "./sockets.js";
export * from "./stderr-logger.js";
+11
View File
@@ -0,0 +1,11 @@
import { access } from "node:fs/promises";
/** Whether something is at `path` that this process may see. Node-only. */
export async function pathExists(path: string): Promise<boolean> {
try {
await access(path);
return true;
} catch {
return false;
}
}