mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-22 17:48:56 +01:00
Acquire a lock before using a certain workspace storage directory and fall back to other directories if necessary
This commit is contained in:
@@ -20,6 +20,8 @@ import { IExtHostTask } from 'vs/workbench/api/common/extHostTask';
|
||||
import { IExtHostTerminalService } from 'vs/workbench/api/common/extHostTerminalService';
|
||||
import { IExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
import { IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths';
|
||||
import { ExtensionStoragePaths } from 'vs/workbench/api/node/extHostStoragePaths';
|
||||
|
||||
// #########################################################################
|
||||
// ### ###
|
||||
@@ -29,6 +31,7 @@ import { ILogService } from 'vs/platform/log/common/log';
|
||||
|
||||
registerSingleton(IExtHostExtensionService, ExtHostExtensionService);
|
||||
registerSingleton(ILogService, ExtHostLogService);
|
||||
registerSingleton(IExtensionStoragePaths, ExtensionStoragePaths);
|
||||
|
||||
registerSingleton(IExtHostDebugService, ExtHostDebugService);
|
||||
registerSingleton(IExtHostOutputService, ExtHostOutputService2);
|
||||
|
||||
275
src/vs/workbench/api/node/extHostStoragePaths.ts
Normal file
275
src/vs/workbench/api/node/extHostStoragePaths.ts
Normal file
@@ -0,0 +1,275 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { URI } from 'vs/base/common/uri';
|
||||
import { ExtensionStoragePaths as CommonExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths';
|
||||
import { Disposable } from 'vs/base/common/lifecycle';
|
||||
import { Schemas } from 'vs/base/common/network';
|
||||
import { IntervalTimer, timeout } from 'vs/base/common/async';
|
||||
import { ILogService } from 'vs/platform/log/common/log';
|
||||
|
||||
export class ExtensionStoragePaths extends CommonExtensionStoragePaths {
|
||||
|
||||
private _workspaceStorageLock: Lock | null = null;
|
||||
|
||||
protected override async _getWorkspaceStorageURI(storageName: string): Promise<URI> {
|
||||
const workspaceStorageURI = await super._getWorkspaceStorageURI(storageName);
|
||||
if (workspaceStorageURI.scheme !== Schemas.file) {
|
||||
return workspaceStorageURI;
|
||||
}
|
||||
|
||||
const workspaceStorageBase = workspaceStorageURI.fsPath;
|
||||
let attempt = 0;
|
||||
do {
|
||||
let workspaceStoragePath: string;
|
||||
if (attempt === 0) {
|
||||
workspaceStoragePath = workspaceStorageBase;
|
||||
} else {
|
||||
workspaceStoragePath = (
|
||||
/[/\\]$/.test(workspaceStorageBase)
|
||||
? `${workspaceStorageBase.substr(0, workspaceStorageBase.length - 1)}-${attempt}`
|
||||
: `${workspaceStorageBase}-${attempt}`
|
||||
);
|
||||
}
|
||||
|
||||
await mkdir(workspaceStoragePath);
|
||||
|
||||
const lockfile = path.join(workspaceStoragePath, 'vscode.lock');
|
||||
const lock = await tryAcquireLock(this._logService, lockfile, false);
|
||||
if (lock) {
|
||||
this._workspaceStorageLock = lock;
|
||||
process.on('exit', () => {
|
||||
lock.dispose();
|
||||
});
|
||||
return URI.file(workspaceStoragePath);
|
||||
}
|
||||
|
||||
attempt++;
|
||||
} while (attempt < 10);
|
||||
|
||||
// just give up
|
||||
return workspaceStorageURI;
|
||||
}
|
||||
|
||||
override onWillDeactivateAll(): void {
|
||||
// the lock will be released soon
|
||||
if (this._workspaceStorageLock) {
|
||||
this._workspaceStorageLock.setWillRelease(6000);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function mkdir(dir: string): Promise<void> {
|
||||
try {
|
||||
await fs.promises.stat(dir);
|
||||
return;
|
||||
} catch {
|
||||
// doesn't exist, that's OK
|
||||
}
|
||||
|
||||
await fs.promises.mkdir(dir);
|
||||
}
|
||||
|
||||
const MTIME_UPDATE_TIME = 1000; // 1s
|
||||
const STALE_LOCK_TIME = 10 * 60 * 1000; // 10 minutes
|
||||
|
||||
class Lock extends Disposable {
|
||||
|
||||
private readonly _timer: IntervalTimer;
|
||||
|
||||
constructor(
|
||||
private readonly logService: ILogService,
|
||||
private readonly filename: string
|
||||
) {
|
||||
super();
|
||||
|
||||
this._timer = this._register(new IntervalTimer());
|
||||
this._timer.cancelAndSet(async () => {
|
||||
const contents = await readLockfileContents(logService, filename);
|
||||
if (!contents || contents.pid !== process.pid) {
|
||||
// we don't hold the lock anymore ...
|
||||
logService.info(`Lock '${filename}': The lock was lost unexpectedly.`);
|
||||
this._timer.cancel();
|
||||
}
|
||||
try {
|
||||
await fs.promises.utimes(filename, new Date(), new Date());
|
||||
} catch (err) {
|
||||
logService.error(err);
|
||||
logService.info(`Lock '${filename}': Could not update mtime.`);
|
||||
}
|
||||
}, MTIME_UPDATE_TIME);
|
||||
}
|
||||
|
||||
public override dispose(): void {
|
||||
super.dispose();
|
||||
try { fs.unlinkSync(this.filename); } catch (err) { }
|
||||
}
|
||||
|
||||
public async setWillRelease(timeUntilReleaseMs: number): Promise<void> {
|
||||
this.logService.info(`Lock '${this.filename}': Marking the lockfile as scheduled to be released in ${timeUntilReleaseMs} ms.`);
|
||||
try {
|
||||
const contents: ILockfileContents = {
|
||||
pid: process.pid,
|
||||
willReleaseAt: Date.now() + timeUntilReleaseMs
|
||||
};
|
||||
await fs.promises.writeFile(this.filename, JSON.stringify(contents), { flag: 'w' });
|
||||
} catch (err) {
|
||||
this.logService.error(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to acquire a lock on a directory.
|
||||
* This does not use the real `flock`, but uses a file.
|
||||
* @returns a disposable if the lock could be acquired or null if it could not.
|
||||
*/
|
||||
async function tryAcquireLock(logService: ILogService, filename: string, isSecondAttempt: boolean): Promise<Lock | null> {
|
||||
try {
|
||||
const contents: ILockfileContents = {
|
||||
pid: process.pid,
|
||||
willReleaseAt: 0
|
||||
};
|
||||
await fs.promises.writeFile(filename, JSON.stringify(contents), { flag: 'wx' });
|
||||
} catch (err) {
|
||||
logService.error(err);
|
||||
}
|
||||
|
||||
// let's see if we got the lock
|
||||
const contents = await readLockfileContents(logService, filename);
|
||||
if (!contents || contents.pid !== process.pid) {
|
||||
// we didn't get the lock
|
||||
if (isSecondAttempt) {
|
||||
logService.info(`Lock '${filename}': Could not acquire lock, giving up.`);
|
||||
return null;
|
||||
}
|
||||
logService.info(`Lock '${filename}': Could not acquire lock, checking if the file is stale.`);
|
||||
return checkStaleAndTryAcquireLock(logService, filename);
|
||||
}
|
||||
|
||||
// we got the lock
|
||||
logService.info(`Lock '${filename}': Lock acquired.`);
|
||||
return new Lock(logService, filename);
|
||||
}
|
||||
|
||||
interface ILockfileContents {
|
||||
pid: number;
|
||||
willReleaseAt: number | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns 0 if the pid cannot be read
|
||||
*/
|
||||
async function readLockfileContents(logService: ILogService, filename: string): Promise<ILockfileContents | null> {
|
||||
let contents: Buffer;
|
||||
try {
|
||||
contents = await fs.promises.readFile(filename);
|
||||
} catch (err) {
|
||||
// cannot read the file
|
||||
logService.error(err);
|
||||
return null;
|
||||
}
|
||||
|
||||
return JSON.parse(String(contents));
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns 0 if the mtime cannot be read
|
||||
*/
|
||||
async function readmtime(logService: ILogService, filename: string): Promise<number> {
|
||||
let stats: fs.Stats;
|
||||
try {
|
||||
stats = await fs.promises.stat(filename);
|
||||
} catch (err) {
|
||||
// cannot read the file stats to check if it is stale or not
|
||||
logService.error(err);
|
||||
return 0;
|
||||
}
|
||||
return stats.mtime.getTime();
|
||||
}
|
||||
|
||||
function processExists(pid: number): boolean {
|
||||
try {
|
||||
process.kill(pid, 0); // throws an exception if the process doesn't exist anymore.
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function checkStaleAndTryAcquireLock(logService: ILogService, filename: string): Promise<Lock | null> {
|
||||
const contents = await readLockfileContents(logService, filename);
|
||||
if (!contents) {
|
||||
logService.info(`Lock '${filename}': Could not read pid of lock holder.`);
|
||||
return tryDeleteAndAcquireLock(logService, filename);
|
||||
}
|
||||
|
||||
if (contents.willReleaseAt) {
|
||||
let timeUntilRelease = contents.willReleaseAt - Date.now();
|
||||
if (timeUntilRelease < 5000) {
|
||||
if (timeUntilRelease > 0) {
|
||||
logService.info(`Lock '${filename}': The lockfile is scheduled to be released in ${timeUntilRelease} ms.`);
|
||||
} else {
|
||||
logService.info(`Lock '${filename}': The lockfile is scheduled to have been released.`);
|
||||
}
|
||||
|
||||
while (timeUntilRelease > 0) {
|
||||
await timeout(Math.min(100, timeUntilRelease));
|
||||
const mtime = await readmtime(logService, filename);
|
||||
if (mtime === 0) {
|
||||
// looks like the lock was released
|
||||
return tryDeleteAndAcquireLock(logService, filename);
|
||||
}
|
||||
timeUntilRelease = contents.willReleaseAt - Date.now();
|
||||
}
|
||||
|
||||
return tryDeleteAndAcquireLock(logService, filename);
|
||||
}
|
||||
}
|
||||
|
||||
if (!processExists(contents.pid)) {
|
||||
logService.info(`Lock '${filename}': The pid ${contents.pid} appears to be gone.`);
|
||||
return tryDeleteAndAcquireLock(logService, filename);
|
||||
}
|
||||
|
||||
const mtime1 = await readmtime(logService, filename);
|
||||
const elapsed1 = Date.now() - mtime1;
|
||||
if (elapsed1 <= STALE_LOCK_TIME) {
|
||||
// the lock does not look stale
|
||||
logService.info(`Lock '${filename}': The lock does not look stale, elapsed: ${elapsed1} ms, giving up.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// the lock holder updates the mtime every 1s.
|
||||
// let's give it a chance to update the mtime
|
||||
// in case of a wake from sleep or something similar
|
||||
logService.info(`Lock '${filename}': The lock looks stale, waiting for 2s.`);
|
||||
await timeout(2000);
|
||||
|
||||
const mtime2 = await readmtime(logService, filename);
|
||||
const elapsed2 = Date.now() - mtime2;
|
||||
if (elapsed2 <= STALE_LOCK_TIME) {
|
||||
// the lock does not look stale
|
||||
logService.info(`Lock '${filename}': The lock does not look stale, elapsed: ${elapsed2} ms, giving up.`);
|
||||
return null;
|
||||
}
|
||||
|
||||
// the lock looks stale
|
||||
logService.info(`Lock '${filename}': The lock looks stale even after waiting for 2s.`);
|
||||
return tryDeleteAndAcquireLock(logService, filename);
|
||||
}
|
||||
|
||||
async function tryDeleteAndAcquireLock(logService: ILogService, filename: string): Promise<Lock | null> {
|
||||
logService.info(`Lock '${filename}': Deleting a stale lock.`);
|
||||
try {
|
||||
await fs.promises.unlink(filename);
|
||||
} catch (err) {
|
||||
// cannot delete the file
|
||||
// maybe the file is already deleted
|
||||
}
|
||||
return tryAcquireLock(logService, filename, true);
|
||||
}
|
||||
Reference in New Issue
Block a user