Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,7 @@
"jsonc-parser": "^3.3.1",
"openpgp": "^6.2.2",
"pretty-bytes": "^7.1.0",
"proper-lockfile": "^4.1.2",
"proxy-agent": "^6.5.0",
"semver": "^7.7.3",
"ua-parser-js": "1.0.40",
Expand All @@ -361,6 +362,7 @@
"@types/eventsource": "^3.0.0",
"@types/glob": "^7.1.3",
"@types/node": "^22.14.1",
"@types/proper-lockfile": "^4.1.4",
"@types/semver": "^7.7.1",
"@types/ua-parser-js": "0.7.36",
"@types/vscode": "^1.73.0",
Expand Down
126 changes: 126 additions & 0 deletions src/core/binaryLock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import prettyBytes from "pretty-bytes";
import * as lockfile from "proper-lockfile";
import * as vscode from "vscode";

import { type Logger } from "../logging/logger";

import * as downloadProgress from "./downloadProgress";

/**
* Timeout to detect stale lock files and take over from stuck processes.
* This value is intentionally small so we can quickly takeover.
*/
const STALE_TIMEOUT_MS = 15000;

const LOCK_POLL_INTERVAL_MS = 500;

type LockRelease = () => Promise<void>;

/**
* Manages file locking for binary downloads to coordinate between multiple
* VS Code windows downloading the same binary.
*/
export class BinaryLock {
constructor(
private readonly vscodeProposed: typeof vscode,
private readonly output: Logger,
) {}

/**
* Acquire the lock, or wait for another process if the lock is held.
* Returns the lock release function and a flag indicating if we waited.
*/
async acquireLockOrWait(
binPath: string,
progressLogPath: string,
): Promise<{ release: LockRelease; waited: boolean }> {
const release = await this.safeAcquireLock(binPath);
if (release) {
return { release, waited: false };
}

this.output.info(
"Another process is downloading the binary, monitoring progress",
);
const newRelease = await this.monitorDownloadProgress(
binPath,
progressLogPath,
);
return { release: newRelease, waited: true };
}

/**
* Attempt to acquire a lock on the binary file.
* Returns the release function if successful, null if lock is already held.
*/
private async safeAcquireLock(path: string): Promise<LockRelease | null> {
try {
const release = await lockfile.lock(path, {
stale: STALE_TIMEOUT_MS,
retries: 0,
realpath: false,
});
return release;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ELOCKED") {
throw error;
}
return null;
}
}

/**
* Monitor download progress from another process by polling the progress log
* and attempting to acquire the lock. Shows a VS Code progress notification.
* Returns the lock release function once the download completes.
*/
private async monitorDownloadProgress(
binPath: string,
progressLogPath: string,
): Promise<LockRelease> {
return await this.vscodeProposed.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
title: "Another window is downloading the Coder CLI binary",
cancellable: false,
},
async (progress) => {
return new Promise<LockRelease>((resolve, reject) => {
const poll = async () => {
try {
await this.updateProgressMonitor(progressLogPath, progress);
const release = await this.safeAcquireLock(binPath);
if (release) {
return resolve(release);
}
// Schedule next poll only after current one completes
setTimeout(poll, LOCK_POLL_INTERVAL_MS);
} catch (error) {
reject(error);
}
};
poll().catch((error) => reject(error));
});
},
);
}

private async updateProgressMonitor(
progressLogPath: string,
progress: vscode.Progress<{ message?: string }>,
): Promise<void> {
const currentProgress =
await downloadProgress.readProgress(progressLogPath);
if (currentProgress) {
const totalBytesPretty =
currentProgress.totalBytes === null
? "unknown"
: prettyBytes(currentProgress.totalBytes);
const message =
currentProgress.status === "verifying"
? "Verifying signature..."
: `${prettyBytes(currentProgress.bytesDownloaded)} / ${totalBytesPretty}`;
progress.report({ message });
}
}
}
Loading