Path: blob/main/src/vs/workbench/api/node/extHostStoragePaths.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import * as fs from 'fs';6import * as path from '../../../base/common/path.js';7import { URI } from '../../../base/common/uri.js';8import { ExtensionStoragePaths as CommonExtensionStoragePaths } from '../common/extHostStoragePaths.js';9import { Disposable } from '../../../base/common/lifecycle.js';10import { Schemas } from '../../../base/common/network.js';11import { IntervalTimer, timeout } from '../../../base/common/async.js';12import { ILogService } from '../../../platform/log/common/log.js';13import { Promises } from '../../../base/node/pfs.js';1415export class ExtensionStoragePaths extends CommonExtensionStoragePaths {1617private _workspaceStorageLock: Lock | null = null;1819protected override async _getWorkspaceStorageURI(storageName: string): Promise<URI> {20const workspaceStorageURI = await super._getWorkspaceStorageURI(storageName);21if (workspaceStorageURI.scheme !== Schemas.file) {22return workspaceStorageURI;23}2425if (this._environment.skipWorkspaceStorageLock) {26this._logService.info(`Skipping acquiring lock for ${workspaceStorageURI.fsPath}.`);27return workspaceStorageURI;28}2930const workspaceStorageBase = workspaceStorageURI.fsPath;31let attempt = 0;32do {33let workspaceStoragePath: string;34if (attempt === 0) {35workspaceStoragePath = workspaceStorageBase;36} else {37workspaceStoragePath = (38/[/\\]$/.test(workspaceStorageBase)39? `${workspaceStorageBase.substr(0, workspaceStorageBase.length - 1)}-${attempt}`40: `${workspaceStorageBase}-${attempt}`41);42}4344await mkdir(workspaceStoragePath);4546const lockfile = path.join(workspaceStoragePath, 'vscode.lock');47const lock = await tryAcquireLock(this._logService, lockfile, false);48if (lock) {49this._workspaceStorageLock = lock;50process.on('exit', () => {51lock.dispose();52});53return URI.file(workspaceStoragePath);54}5556attempt++;57} while (attempt < 10);5859// just give up60return workspaceStorageURI;61}6263override onWillDeactivateAll(): void {64// the lock will be released soon65this._workspaceStorageLock?.setWillRelease(6000);66}67}6869async function mkdir(dir: string): Promise<void> {70try {71await fs.promises.stat(dir);72return;73} catch {74// doesn't exist, that's OK75}7677try {78await fs.promises.mkdir(dir, { recursive: true });79} catch {80}81}8283const MTIME_UPDATE_TIME = 1000; // 1s84const STALE_LOCK_TIME = 10 * 60 * 1000; // 10 minutes8586class Lock extends Disposable {8788private readonly _timer: IntervalTimer;8990constructor(91private readonly logService: ILogService,92private readonly filename: string93) {94super();9596this._timer = this._register(new IntervalTimer());97this._timer.cancelAndSet(async () => {98const contents = await readLockfileContents(logService, filename);99if (!contents || contents.pid !== process.pid) {100// we don't hold the lock anymore ...101logService.info(`Lock '${filename}': The lock was lost unexpectedly.`);102this._timer.cancel();103}104try {105await fs.promises.utimes(filename, new Date(), new Date());106} catch (err) {107logService.error(err);108logService.info(`Lock '${filename}': Could not update mtime.`);109}110}, MTIME_UPDATE_TIME);111}112113public override dispose(): void {114super.dispose();115try { fs.unlinkSync(this.filename); } catch (err) { }116}117118public async setWillRelease(timeUntilReleaseMs: number): Promise<void> {119this.logService.info(`Lock '${this.filename}': Marking the lockfile as scheduled to be released in ${timeUntilReleaseMs} ms.`);120try {121const contents: ILockfileContents = {122pid: process.pid,123willReleaseAt: Date.now() + timeUntilReleaseMs124};125await Promises.writeFile(this.filename, JSON.stringify(contents), { flag: 'w' });126} catch (err) {127this.logService.error(err);128}129}130}131132/**133* Attempt to acquire a lock on a directory.134* This does not use the real `flock`, but uses a file.135* @returns a disposable if the lock could be acquired or null if it could not.136*/137async function tryAcquireLock(logService: ILogService, filename: string, isSecondAttempt: boolean): Promise<Lock | null> {138try {139const contents: ILockfileContents = {140pid: process.pid,141willReleaseAt: 0142};143await Promises.writeFile(filename, JSON.stringify(contents), { flag: 'wx' });144} catch (err) {145logService.error(err);146}147148// let's see if we got the lock149const contents = await readLockfileContents(logService, filename);150if (!contents || contents.pid !== process.pid) {151// we didn't get the lock152if (isSecondAttempt) {153logService.info(`Lock '${filename}': Could not acquire lock, giving up.`);154return null;155}156logService.info(`Lock '${filename}': Could not acquire lock, checking if the file is stale.`);157return checkStaleAndTryAcquireLock(logService, filename);158}159160// we got the lock161logService.info(`Lock '${filename}': Lock acquired.`);162return new Lock(logService, filename);163}164165interface ILockfileContents {166pid: number;167willReleaseAt: number | undefined;168}169170/**171* @returns 0 if the pid cannot be read172*/173async function readLockfileContents(logService: ILogService, filename: string): Promise<ILockfileContents | null> {174let contents: Buffer;175try {176contents = await fs.promises.readFile(filename);177} catch (err) {178// cannot read the file179logService.error(err);180return null;181}182183try {184return JSON.parse(String(contents));185} catch (err) {186// cannot parse the file187logService.error(err);188return null;189}190}191192/**193* @returns 0 if the mtime cannot be read194*/195async function readmtime(logService: ILogService, filename: string): Promise<number> {196let stats: fs.Stats;197try {198stats = await fs.promises.stat(filename);199} catch (err) {200// cannot read the file stats to check if it is stale or not201logService.error(err);202return 0;203}204return stats.mtime.getTime();205}206207function processExists(pid: number): boolean {208try {209process.kill(pid, 0); // throws an exception if the process doesn't exist anymore.210return true;211} catch (e) {212return false;213}214}215216async function checkStaleAndTryAcquireLock(logService: ILogService, filename: string): Promise<Lock | null> {217const contents = await readLockfileContents(logService, filename);218if (!contents) {219logService.info(`Lock '${filename}': Could not read pid of lock holder.`);220return tryDeleteAndAcquireLock(logService, filename);221}222223if (contents.willReleaseAt) {224let timeUntilRelease = contents.willReleaseAt - Date.now();225if (timeUntilRelease < 5000) {226if (timeUntilRelease > 0) {227logService.info(`Lock '${filename}': The lockfile is scheduled to be released in ${timeUntilRelease} ms.`);228} else {229logService.info(`Lock '${filename}': The lockfile is scheduled to have been released.`);230}231232while (timeUntilRelease > 0) {233await timeout(Math.min(100, timeUntilRelease));234const mtime = await readmtime(logService, filename);235if (mtime === 0) {236// looks like the lock was released237return tryDeleteAndAcquireLock(logService, filename);238}239timeUntilRelease = contents.willReleaseAt - Date.now();240}241242return tryDeleteAndAcquireLock(logService, filename);243}244}245246if (!processExists(contents.pid)) {247logService.info(`Lock '${filename}': The pid ${contents.pid} appears to be gone.`);248return tryDeleteAndAcquireLock(logService, filename);249}250251const mtime1 = await readmtime(logService, filename);252const elapsed1 = Date.now() - mtime1;253if (elapsed1 <= STALE_LOCK_TIME) {254// the lock does not look stale255logService.info(`Lock '${filename}': The lock does not look stale, elapsed: ${elapsed1} ms, giving up.`);256return null;257}258259// the lock holder updates the mtime every 1s.260// let's give it a chance to update the mtime261// in case of a wake from sleep or something similar262logService.info(`Lock '${filename}': The lock looks stale, waiting for 2s.`);263await timeout(2000);264265const mtime2 = await readmtime(logService, filename);266const elapsed2 = Date.now() - mtime2;267if (elapsed2 <= STALE_LOCK_TIME) {268// the lock does not look stale269logService.info(`Lock '${filename}': The lock does not look stale, elapsed: ${elapsed2} ms, giving up.`);270return null;271}272273// the lock looks stale274logService.info(`Lock '${filename}': The lock looks stale even after waiting for 2s.`);275return tryDeleteAndAcquireLock(logService, filename);276}277278async function tryDeleteAndAcquireLock(logService: ILogService, filename: string): Promise<Lock | null> {279logService.info(`Lock '${filename}': Deleting a stale lock.`);280try {281await fs.promises.unlink(filename);282} catch (err) {283// cannot delete the file284// maybe the file is already deleted285}286return tryAcquireLock(logService, filename, true);287}288289290