Files
rea/src/application/ClientConfigPath.ts
T
42e1a17d6e fix(setup): preserve concurrent client configuration edits (#4855)
* fix(setup): preserve concurrent client configuration edits

* fix(setup): normalize configuration transaction queue paths

* fix(setup): report concurrent snapshot read conflicts

* test(setup): isolate Pi workflows from installed Hopper

* test: align inherited fixtures with current analysis contracts

* test(ida): assert search recovery instead of diagnostic prose

* fix(setup): share configuration queue keys across directory aliases

Resolve parent-directory aliases for regular and missing configuration
paths so a directory symlink and its real directory serialize setup
instead of racing the same file.

Co-authored-by: morluto <morluto@users.noreply.github.com>

---------

Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: morluto <morluto@users.noreply.github.com>
2026-10-23 05:06:13 +08:00

96 lines
3.2 KiB
TypeScript

import { lstat, realpath } from "node:fs/promises";
import { basename, dirname, join, resolve } from "node:path";
interface ClientConfigPathStats {
readonly uid?: number;
isFile?(): boolean;
isSymbolicLink(): boolean;
}
interface ClientConfigPathFileSystem {
lstat(path: string): Promise<ClientConfigPathStats>;
realpath?(path: string): Promise<string>;
}
const systemFileSystem: ClientConfigPathFileSystem = { lstat, realpath };
/**
* Resolve one stable transaction path for a configuration file.
* An existing user-owned config symlink resolves to its target file.
* Parent-directory aliases resolve for regular and missing files, so a
* directory symlink and its real directory share one queue key. Missing
* paths stay eligible for first-run creation.
*/
export const resolveClientConfigTransactionPath = async (
requestedPath: string,
fileSystem: ClientConfigPathFileSystem = systemFileSystem,
): Promise<string | undefined> => {
let requestedStats: ClientConfigPathStats | undefined;
try {
requestedStats = await fileSystem.lstat(requestedPath);
} catch (cause: unknown) {
if (!isMissing(cause)) return undefined;
}
if (requestedStats?.isSymbolicLink() === true)
return resolveOwnedConfigSymlink(requestedPath, requestedStats, fileSystem);
return canonicalRegularConfigPath(requestedPath, fileSystem);
};
const resolveOwnedConfigSymlink = async (
requestedPath: string,
requestedStats: ClientConfigPathStats,
fileSystem: ClientConfigPathFileSystem,
): Promise<string | undefined> => {
const currentUid =
typeof process.getuid === "function" ? process.getuid() : undefined;
if (
currentUid === undefined ||
requestedStats.uid !== currentUid ||
fileSystem.realpath === undefined
)
return undefined;
try {
const canonicalPath = await fileSystem.realpath(requestedPath);
const targetStats = await fileSystem.lstat(canonicalPath);
return targetStats.isFile?.() === true && targetStats.uid === currentUid
? canonicalPath
: undefined;
} catch (cause: unknown) {
// best-effort cleanup: unreadable or vanishing config paths mean no registration.
void cause;
return undefined;
}
};
/**
* Resolve parent-directory aliases, including when the file or an intermediate
* directory is absent. A realpath failure keeps the requested spelling.
*/
const canonicalRegularConfigPath = async (
requestedPath: string,
fileSystem: ClientConfigPathFileSystem,
): Promise<string> => {
if (fileSystem.realpath === undefined) return requestedPath;
let ancestor = resolve(requestedPath);
const missing: string[] = [];
while (true) {
try {
return join(await fileSystem.realpath(ancestor), ...missing);
} catch (cause: unknown) {
if (!isAbsentPath(cause) || dirname(ancestor) === ancestor)
return requestedPath;
missing.unshift(basename(ancestor));
ancestor = dirname(ancestor);
}
}
};
const isMissing = (cause: unknown): boolean =>
cause instanceof Error && "code" in cause && cause.code === "ENOENT";
const isAbsentPath = (cause: unknown): boolean =>
cause instanceof Error &&
"code" in cause &&
(cause.code === "ENOENT" || cause.code === "ENOTDIR");