Skip to content

Commit

Permalink
Acquire a lock before using a certain workspace storage directory and…
Browse files Browse the repository at this point in the history
… fall back to other directories if necessary
  • Loading branch information
alexdima committed Jun 28, 2021
1 parent cd13f36 commit b1b44a3
Show file tree
Hide file tree
Showing 6 changed files with 293 additions and 5 deletions.
2 changes: 0 additions & 2 deletions src/vs/workbench/api/common/extHost.common.services.ts
Expand Up @@ -14,7 +14,6 @@ import { IExtHostTerminalService, WorkerExtHostTerminalService } from 'vs/workbe
import { IExtHostTask, WorkerExtHostTask } from 'vs/workbench/api/common/extHostTask';
import { IExtHostDebugService, WorkerExtHostDebugService } from 'vs/workbench/api/common/extHostDebugService';
import { IExtHostSearch, ExtHostSearch } from 'vs/workbench/api/common/extHostSearch';
import { IExtensionStoragePaths, ExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths';
import { IExtHostStorage, ExtHostStorage } from 'vs/workbench/api/common/extHostStorage';
import { IExtHostTunnelService, ExtHostTunnelService } from 'vs/workbench/api/common/extHostTunnelService';
import { IExtHostApiDeprecationService, ExtHostApiDeprecationService, } from 'vs/workbench/api/common/extHostApiDeprecationService';
Expand All @@ -25,7 +24,6 @@ import { IExtHostSecretState, ExtHostSecretState } from 'vs/workbench/api/common
import { ExtHostTelemetry, IExtHostTelemetry } from 'vs/workbench/api/common/extHostTelemetry';
import { ExtHostEditorTabs, IExtHostEditorTabs } from 'vs/workbench/api/common/extHostEditorTabs';

registerSingleton(IExtensionStoragePaths, ExtensionStoragePaths);
registerSingleton(IExtHostApiDeprecationService, ExtHostApiDeprecationService);
registerSingleton(IExtHostCommands, ExtHostCommands);
registerSingleton(IExtHostConfiguration, ExtHostConfiguration);
Expand Down
2 changes: 2 additions & 0 deletions src/vs/workbench/api/common/extHostExtensionService.ts
Expand Up @@ -204,6 +204,8 @@ export abstract class AbstractExtHostExtensionService extends Disposable impleme
}

public async deactivateAll(): Promise<void> {
this._storagePath.onWillDeactivateAll();

let allPromises: Promise<void>[] = [];
try {
const allExtensions = this._registry.getAllExtensionDescriptions();
Expand Down
14 changes: 11 additions & 3 deletions src/vs/workbench/api/common/extHostStoragePaths.ts
Expand Up @@ -18,34 +18,39 @@ export interface IExtensionStoragePaths {
whenReady: Promise<any>;
workspaceValue(extension: IExtensionDescription): URI | undefined;
globalValue(extension: IExtensionDescription): URI;
onWillDeactivateAll(): void;
}

export class ExtensionStoragePaths implements IExtensionStoragePaths {

readonly _serviceBrand: undefined;

private readonly _workspace?: IStaticWorkspaceData;
private readonly _environment: IEnvironment;
protected readonly _environment: IEnvironment;

readonly whenReady: Promise<URI | undefined>;
private _value?: URI;

constructor(
@IExtHostInitDataService initData: IExtHostInitDataService,
@ILogService private readonly _logService: ILogService,
@ILogService protected readonly _logService: ILogService,
@IExtHostConsumerFileSystem private readonly _extHostFileSystem: IExtHostConsumerFileSystem
) {
this._workspace = initData.workspace ?? undefined;
this._environment = initData.environment;
this.whenReady = this._getOrCreateWorkspaceStoragePath().then(value => this._value = value);
}

protected async _getWorkspaceStorageURI(storageName: string): Promise<URI> {
return URI.joinPath(this._environment.workspaceStorageHome, storageName);
}

private async _getOrCreateWorkspaceStoragePath(): Promise<URI | undefined> {
if (!this._workspace) {
return Promise.resolve(undefined);
}
const storageName = this._workspace.id;
const storageUri = URI.joinPath(this._environment.workspaceStorageHome, storageName);
const storageUri = await this._getWorkspaceStorageURI(storageName);

try {
await this._extHostFileSystem.value.stat(storageUri);
Expand Down Expand Up @@ -84,4 +89,7 @@ export class ExtensionStoragePaths implements IExtensionStoragePaths {
globalValue(extension: IExtensionDescription): URI {
return URI.joinPath(this._environment.globalStorageHome, extension.identifier.value.toLowerCase());
}

onWillDeactivateAll(): void {
}
}
3 changes: 3 additions & 0 deletions src/vs/workbench/api/node/extHost.node.services.ts
Expand Up @@ -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';

// #########################################################################
// ### ###
Expand All @@ -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);
Expand Down
275 changes: 275 additions & 0 deletions src/vs/workbench/api/node/extHostStoragePaths.ts
@@ -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);
}
2 changes: 2 additions & 0 deletions src/vs/workbench/api/worker/extHost.worker.services.ts
Expand Up @@ -6,6 +6,7 @@
import { registerSingleton } from 'vs/platform/instantiation/common/extensions';
import { ILogService } from 'vs/platform/log/common/log';
import { IExtHostExtensionService } from 'vs/workbench/api/common/extHostExtensionService';
import { ExtensionStoragePaths, IExtensionStoragePaths } from 'vs/workbench/api/common/extHostStoragePaths';
import { ExtHostExtensionService } from 'vs/workbench/api/worker/extHostExtensionService';
import { ExtHostLogService } from 'vs/workbench/api/worker/extHostLogService';

Expand All @@ -17,3 +18,4 @@ import { ExtHostLogService } from 'vs/workbench/api/worker/extHostLogService';

registerSingleton(IExtHostExtensionService, ExtHostExtensionService);
registerSingleton(ILogService, ExtHostLogService);
registerSingleton(IExtensionStoragePaths, ExtensionStoragePaths);

0 comments on commit b1b44a3

Please sign in to comment.