mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-25 18:52:39 +00:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f71591d5a3 | ||
|
|
d5a83362a7 |
@@ -16,7 +16,6 @@ interface FakeSpawnBehavior {
|
|||||||
|
|
||||||
interface FakeSpawnController {
|
interface FakeSpawnController {
|
||||||
activeCount: number;
|
activeCount: number;
|
||||||
nextPid: number;
|
|
||||||
peakActiveCount: number;
|
peakActiveCount: number;
|
||||||
processes: FakeChildProcess[];
|
processes: FakeChildProcess[];
|
||||||
queue: FakeSpawnBehavior[];
|
queue: FakeSpawnBehavior[];
|
||||||
@@ -25,7 +24,6 @@ interface FakeSpawnController {
|
|||||||
|
|
||||||
const fakeSpawnController = vi.hoisted<FakeSpawnController>(() => ({
|
const fakeSpawnController = vi.hoisted<FakeSpawnController>(() => ({
|
||||||
activeCount: 0,
|
activeCount: 0,
|
||||||
nextPid: 1000,
|
|
||||||
peakActiveCount: 0,
|
peakActiveCount: 0,
|
||||||
processes: [],
|
processes: [],
|
||||||
queue: [],
|
queue: [],
|
||||||
@@ -35,7 +33,6 @@ const fakeSpawnController = vi.hoisted<FakeSpawnController>(() => ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.activeCount = 0;
|
this.activeCount = 0;
|
||||||
this.nextPid = 1000;
|
|
||||||
this.peakActiveCount = 0;
|
this.peakActiveCount = 0;
|
||||||
this.processes = [];
|
this.processes = [];
|
||||||
this.queue = [];
|
this.queue = [];
|
||||||
@@ -43,7 +40,8 @@ const fakeSpawnController = vi.hoisted<FakeSpawnController>(() => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
class FakeChildProcess extends EventEmitter {
|
class FakeChildProcess extends EventEmitter {
|
||||||
public readonly pid: number;
|
// This is not an OS process; tree cleanup must use the direct-child fallback.
|
||||||
|
public readonly pid = undefined;
|
||||||
public readonly stderr = new EventEmitter();
|
public readonly stderr = new EventEmitter();
|
||||||
public readonly stdout = new EventEmitter();
|
public readonly stdout = new EventEmitter();
|
||||||
public killed = false;
|
public killed = false;
|
||||||
@@ -57,8 +55,6 @@ class FakeChildProcess extends EventEmitter {
|
|||||||
public constructor(behavior: FakeSpawnBehavior) {
|
public constructor(behavior: FakeSpawnBehavior) {
|
||||||
super();
|
super();
|
||||||
this.behavior = behavior;
|
this.behavior = behavior;
|
||||||
this.pid = fakeSpawnController.nextPid;
|
|
||||||
fakeSpawnController.nextPid += 1;
|
|
||||||
|
|
||||||
fakeSpawnController.activeCount += 1;
|
fakeSpawnController.activeCount += 1;
|
||||||
fakeSpawnController.peakActiveCount = Math.max(
|
fakeSpawnController.peakActiveCount = Math.max(
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import {
|
|||||||
submitGitCommandTrace,
|
submitGitCommandTrace,
|
||||||
} from "./git-command-trace.js";
|
} from "./git-command-trace.js";
|
||||||
import { spawnProcess } from "./spawn.js";
|
import { spawnProcess } from "./spawn.js";
|
||||||
|
import { signalProcessTree } from "./tree-kill.js";
|
||||||
import {
|
import {
|
||||||
GitProcessScheduler,
|
GitProcessScheduler,
|
||||||
type GitProcessPriority,
|
type GitProcessPriority,
|
||||||
@@ -422,7 +423,7 @@ function executeGitCommand<Output>(
|
|||||||
|
|
||||||
timer = setTimeout(() => {
|
timer = setTimeout(() => {
|
||||||
timeoutError = new Error(`Git command timed out after ${timeout}ms: ${command}`);
|
timeoutError = new Error(`Git command timed out after ${timeout}ms: ${command}`);
|
||||||
child.kill("SIGKILL");
|
void signalProcessTree(child, "SIGKILL");
|
||||||
settle(() => reject(timeoutError));
|
settle(() => reject(timeoutError));
|
||||||
if (processExit) {
|
if (processExit) {
|
||||||
settleTimeoutTrace(processExit.exitCode, processExit.signal);
|
settleTimeoutTrace(processExit.exitCode, processExit.signal);
|
||||||
|
|||||||
@@ -1,12 +1,17 @@
|
|||||||
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
import { createServer, type Socket } from "node:net";
|
||||||
import { afterEach, describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
|
|
||||||
import { runGitCommand } from "./run-git-command.js";
|
import { runGitCommand } from "./run-git-command.js";
|
||||||
|
|
||||||
const tempDirs: string[] = [];
|
const tempDirs: string[] = [];
|
||||||
|
|
||||||
|
function allowConnectionReset(error: NodeJS.ErrnoException): void {
|
||||||
|
if (error.code !== "ECONNRESET") throw error;
|
||||||
|
}
|
||||||
|
|
||||||
function makeTempRepo(): string {
|
function makeTempRepo(): string {
|
||||||
const repo = mkdtempSync(path.join(tmpdir(), "paseo-git-shell-"));
|
const repo = mkdtempSync(path.join(tmpdir(), "paseo-git-shell-"));
|
||||||
tempDirs.push(repo);
|
tempDirs.push(repo);
|
||||||
@@ -20,6 +25,49 @@ afterEach(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("runGitCommand shell behavior", () => {
|
describe("runGitCommand shell behavior", () => {
|
||||||
|
it.each(["http", "ssh"])(
|
||||||
|
"closes a stalled %s fetch connection on timeout and admits the next command",
|
||||||
|
async (protocol) => {
|
||||||
|
const repo = makeTempRepo();
|
||||||
|
await runGitCommand(["init"], { cwd: repo });
|
||||||
|
const sockets: Socket[] = [];
|
||||||
|
const server = createServer((socket) => {
|
||||||
|
sockets.push(socket);
|
||||||
|
// Force-killing a Windows peer resets the connection instead of sending EOF.
|
||||||
|
socket.on("error", allowConnectionReset);
|
||||||
|
// Consume the request but never answer, keeping the real transport helper busy.
|
||||||
|
socket.resume();
|
||||||
|
});
|
||||||
|
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||||
|
const address = server.address();
|
||||||
|
if (!address || typeof address === "string") throw new Error("Missing TCP address");
|
||||||
|
const remote = `${protocol}://127.0.0.1:${address.port}/repo.git`;
|
||||||
|
try {
|
||||||
|
await expect(
|
||||||
|
runGitCommand(["fetch", remote, "main"], {
|
||||||
|
cwd: repo,
|
||||||
|
timeout: 2000,
|
||||||
|
envOverlay: {
|
||||||
|
NO_PROXY: "127.0.0.1",
|
||||||
|
no_proxy: "127.0.0.1",
|
||||||
|
GIT_SSH_COMMAND: "ssh -F none -o BatchMode=yes",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).rejects.toThrow("Git command timed out after 2000ms");
|
||||||
|
|
||||||
|
expect(sockets).toHaveLength(1);
|
||||||
|
await expect.poll(() => sockets[0]?.destroyed, { timeout: 3000 }).toBe(true);
|
||||||
|
await expect(runGitCommand(["--version"], { cwd: repo })).resolves.toMatchObject({
|
||||||
|
exitCode: 0,
|
||||||
|
truncated: false,
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
for (const socket of sockets) socket.destroy();
|
||||||
|
await new Promise<void>((resolve) => server.close(() => resolve()));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
it("passes git arguments directly instead of through the platform shell", async () => {
|
it("passes git arguments directly instead of through the platform shell", async () => {
|
||||||
const repo = makeTempRepo();
|
const repo = makeTempRepo();
|
||||||
const literalName = "%PASEO_GIT_SHELL_SENTINEL%";
|
const literalName = "%PASEO_GIT_SHELL_SENTINEL%";
|
||||||
|
|||||||
@@ -55,10 +55,18 @@ function createLegacyWorktreeForTest(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function allowConnectionReset(error: NodeJS.ErrnoException): void {
|
||||||
|
if (error.code !== "ECONNRESET") throw error;
|
||||||
|
}
|
||||||
|
|
||||||
// A remote that accepts connections and never answers, like a VPN-only host while off the VPN.
|
// A remote that accepts connections and never answers, like a VPN-only host while off the VPN.
|
||||||
async function startSilentRemote(): Promise<{ url: string; close: () => Promise<void> }> {
|
async function startSilentRemote(): Promise<{ url: string; close: () => Promise<void> }> {
|
||||||
const connections = new Set<Socket>();
|
const connections = new Set<Socket>();
|
||||||
const server = createServer((socket) => connections.add(socket));
|
const server = createServer((socket) => {
|
||||||
|
connections.add(socket);
|
||||||
|
// Timeout cleanup force-kills the peer, which can reset its connection on Windows.
|
||||||
|
socket.on("error", allowConnectionReset);
|
||||||
|
});
|
||||||
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||||||
const { port } = server.address() as AddressInfo;
|
const { port } = server.address() as AddressInfo;
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user