Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/api/node/extHostStoragePaths.ts
3296 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import * as fs from 'fs';
7
import * as path from '../../../base/common/path.js';
8
import { URI } from '../../../base/common/uri.js';
9
import { ExtensionStoragePaths as CommonExtensionStoragePaths } from '../common/extHostStoragePaths.js';
10
import { Disposable } from '../../../base/common/lifecycle.js';
11
import { Schemas } from '../../../base/common/network.js';
12
import { IntervalTimer, timeout } from '../../../base/common/async.js';
13
import { ILogService } from '../../../platform/log/common/log.js';
14
import { Promises } from '../../../base/node/pfs.js';
15
16
export class ExtensionStoragePaths extends CommonExtensionStoragePaths {
17
18
private _workspaceStorageLock: Lock | null = null;
19
20
protected override async _getWorkspaceStorageURI(storageName: string): Promise<URI> {
21
const workspaceStorageURI = await super._getWorkspaceStorageURI(storageName);
22
if (workspaceStorageURI.scheme !== Schemas.file) {
23
return workspaceStorageURI;
24
}
25
26
if (this._environment.skipWorkspaceStorageLock) {
27
this._logService.info(`Skipping acquiring lock for ${workspaceStorageURI.fsPath}.`);
28
return workspaceStorageURI;
29
}
30
31
const workspaceStorageBase = workspaceStorageURI.fsPath;
32
let attempt = 0;
33
do {
34
let workspaceStoragePath: string;
35
if (attempt === 0) {
36
workspaceStoragePath = workspaceStorageBase;
37
} else {
38
workspaceStoragePath = (
39
/[/\\]$/.test(workspaceStorageBase)
40
? `${workspaceStorageBase.substr(0, workspaceStorageBase.length - 1)}-${attempt}`
41
: `${workspaceStorageBase}-${attempt}`
42
);
43
}
44
45
await mkdir(workspaceStoragePath);
46
47
const lockfile = path.join(workspaceStoragePath, 'vscode.lock');
48
const lock = await tryAcquireLock(this._logService, lockfile, false);
49
if (lock) {
50
this._workspaceStorageLock = lock;
51
process.on('exit', () => {
52
lock.dispose();
53
});
54
return URI.file(workspaceStoragePath);
55
}
56
57
attempt++;
58
} while (attempt < 10);
59
60
// just give up
61
return workspaceStorageURI;
62
}
63
64
override onWillDeactivateAll(): void {
65
// the lock will be released soon
66
this._workspaceStorageLock?.setWillRelease(6000);
67
}
68
}
69
70
async function mkdir(dir: string): Promise<void> {
71
try {
72
await fs.promises.stat(dir);
73
return;
74
} catch {
75
// doesn't exist, that's OK
76
}
77
78
try {
79
await fs.promises.mkdir(dir, { recursive: true });
80
} catch {
81
}
82
}
83
84
const MTIME_UPDATE_TIME = 1000; // 1s
85
const STALE_LOCK_TIME = 10 * 60 * 1000; // 10 minutes
86
87
class Lock extends Disposable {
88
89
private readonly _timer: IntervalTimer;
90
91
constructor(
92
private readonly logService: ILogService,
93
private readonly filename: string
94
) {
95
super();
96
97
this._timer = this._register(new IntervalTimer());
98
this._timer.cancelAndSet(async () => {
99
const contents = await readLockfileContents(logService, filename);
100
if (!contents || contents.pid !== process.pid) {
101
// we don't hold the lock anymore ...
102
logService.info(`Lock '${filename}': The lock was lost unexpectedly.`);
103
this._timer.cancel();
104
}
105
try {
106
await fs.promises.utimes(filename, new Date(), new Date());
107
} catch (err) {
108
logService.error(err);
109
logService.info(`Lock '${filename}': Could not update mtime.`);
110
}
111
}, MTIME_UPDATE_TIME);
112
}
113
114
public override dispose(): void {
115
super.dispose();
116
try { fs.unlinkSync(this.filename); } catch (err) { }
117
}
118
119
public async setWillRelease(timeUntilReleaseMs: number): Promise<void> {
120
this.logService.info(`Lock '${this.filename}': Marking the lockfile as scheduled to be released in ${timeUntilReleaseMs} ms.`);
121
try {
122
const contents: ILockfileContents = {
123
pid: process.pid,
124
willReleaseAt: Date.now() + timeUntilReleaseMs
125
};
126
await Promises.writeFile(this.filename, JSON.stringify(contents), { flag: 'w' });
127
} catch (err) {
128
this.logService.error(err);
129
}
130
}
131
}
132
133
/**
134
* Attempt to acquire a lock on a directory.
135
* This does not use the real `flock`, but uses a file.
136
* @returns a disposable if the lock could be acquired or null if it could not.
137
*/
138
async function tryAcquireLock(logService: ILogService, filename: string, isSecondAttempt: boolean): Promise<Lock | null> {
139
try {
140
const contents: ILockfileContents = {
141
pid: process.pid,
142
willReleaseAt: 0
143
};
144
await Promises.writeFile(filename, JSON.stringify(contents), { flag: 'wx' });
145
} catch (err) {
146
logService.error(err);
147
}
148
149
// let's see if we got the lock
150
const contents = await readLockfileContents(logService, filename);
151
if (!contents || contents.pid !== process.pid) {
152
// we didn't get the lock
153
if (isSecondAttempt) {
154
logService.info(`Lock '${filename}': Could not acquire lock, giving up.`);
155
return null;
156
}
157
logService.info(`Lock '${filename}': Could not acquire lock, checking if the file is stale.`);
158
return checkStaleAndTryAcquireLock(logService, filename);
159
}
160
161
// we got the lock
162
logService.info(`Lock '${filename}': Lock acquired.`);
163
return new Lock(logService, filename);
164
}
165
166
interface ILockfileContents {
167
pid: number;
168
willReleaseAt: number | undefined;
169
}
170
171
/**
172
* @returns 0 if the pid cannot be read
173
*/
174
async function readLockfileContents(logService: ILogService, filename: string): Promise<ILockfileContents | null> {
175
let contents: Buffer;
176
try {
177
contents = await fs.promises.readFile(filename);
178
} catch (err) {
179
// cannot read the file
180
logService.error(err);
181
return null;
182
}
183
184
try {
185
return JSON.parse(String(contents));
186
} catch (err) {
187
// cannot parse the file
188
logService.error(err);
189
return null;
190
}
191
}
192
193
/**
194
* @returns 0 if the mtime cannot be read
195
*/
196
async function readmtime(logService: ILogService, filename: string): Promise<number> {
197
let stats: fs.Stats;
198
try {
199
stats = await fs.promises.stat(filename);
200
} catch (err) {
201
// cannot read the file stats to check if it is stale or not
202
logService.error(err);
203
return 0;
204
}
205
return stats.mtime.getTime();
206
}
207
208
function processExists(pid: number): boolean {
209
try {
210
process.kill(pid, 0); // throws an exception if the process doesn't exist anymore.
211
return true;
212
} catch (e) {
213
return false;
214
}
215
}
216
217
async function checkStaleAndTryAcquireLock(logService: ILogService, filename: string): Promise<Lock | null> {
218
const contents = await readLockfileContents(logService, filename);
219
if (!contents) {
220
logService.info(`Lock '${filename}': Could not read pid of lock holder.`);
221
return tryDeleteAndAcquireLock(logService, filename);
222
}
223
224
if (contents.willReleaseAt) {
225
let timeUntilRelease = contents.willReleaseAt - Date.now();
226
if (timeUntilRelease < 5000) {
227
if (timeUntilRelease > 0) {
228
logService.info(`Lock '${filename}': The lockfile is scheduled to be released in ${timeUntilRelease} ms.`);
229
} else {
230
logService.info(`Lock '${filename}': The lockfile is scheduled to have been released.`);
231
}
232
233
while (timeUntilRelease > 0) {
234
await timeout(Math.min(100, timeUntilRelease));
235
const mtime = await readmtime(logService, filename);
236
if (mtime === 0) {
237
// looks like the lock was released
238
return tryDeleteAndAcquireLock(logService, filename);
239
}
240
timeUntilRelease = contents.willReleaseAt - Date.now();
241
}
242
243
return tryDeleteAndAcquireLock(logService, filename);
244
}
245
}
246
247
if (!processExists(contents.pid)) {
248
logService.info(`Lock '${filename}': The pid ${contents.pid} appears to be gone.`);
249
return tryDeleteAndAcquireLock(logService, filename);
250
}
251
252
const mtime1 = await readmtime(logService, filename);
253
const elapsed1 = Date.now() - mtime1;
254
if (elapsed1 <= STALE_LOCK_TIME) {
255
// the lock does not look stale
256
logService.info(`Lock '${filename}': The lock does not look stale, elapsed: ${elapsed1} ms, giving up.`);
257
return null;
258
}
259
260
// the lock holder updates the mtime every 1s.
261
// let's give it a chance to update the mtime
262
// in case of a wake from sleep or something similar
263
logService.info(`Lock '${filename}': The lock looks stale, waiting for 2s.`);
264
await timeout(2000);
265
266
const mtime2 = await readmtime(logService, filename);
267
const elapsed2 = Date.now() - mtime2;
268
if (elapsed2 <= STALE_LOCK_TIME) {
269
// the lock does not look stale
270
logService.info(`Lock '${filename}': The lock does not look stale, elapsed: ${elapsed2} ms, giving up.`);
271
return null;
272
}
273
274
// the lock looks stale
275
logService.info(`Lock '${filename}': The lock looks stale even after waiting for 2s.`);
276
return tryDeleteAndAcquireLock(logService, filename);
277
}
278
279
async function tryDeleteAndAcquireLock(logService: ILogService, filename: string): Promise<Lock | null> {
280
logService.info(`Lock '${filename}': Deleting a stale lock.`);
281
try {
282
await fs.promises.unlink(filename);
283
} catch (err) {
284
// cannot delete the file
285
// maybe the file is already deleted
286
}
287
return tryAcquireLock(logService, filename, true);
288
}
289
290