Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/platform/files/node/diskFileSystemProviderServer.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 { Emitter, Event } from '../../../base/common/event.js';
7
import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js';
8
import { DiskFileSystemProvider } from './diskFileSystemProvider.js';
9
import { Disposable, dispose, IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
10
import { ILogService } from '../../log/common/log.js';
11
import { IURITransformer } from '../../../base/common/uriIpc.js';
12
import { URI, UriComponents } from '../../../base/common/uri.js';
13
import { VSBuffer } from '../../../base/common/buffer.js';
14
import { ReadableStreamEventPayload, listenStream } from '../../../base/common/stream.js';
15
import { IStat, IFileReadStreamOptions, IFileWriteOptions, IFileOpenOptions, IFileDeleteOptions, IFileOverwriteOptions, IFileChange, IWatchOptions, FileType, IFileAtomicReadOptions } from '../common/files.js';
16
import { CancellationTokenSource } from '../../../base/common/cancellation.js';
17
import { IEnvironmentService } from '../../environment/common/environment.js';
18
import { IRecursiveWatcherOptions } from '../common/watcher.js';
19
20
export interface ISessionFileWatcher extends IDisposable {
21
watch(req: number, resource: URI, opts: IWatchOptions): IDisposable;
22
}
23
24
/**
25
* A server implementation for a IPC based file system provider client.
26
*/
27
export abstract class AbstractDiskFileSystemProviderChannel<T> extends Disposable implements IServerChannel<T> {
28
29
constructor(
30
protected readonly provider: DiskFileSystemProvider,
31
protected readonly logService: ILogService
32
) {
33
super();
34
}
35
36
call(ctx: T, command: string, arg?: any): Promise<any> {
37
const uriTransformer = this.getUriTransformer(ctx);
38
39
switch (command) {
40
case 'stat': return this.stat(uriTransformer, arg[0]);
41
case 'realpath': return this.realpath(uriTransformer, arg[0]);
42
case 'readdir': return this.readdir(uriTransformer, arg[0]);
43
case 'open': return this.open(uriTransformer, arg[0], arg[1]);
44
case 'close': return this.close(arg[0]);
45
case 'read': return this.read(arg[0], arg[1], arg[2]);
46
case 'readFile': return this.readFile(uriTransformer, arg[0], arg[1]);
47
case 'write': return this.write(arg[0], arg[1], arg[2], arg[3], arg[4]);
48
case 'writeFile': return this.writeFile(uriTransformer, arg[0], arg[1], arg[2]);
49
case 'rename': return this.rename(uriTransformer, arg[0], arg[1], arg[2]);
50
case 'copy': return this.copy(uriTransformer, arg[0], arg[1], arg[2]);
51
case 'cloneFile': return this.cloneFile(uriTransformer, arg[0], arg[1]);
52
case 'mkdir': return this.mkdir(uriTransformer, arg[0]);
53
case 'delete': return this.delete(uriTransformer, arg[0], arg[1]);
54
case 'watch': return this.watch(uriTransformer, arg[0], arg[1], arg[2], arg[3]);
55
case 'unwatch': return this.unwatch(arg[0], arg[1]);
56
}
57
58
throw new Error(`IPC Command ${command} not found`);
59
}
60
61
listen(ctx: T, event: string, arg: any): Event<any> {
62
const uriTransformer = this.getUriTransformer(ctx);
63
64
switch (event) {
65
case 'fileChange': return this.onFileChange(uriTransformer, arg[0]);
66
case 'readFileStream': return this.onReadFileStream(uriTransformer, arg[0], arg[1]);
67
}
68
69
throw new Error(`Unknown event ${event}`);
70
}
71
72
protected abstract getUriTransformer(ctx: T): IURITransformer;
73
74
protected abstract transformIncoming(uriTransformer: IURITransformer, _resource: UriComponents, supportVSCodeResource?: boolean): URI;
75
76
//#region File Metadata Resolving
77
78
private stat(uriTransformer: IURITransformer, _resource: UriComponents): Promise<IStat> {
79
const resource = this.transformIncoming(uriTransformer, _resource, true);
80
81
return this.provider.stat(resource);
82
}
83
84
private realpath(uriTransformer: IURITransformer, _resource: UriComponents): Promise<string> {
85
const resource = this.transformIncoming(uriTransformer, _resource, true);
86
87
return this.provider.realpath(resource);
88
}
89
90
private readdir(uriTransformer: IURITransformer, _resource: UriComponents): Promise<[string, FileType][]> {
91
const resource = this.transformIncoming(uriTransformer, _resource);
92
93
return this.provider.readdir(resource);
94
}
95
96
//#endregion
97
98
//#region File Reading/Writing
99
100
private async readFile(uriTransformer: IURITransformer, _resource: UriComponents, opts?: IFileAtomicReadOptions): Promise<VSBuffer> {
101
const resource = this.transformIncoming(uriTransformer, _resource, true);
102
const buffer = await this.provider.readFile(resource, opts);
103
104
return VSBuffer.wrap(buffer);
105
}
106
107
private onReadFileStream(uriTransformer: IURITransformer, _resource: URI, opts: IFileReadStreamOptions): Event<ReadableStreamEventPayload<VSBuffer>> {
108
const resource = this.transformIncoming(uriTransformer, _resource, true);
109
const cts = new CancellationTokenSource();
110
111
const emitter = new Emitter<ReadableStreamEventPayload<VSBuffer>>({
112
onDidRemoveLastListener: () => {
113
114
// Ensure to cancel the read operation when there is no more
115
// listener on the other side to prevent unneeded work.
116
cts.cancel();
117
}
118
});
119
120
const fileStream = this.provider.readFileStream(resource, opts, cts.token);
121
listenStream(fileStream, {
122
onData: chunk => emitter.fire(VSBuffer.wrap(chunk)),
123
onError: error => emitter.fire(error),
124
onEnd: () => {
125
126
// Forward event
127
emitter.fire('end');
128
129
// Cleanup
130
emitter.dispose();
131
cts.dispose();
132
}
133
});
134
135
return emitter.event;
136
}
137
138
private writeFile(uriTransformer: IURITransformer, _resource: UriComponents, content: VSBuffer, opts: IFileWriteOptions): Promise<void> {
139
const resource = this.transformIncoming(uriTransformer, _resource);
140
141
return this.provider.writeFile(resource, content.buffer, opts);
142
}
143
144
private open(uriTransformer: IURITransformer, _resource: UriComponents, opts: IFileOpenOptions): Promise<number> {
145
const resource = this.transformIncoming(uriTransformer, _resource, true);
146
147
return this.provider.open(resource, opts);
148
}
149
150
private close(fd: number): Promise<void> {
151
return this.provider.close(fd);
152
}
153
154
private async read(fd: number, pos: number, length: number): Promise<[VSBuffer, number]> {
155
const buffer = VSBuffer.alloc(length);
156
const bufferOffset = 0; // offset is 0 because we create a buffer to read into for each call
157
const bytesRead = await this.provider.read(fd, pos, buffer.buffer, bufferOffset, length);
158
159
return [buffer, bytesRead];
160
}
161
162
private write(fd: number, pos: number, data: VSBuffer, offset: number, length: number): Promise<number> {
163
return this.provider.write(fd, pos, data.buffer, offset, length);
164
}
165
166
//#endregion
167
168
//#region Move/Copy/Delete/Create Folder
169
170
private mkdir(uriTransformer: IURITransformer, _resource: UriComponents): Promise<void> {
171
const resource = this.transformIncoming(uriTransformer, _resource);
172
173
return this.provider.mkdir(resource);
174
}
175
176
protected delete(uriTransformer: IURITransformer, _resource: UriComponents, opts: IFileDeleteOptions): Promise<void> {
177
const resource = this.transformIncoming(uriTransformer, _resource);
178
179
return this.provider.delete(resource, opts);
180
}
181
182
private rename(uriTransformer: IURITransformer, _source: UriComponents, _target: UriComponents, opts: IFileOverwriteOptions): Promise<void> {
183
const source = this.transformIncoming(uriTransformer, _source);
184
const target = this.transformIncoming(uriTransformer, _target);
185
186
return this.provider.rename(source, target, opts);
187
}
188
189
private copy(uriTransformer: IURITransformer, _source: UriComponents, _target: UriComponents, opts: IFileOverwriteOptions): Promise<void> {
190
const source = this.transformIncoming(uriTransformer, _source);
191
const target = this.transformIncoming(uriTransformer, _target);
192
193
return this.provider.copy(source, target, opts);
194
}
195
196
//#endregion
197
198
//#region Clone File
199
200
private cloneFile(uriTransformer: IURITransformer, _source: UriComponents, _target: UriComponents): Promise<void> {
201
const source = this.transformIncoming(uriTransformer, _source);
202
const target = this.transformIncoming(uriTransformer, _target);
203
204
return this.provider.cloneFile(source, target);
205
}
206
207
//#endregion
208
209
//#region File Watching
210
211
private readonly sessionToWatcher = new Map<string /* session ID */, ISessionFileWatcher>();
212
private readonly watchRequests = new Map<string /* session ID + request ID */, IDisposable>();
213
214
private onFileChange(uriTransformer: IURITransformer, sessionId: string): Event<IFileChange[] | string> {
215
216
// We want a specific emitter for the given session so that events
217
// from the one session do not end up on the other session. As such
218
// we create a `SessionFileWatcher` and a `Emitter` for that session.
219
220
const emitter = new Emitter<IFileChange[] | string>({
221
onWillAddFirstListener: () => {
222
this.sessionToWatcher.set(sessionId, this.createSessionFileWatcher(uriTransformer, emitter));
223
},
224
onDidRemoveLastListener: () => {
225
dispose(this.sessionToWatcher.get(sessionId));
226
this.sessionToWatcher.delete(sessionId);
227
}
228
});
229
230
return emitter.event;
231
}
232
233
private async watch(uriTransformer: IURITransformer, sessionId: string, req: number, _resource: UriComponents, opts: IWatchOptions): Promise<void> {
234
const watcher = this.sessionToWatcher.get(sessionId);
235
if (watcher) {
236
const resource = this.transformIncoming(uriTransformer, _resource);
237
const disposable = watcher.watch(req, resource, opts);
238
this.watchRequests.set(sessionId + req, disposable);
239
}
240
}
241
242
private async unwatch(sessionId: string, req: number): Promise<void> {
243
const id = sessionId + req;
244
const disposable = this.watchRequests.get(id);
245
if (disposable) {
246
dispose(disposable);
247
this.watchRequests.delete(id);
248
}
249
}
250
251
protected abstract createSessionFileWatcher(uriTransformer: IURITransformer, emitter: Emitter<IFileChange[] | string>): ISessionFileWatcher;
252
253
//#endregion
254
255
override dispose(): void {
256
super.dispose();
257
258
for (const [, disposable] of this.watchRequests) {
259
disposable.dispose();
260
}
261
this.watchRequests.clear();
262
263
for (const [, disposable] of this.sessionToWatcher) {
264
disposable.dispose();
265
}
266
this.sessionToWatcher.clear();
267
}
268
}
269
270
export abstract class AbstractSessionFileWatcher extends Disposable implements ISessionFileWatcher {
271
272
private readonly watcherRequests = new Map<number, IDisposable>();
273
274
// To ensure we use one file watcher per session, we keep a
275
// disk file system provider instantiated for this session.
276
// The provider is cheap and only stateful when file watching
277
// starts.
278
//
279
// This is important because we want to ensure that we only
280
// forward events from the watched paths for this session and
281
// not other clients that asked to watch other paths.
282
private readonly fileWatcher: DiskFileSystemProvider;
283
284
constructor(
285
private readonly uriTransformer: IURITransformer,
286
sessionEmitter: Emitter<IFileChange[] | string>,
287
logService: ILogService,
288
private readonly environmentService: IEnvironmentService
289
) {
290
super();
291
292
this.fileWatcher = this._register(new DiskFileSystemProvider(logService));
293
294
this.registerListeners(sessionEmitter);
295
}
296
297
private registerListeners(sessionEmitter: Emitter<IFileChange[] | string>): void {
298
const localChangeEmitter = this._register(new Emitter<readonly IFileChange[]>());
299
300
this._register(localChangeEmitter.event((events) => {
301
sessionEmitter.fire(
302
events.map(e => ({
303
resource: this.uriTransformer.transformOutgoingURI(e.resource),
304
type: e.type,
305
cId: e.cId
306
}))
307
);
308
}));
309
310
this._register(this.fileWatcher.onDidChangeFile(events => localChangeEmitter.fire(events)));
311
this._register(this.fileWatcher.onDidWatchError(error => sessionEmitter.fire(error)));
312
}
313
314
protected getRecursiveWatcherOptions(environmentService: IEnvironmentService): IRecursiveWatcherOptions | undefined {
315
return undefined; // subclasses can override
316
}
317
318
protected getExtraExcludes(environmentService: IEnvironmentService): string[] | undefined {
319
return undefined; // subclasses can override
320
}
321
322
watch(req: number, resource: URI, opts: IWatchOptions): IDisposable {
323
const extraExcludes = this.getExtraExcludes(this.environmentService);
324
if (Array.isArray(extraExcludes)) {
325
opts.excludes = [...opts.excludes, ...extraExcludes];
326
}
327
328
this.watcherRequests.set(req, this.fileWatcher.watch(resource, opts));
329
330
return toDisposable(() => {
331
dispose(this.watcherRequests.get(req));
332
this.watcherRequests.delete(req);
333
});
334
}
335
336
override dispose(): void {
337
for (const [, disposable] of this.watcherRequests) {
338
disposable.dispose();
339
}
340
this.watcherRequests.clear();
341
342
super.dispose();
343
}
344
}
345
346