fix(coding-agent/launch): handle EISDIR from realpath on drive roots

fs.realpath throws EISDIR on Windows drive roots (e.g. R:\), but
canonicalProjectDir in launch/presence.ts and launch/client.ts only
recovered ENOENT, aborting startup. Both now fall back to path.resolve()
on EISDIR, matching the existing ENOENT handling.

Fixes #5708
This commit is contained in:
roboomp
2026-07-16 13:55:05 +00:00
parent 6ae7cdbf97
commit eb149d1b0a
4 changed files with 56 additions and 4 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- Fixed a startup crash on Windows when running from a drive root (e.g. `R:\`): `fs.realpath` throws `EISDIR` there, but `canonicalProjectDir` in `launch/presence.ts` and `launch/client.ts` only recovered `ENOENT`. It now also falls back to `path.resolve()` on `EISDIR` ([#5708](https://github.com/can1357/oh-my-pi/issues/5708) by [@ve3xone](https://github.com/ve3xone)).
## [17.0.1] - 2026-07-16
### Changed
+2 -2
View File
@@ -2,7 +2,7 @@ import * as fs from "node:fs/promises";
import * as net from "node:net";
import * as os from "node:os";
import * as path from "node:path";
import { isEexist, isEnoent, postmortem } from "@oh-my-pi/pi-utils";
import { isEexist, isEisdir, isEnoent, postmortem } from "@oh-my-pi/pi-utils";
import { resolveWorkerSpawnCmd, workerEnvFromParent } from "../subprocess/worker-client";
import { daemonBrokerEndpoint, daemonRuntimeDir } from "./paths";
import {
@@ -49,7 +49,7 @@ async function canonicalProjectDir(projectDir: string): Promise<string> {
try {
return await fs.realpath(resolved);
} catch (error) {
if (isEnoent(error)) return resolved;
if (isEnoent(error) || isEisdir(error)) return resolved;
throw error;
}
}
+2 -2
View File
@@ -1,6 +1,6 @@
import * as fs from "node:fs/promises";
import * as path from "node:path";
import { isEnoent, postmortem } from "@oh-my-pi/pi-utils";
import { isEisdir, isEnoent, postmortem } from "@oh-my-pi/pi-utils";
import { daemonRuntimeDir } from "./paths";
const CLIENTS_DIR = "clients";
@@ -15,7 +15,7 @@ async function canonicalProjectDir(projectDir: string): Promise<string> {
try {
return await fs.realpath(resolved);
} catch (error) {
if (isEnoent(error)) return resolved;
if (isEnoent(error) || isEisdir(error)) return resolved;
throw error;
}
}
@@ -0,0 +1,48 @@
import { afterEach, describe, expect, it, vi } from "bun:test";
import type { PathLike } from "node:fs";
import * as fs from "node:fs/promises";
import * as os from "node:os";
import * as path from "node:path";
import { registerDaemonProjectPresence } from "../../src/launch/presence";
describe("daemon presence canonicalProjectDir EISDIR fallback", () => {
const originalRealpath = fs.realpath.bind(fs);
afterEach(() => {
vi.restoreAllMocks();
});
it("falls back to the resolved path when realpath throws EISDIR", async () => {
const projectDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-eisdir-fallback-"));
const runtimeDir = path.join(projectDir, "runtime");
const resolvedProjectDir = path.resolve(projectDir);
let realpathCalls = 0;
vi.spyOn(fs, "realpath").mockImplementation((async (p: PathLike) => {
if (path.resolve(String(p)) === resolvedProjectDir) {
realpathCalls++;
const err = new Error("EISDIR: illegal operation on a directory") as NodeJS.ErrnoException;
err.code = "EISDIR";
err.errno = -21;
err.syscall = "lstat";
err.path = `R:${path.sep}`;
throw err;
}
return originalRealpath(p);
}) as typeof fs.realpath);
try {
const presence = await registerDaemonProjectPresence(projectDir, runtimeDir);
expect(typeof presence.close).toBe("function");
expect(realpathCalls).toBe(1);
const clientsDir = path.join(runtimeDir, "clients");
const entries = await fs.readdir(clientsDir);
expect(entries).toHaveLength(1);
await presence.close();
} finally {
await fs.rm(projectDir, { recursive: true, force: true });
}
});
});