fix: use fs backend for host apply-patch

This commit is contained in:
Codex
2026-06-26 18:12:44 +00:00
parent c8fb148db3
commit 590feb7d9f
7 changed files with 175 additions and 20 deletions
+102 -14
View File
@@ -6,6 +6,7 @@ import { join } from "node:path";
import { type UniDeskConfig, repoRoot } from "./config";
import {
ApplyPatchV2Error,
applyPatchV2RemoteCommand,
decodeApplyPatchV2BulkRead,
formatApplyPatchV2BulkReplacementPayload,
isApplyPatchV2HelpArgs,
@@ -13,6 +14,7 @@ import {
type ApplyPatchV2BulkReplacementWritePlan,
type ApplyPatchV2Executor,
type ApplyPatchV2FileSystem,
type ApplyPatchV2RemoteOperation,
} from "./apply-patch-v2";
import {
isSshFileTransferOperation,
@@ -2912,16 +2914,18 @@ type WindowsApplyPatchFsOperation =
| "delete";
const windowsApplyPatchWriteB64ChunkChars = 12_000;
const posixApplyPatchWriteB64ChunkChars = 12_000;
export function createWindowsApplyPatchFileSystem(invocation: ParsedSshInvocation, runRemoteCommand: (remoteCommand: string, input?: string) => Promise<SshCaptureResult>): ApplyPatchV2FileSystem {
async function checked(operation: WindowsApplyPatchFsOperation, args: string[], input?: string): Promise<SshCaptureResult> {
const command = buildWindowsPowerShellInvocation(windowsApplyPatchFsScript(invocation.route.workspace, operation, args));
type PosixApplyPatchFsOperation = Exclude<ApplyPatchV2RemoteOperation, "write-b64-argv">;
export function createPosixApplyPatchFileSystem(invocation: ParsedSshInvocation, runRemoteCommand: (command: string[], input?: string) => Promise<SshCaptureResult>): ApplyPatchV2FileSystem {
async function checked(operation: PosixApplyPatchFsOperation, args: string[], input?: string): Promise<SshCaptureResult> {
const startedAtMs = Date.now();
let result: SshCaptureResult;
try {
result = await runRemoteCommand(command, input);
result = await runRemoteCommand(applyPatchV2RemoteCommand(operation, args), input);
} catch (error) {
throw new ApplyPatchV2Error(`windows apply-patch fs operation failed: ${operation}`, windowsApplyPatchFsFailureDetails({
throw new ApplyPatchV2Error(`posix apply-patch fs operation failed: ${operation}`, applyPatchFsFailureDetails({
operation,
args,
input,
@@ -2931,7 +2935,85 @@ export function createWindowsApplyPatchFileSystem(invocation: ParsedSshInvocatio
}));
}
if (result.exitCode === 0) return result;
throw new ApplyPatchV2Error(`windows apply-patch fs operation failed: ${operation}`, windowsApplyPatchFsFailureDetails({
throw new ApplyPatchV2Error(`posix apply-patch fs operation failed: ${operation}`, applyPatchFsFailureDetails({
operation,
args,
input,
invocation,
result,
remoteElapsedMs: Math.max(0, Date.now() - startedAtMs),
}));
}
return {
async stat(filePath) {
const result = await checked("stat", [filePath]);
const [bytesText, sha256] = result.stdout.trim().split(/\s+/u);
const bytes = Number(bytesText);
if (!Number.isSafeInteger(bytes) || bytes < 0 || !/^[0-9a-f]{64}$/u.test(sha256 ?? "")) {
throw new Error(`posix apply-patch fs stat returned invalid metadata: ${JSON.stringify({ filePath, stdout: result.stdout.slice(0, 500) })}`);
}
return { bytes, sha256: sha256! };
},
async readBlock(filePath, blockIndex, blockBytes) {
const result = await checked("read-b64-block", [filePath, String(blockIndex), String(blockBytes)]);
const encoded = result.stdout
.split(/\r?\n/u)
.filter((line) => !line.startsWith("UNIDESK_APPLY_PATCH_V2_BLOCK "))
.join("")
.replace(/\s+/gu, "");
return encoded.length === 0 ? Buffer.alloc(0) : Buffer.from(encoded, "base64");
},
async writeFile(filePath, content) {
const encoded = content.toString("base64");
const expectedBytes = String(content.length);
const expectedSha256 = createHash("sha256").update(content).digest("hex");
try {
await checked("write-b64-stdin", [filePath, expectedBytes, expectedSha256], encoded);
return;
} catch {
const token = `${process.pid}-${Date.now()}-${randomBytes(4).toString("hex")}-${expectedSha256.slice(0, 12)}`;
await checked("write-b64-begin", [filePath, token]);
for (const chunk of chunkString(encoded, posixApplyPatchWriteB64ChunkChars)) {
await checked("write-b64-append", [filePath, token, chunk]);
}
await checked("write-b64-commit", [filePath, token, expectedBytes, expectedSha256]);
}
},
async deleteFile(filePath) {
await checked("delete", [filePath]);
},
async readFiles(filePaths) {
const result = await checked("read-bulk-b64", filePaths);
return decodeApplyPatchV2BulkRead(result.stdout, filePaths);
},
async applyReplacementsBulk(filePaths, plans: Map<string, ApplyPatchV2BulkReplacementWritePlan>) {
const { targets, payload } = formatApplyPatchV2BulkReplacementPayload(filePaths, plans);
if (targets.length === 0) return;
await checked("apply-replacements-bulk-stdin", [String(targets.length)], payload);
},
};
}
export function createWindowsApplyPatchFileSystem(invocation: ParsedSshInvocation, runRemoteCommand: (remoteCommand: string, input?: string) => Promise<SshCaptureResult>): ApplyPatchV2FileSystem {
async function checked(operation: WindowsApplyPatchFsOperation, args: string[], input?: string): Promise<SshCaptureResult> {
const command = buildWindowsPowerShellInvocation(windowsApplyPatchFsScript(invocation.route.workspace, operation, args));
const startedAtMs = Date.now();
let result: SshCaptureResult;
try {
result = await runRemoteCommand(command, input);
} catch (error) {
throw new ApplyPatchV2Error(`windows apply-patch fs operation failed: ${operation}`, applyPatchFsFailureDetails({
operation,
args,
input,
invocation,
remoteElapsedMs: Math.max(0, Date.now() - startedAtMs),
cause: error instanceof Error ? { name: error.name, message: error.message } : { message: String(error) },
}));
}
if (result.exitCode === 0) return result;
throw new ApplyPatchV2Error(`windows apply-patch fs operation failed: ${operation}`, applyPatchFsFailureDetails({
operation,
args,
input,
@@ -2987,8 +3069,8 @@ export function createWindowsApplyPatchFileSystem(invocation: ParsedSshInvocatio
};
}
function windowsApplyPatchFsFailureDetails(options: {
operation: WindowsApplyPatchFsOperation;
function applyPatchFsFailureDetails(options: {
operation: string;
args: string[];
input?: string;
invocation: ParsedSshInvocation;
@@ -2998,8 +3080,8 @@ function windowsApplyPatchFsFailureDetails(options: {
}): Record<string, unknown> {
const { operation, args, input, invocation, result } = options;
const inputBytes = input === undefined ? 0 : Buffer.byteLength(input, "utf8");
const expected = windowsApplyPatchExpectedWrite(operation, args);
const targetCount = windowsApplyPatchBulkTargetCount(operation, args);
const expected = applyPatchFsExpectedWrite(operation, args);
const targetCount = applyPatchFsBulkTargetCount(operation, args);
return {
operation,
route: invocation.route.raw,
@@ -3021,14 +3103,18 @@ function windowsApplyPatchFsFailureDetails(options: {
};
}
function windowsApplyPatchExpectedWrite(operation: WindowsApplyPatchFsOperation, args: string[]): { expectedBytes?: string; expectedSha256?: string } {
function applyPatchFsExpectedWrite(operation: string, args: string[]): { expectedBytes?: string; expectedSha256?: string } {
if (operation === "write-b64-stdin") return { expectedBytes: args[1], expectedSha256: args[2] };
if (operation === "write-b64-commit") return { expectedBytes: args[2], expectedSha256: args[3] };
return {};
}
function windowsApplyPatchBulkTargetCount(operation: WindowsApplyPatchFsOperation, args: string[]): number | null {
if (operation !== "read-bulk-b64" && operation !== "apply-replacements-bulk-stdin") return null;
function applyPatchFsBulkTargetCount(operation: string, args: string[]): number | null {
if (operation === "read-bulk-b64") {
const count = Number(args[0]);
return Number.isSafeInteger(count) && count >= 0 ? count : args.length;
}
if (operation !== "apply-replacements-bulk-stdin") return null;
const count = Number(args[0]);
return Number.isSafeInteger(count) && count >= 0 ? count : null;
}
@@ -3476,7 +3562,9 @@ export async function runSsh(config: UniDeskConfig, providerId: string, args: st
const applyPatch = effectiveApplyPatchV2Invocation(invocation, normalizedArgs.slice(1));
const executor: ApplyPatchV2Executor = applyPatch.invocation.route.plane === "win"
? { fs: createWindowsApplyPatchFileSystem(applyPatch.invocation, (remoteCommand, input) => runSshCaptureRemoteCommand(config, applyPatch.invocation, remoteCommand, input)) }
: { run: (command, input) => runSshCaptureCommand(config, applyPatch.invocation, command, input) };
: applyPatch.invocation.route.plane === "host"
? { fs: createPosixApplyPatchFileSystem(applyPatch.invocation, (command, input) => runSshCaptureCommand(config, applyPatch.invocation, command, input)) }
: { run: (command, input) => runSshCaptureCommand(config, applyPatch.invocation, command, input) };
return await runApplyPatchV2({
executor,
stdin: process.stdin,