Skip to content

Commit a0121f2

Browse files
committed
Implement file provider
Reading, watching, saving, etc all seem to work now.
1 parent 98f0013 commit a0121f2

2 files changed

Lines changed: 122 additions & 53 deletions

File tree

channel.ts

Lines changed: 100 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,94 +1,153 @@
11
import * as path from "path";
22

3+
import { VSBuffer } from "vs/base/common/buffer";
34
import { Emitter, Event } from "vs/base/common/event";
5+
import { IDisposable } from "vs/base/common/lifecycle";
6+
import { Schemas } from "vs/base/common/network";
47
import { OS } from "vs/base/common/platform";
5-
import { URI } from "vs/base/common/uri";
8+
import { URI, UriComponents } from "vs/base/common/uri";
69
import { IServerChannel } from "vs/base/parts/ipc/common/ipc";
710
import { IDiagnosticInfo } from "vs/platform/diagnostics/common/diagnosticsService";
811
import { IEnvironmentService } from "vs/platform/environment/common/environment";
912
import { FileDeleteOptions, FileOverwriteOptions, FileType, IStat, IWatchOptions, FileOpenOptions } from "vs/platform/files/common/files";
13+
import { ILogService } from "vs/platform/log/common/log";
1014
import { IRemoteAgentEnvironment } from "vs/platform/remote/common/remoteAgentEnvironment";
15+
import { DiskFileSystemProvider } from "vs/workbench/services/files/node/diskFileSystemProvider";
16+
17+
/**
18+
* Extend the file provider to allow unwatching.
19+
*/
20+
class Watcher extends DiskFileSystemProvider {
21+
public readonly watches = new Map<number, IDisposable>();
22+
23+
public dispose(): void {
24+
this.watches.forEach((w) => w.dispose());
25+
this.watches.clear();
26+
super.dispose();
27+
}
28+
29+
public _watch(req: number, resource: URI, opts: IWatchOptions): void {
30+
this.watches.set(req, this.watch(resource, opts));
31+
}
32+
33+
public unwatch(req: number): void {
34+
this.watches.get(req)!.dispose();
35+
this.watches.delete(req);
36+
}
37+
}
1138

1239
/**
1340
* See: src/vs/platform/remote/common/remoteAgentFileSystemChannel.ts.
1441
*/
1542
export class FileProviderChannel implements IServerChannel {
16-
public listen(_context: any, event: string): Event<any> {
43+
private readonly provider: DiskFileSystemProvider;
44+
private readonly watchers = new Map<string, Watcher>();
45+
46+
public constructor(private readonly logService: ILogService) {
47+
this.provider = new DiskFileSystemProvider(this.logService);
48+
}
49+
50+
public listen(_: unknown, event: string, args?: any): Event<any> {
1751
switch (event) {
52+
// This is where the actual file changes are sent. The watch method just
53+
// adds things that will fire here. That means we have to split up
54+
// watchers based on the session otherwise sessions would get events for
55+
// other sessions. There is also no point in having the watcher unless
56+
// something is listening. I'm not sure there is a different way to
57+
// dispose, anyway.
1858
case "filechange":
19-
// TODO: not sure what to do here yet
20-
return new Emitter().event;
59+
const session = args[0];
60+
const emitter = new Emitter({
61+
onFirstListenerAdd: () => {
62+
const provider = new Watcher(this.logService);
63+
this.watchers.set(session, provider);
64+
provider.onDidChangeFile((events) => {
65+
emitter.fire(events.map((event) => ({
66+
...event,
67+
resource: event.resource.with({ scheme: Schemas.vscodeRemote }),
68+
})));
69+
});
70+
provider.onDidErrorOccur((event) => emitter.fire(event));
71+
},
72+
onLastListenerRemove: () => {
73+
this.watchers.get(session)!.dispose();
74+
this.watchers.delete(session);
75+
},
76+
});
77+
78+
return emitter.event;
2179
}
2280

2381
throw new Error(`Invalid listen "${event}"`);
2482
}
2583

2684
public call(_: unknown, command: string, args?: any): Promise<any> {
27-
console.log("got call", command, args);
2885
switch (command) {
2986
case "stat": return this.stat(args[0]);
3087
case "open": return this.open(args[0], args[1]);
3188
case "close": return this.close(args[0]);
32-
case "read": return this.read(args[0], args[1], args[2], args[3], args[4]);
89+
case "read": return this.read(args[0], args[1], args[2]);
3390
case "write": return this.write(args[0], args[1], args[2], args[3], args[4]);
3491
case "delete": return this.delete(args[0], args[1]);
3592
case "mkdir": return this.mkdir(args[0]);
3693
case "readdir": return this.readdir(args[0]);
3794
case "rename": return this.rename(args[0], args[1], args[2]);
3895
case "copy": return this.copy(args[0], args[1], args[2]);
39-
case "watch": return this.watch(args[0], args[1]);
40-
case "unwatch": return this.unwatch(args[0]), args[1];
96+
case "watch": return this.watch(args[0], args[1], args[2], args[3]);
97+
case "unwatch": return this.unwatch(args[0], args[1]);
4198
}
4299

43100
throw new Error(`Invalid call "${command}"`);
44101
}
45102

46-
private async stat(resource: URI): Promise<IStat> {
47-
throw new Error("not implemented");
103+
private async stat(resource: UriComponents): Promise<IStat> {
104+
return this.provider.stat(URI.from(resource));
48105
}
49106

50-
private async open(resource: URI, opts: FileOpenOptions): Promise<number> {
51-
throw new Error("not implemented");
107+
private async open(resource: UriComponents, opts: FileOpenOptions): Promise<number> {
108+
return this.provider.open(URI.from(resource), opts);
52109
}
53110

54111
private async close(fd: number): Promise<void> {
55-
throw new Error("not implemented");
112+
return this.provider.close(fd);
56113
}
57114

58-
private async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
59-
throw new Error("not implemented");
115+
private async read(fd: number, pos: number, length: number): Promise<[VSBuffer, number]> {
116+
const buffer = VSBuffer.alloc(length);
117+
const bytesRead = await this.provider.read(fd, pos, buffer.buffer, 0, length);
118+
return [buffer, bytesRead];
60119
}
61120

62-
private async write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
63-
throw new Error("not implemented");
121+
private write(fd: number, pos: number, buffer: VSBuffer, offset: number, length: number): Promise<number> {
122+
return this.provider.write(fd, pos, buffer.buffer, offset, length);
64123
}
65124

66-
private async delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
67-
throw new Error("not implemented");
125+
private async delete(resource: UriComponents, opts: FileDeleteOptions): Promise<void> {
126+
return this.provider.delete(URI.from(resource), opts);
68127
}
69128

70-
private async mkdir(resource: URI): Promise<void> {
71-
throw new Error("not implemented");
129+
private async mkdir(resource: UriComponents): Promise<void> {
130+
return this.provider.mkdir(URI.from(resource));
72131
}
73132

74-
private async readdir(resource: URI): Promise<[string, FileType][]> {
75-
throw new Error("not implemented");
133+
private async readdir(resource: UriComponents): Promise<[string, FileType][]> {
134+
return this.provider.readdir(URI.from(resource));
76135
}
77136

78-
private async rename(resource: URI, target: URI, opts: FileOverwriteOptions): Promise<void> {
79-
throw new Error("not implemented");
137+
private async rename(resource: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise<void> {
138+
return this.provider.rename(URI.from(resource), URI.from(target), opts);
80139
}
81140

82-
private copy(resource: URI, target: URI, opts: FileOverwriteOptions): Promise<void> {
83-
throw new Error("not implemented");
141+
private copy(resource: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise<void> {
142+
return this.provider.copy(URI.from(resource), URI.from(target), opts);
84143
}
85144

86-
private watch(resource: URI, opts: IWatchOptions): Promise<void> {
87-
throw new Error("not implemented");
145+
private async watch(session: string, req: number, resource: UriComponents, opts: IWatchOptions): Promise<void> {
146+
this.watchers.get(session)!._watch(req, URI.from(resource), opts);
88147
}
89148

90-
private unwatch(resource: URI): void {
91-
throw new Error("not implemented");
149+
private async unwatch(session: string, req: number): Promise<void> {
150+
this.watchers.get(session)!.unwatch(req);
92151
}
93152
}
94153

@@ -112,16 +171,18 @@ export class ExtensionEnvironmentChannel implements IServerChannel {
112171
}
113172

114173
private async getEnvironmentData(): Promise<IRemoteAgentEnvironment> {
174+
// TODO: this `with` stuff feels a bit jank.
175+
// Maybe it should already come in like this instead.
115176
return {
116177
pid: process.pid,
117-
appRoot: URI.file(this.environment.appRoot),
118-
appSettingsHome: this.environment.appSettingsHome,
119-
settingsPath: this.environment.machineSettingsHome,
120-
logsPath: URI.file(this.environment.logsPath),
121-
extensionsPath: URI.file(this.environment.extensionsPath),
122-
extensionHostLogsPath: URI.file(path.join(this.environment.logsPath, "extension-host")), // TODO
123-
globalStorageHome: URI.file(this.environment.globalStorageHome),
124-
userHome: URI.file(this.environment.userHome),
178+
appRoot: URI.file(this.environment.appRoot).with({ scheme: Schemas.vscodeRemote }),
179+
appSettingsHome: this.environment.appSettingsHome.with({ scheme: Schemas.vscodeRemote }),
180+
settingsPath: this.environment.machineSettingsHome.with({ scheme: Schemas.vscodeRemote }),
181+
logsPath: URI.file(this.environment.logsPath).with({ scheme: Schemas.vscodeRemote }),
182+
extensionsPath: URI.file(this.environment.extensionsPath).with({ scheme: Schemas.vscodeRemote }),
183+
extensionHostLogsPath: URI.file(path.join(this.environment.logsPath, "extension-host")).with({ scheme: Schemas.vscodeRemote }), // TODO
184+
globalStorageHome: URI.file(this.environment.globalStorageHome).with({ scheme: Schemas.vscodeRemote }),
185+
userHome: URI.file(this.environment.userHome).with({ scheme: Schemas.vscodeRemote }),
125186
extensions: [], // TODO
126187
os: OS,
127188
};

server.ts

Lines changed: 22 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ import * as url from "url";
77

88
import { Emitter } from "vs/base/common/event";
99
import { getMediaMime } from "vs/base/common/mime";
10+
import { Schemas } from "vs/base/common/network";
1011
import { extname } from "vs/base/common/path";
12+
import { URI } from "vs/base/common/uri";
1113
import { IPCServer, ClientConnectionEvent } from "vs/base/parts/ipc/common/ipc";
1214
import { validatePaths } from "vs/code/node/paths";
1315
import { parseMainProcessArgv } from "vs/platform/environment/node/argvHelper";
@@ -16,8 +18,10 @@ import { EnvironmentService } from "vs/platform/environment/node/environmentServ
1618
import { InstantiationService } from "vs/platform/instantiation/common/instantiationService";
1719
import { ConsoleLogMainService } from "vs/platform/log/common/log";
1820
import { LogLevelSetterChannel } from "vs/platform/log/common/logIpc";
21+
import { IProductConfiguration } from "vs/platform/product/common/product";
1922
import { ConnectionType } from "vs/platform/remote/common/remoteAgentConnection";
2023
import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from "vs/platform/remote/common/remoteAgentFileSystemChannel";
24+
import { IWorkbenchConstructionOptions } from "vs/workbench/workbench.web.api";
2125

2226
import { Connection, Server as IServer } from "vs/server/connection";
2327
import { ExtensionEnvironmentChannel, FileProviderChannel } from "vs/server/channel";
@@ -29,6 +33,13 @@ export enum HttpCode {
2933
BadRequest = 400,
3034
}
3135

36+
export interface Options {
37+
WORKBENCH_WEB_CONGIGURATION: IWorkbenchConstructionOptions;
38+
REMOTE_USER_DATA_URI: URI;
39+
PRODUCT_CONFIGURATION: IProductConfiguration | null;
40+
CONNECTION_AUTH_TOKEN: string;
41+
}
42+
3243
export class HttpError extends Error {
3344
public constructor(message: string, public readonly code: number) {
3445
super(message);
@@ -53,6 +64,8 @@ export class Server implements IServer {
5364
// The web server.
5465
private readonly server: http.Server;
5566

67+
private readonly environmentService: EnvironmentService;
68+
5669
// Persistent connections. These can reconnect within a timeout. Individual
5770
// sockets will add connections made through them to this map and remove them
5871
// when they close.
@@ -97,7 +110,7 @@ export class Server implements IServer {
97110
return process.exit(1);
98111
}
99112

100-
const environmentService = new EnvironmentService(args, process.execPath);
113+
this.environmentService = new EnvironmentService(args, process.execPath);
101114

102115
// TODO: might want to use spdlog.
103116
const logService = new ConsoleLogMainService();
@@ -107,11 +120,11 @@ export class Server implements IServer {
107120
instantiationService.invokeFunction(() => {
108121
this.ipc.registerChannel(
109122
REMOTE_FILE_SYSTEM_CHANNEL_NAME,
110-
new FileProviderChannel(),
123+
new FileProviderChannel(logService),
111124
);
112125
this.ipc.registerChannel(
113126
"remoteextensionsenvironment",
114-
new ExtensionEnvironmentChannel(environmentService),
127+
new ExtensionEnvironmentChannel(this.environmentService),
115128
);
116129
});
117130
}
@@ -133,25 +146,20 @@ export class Server implements IServer {
133146

134147
let html = await util.promisify(fs.readFile)(htmlPath, "utf8");
135148

136-
const options = {
137-
WEBVIEW_ENDPOINT: {},
149+
const options: Options = {
138150
WORKBENCH_WEB_CONGIGURATION: {
139-
remoteAuthority: request.headers.host,
140-
},
141-
REMOTE_USER_DATA_URI: {
142-
scheme: "http",
143-
authority: request.headers.host,
144-
path: "/",
151+
remoteAuthority: request.headers.host as string,
145152
},
146-
PRODUCT_CONFIGURATION: {},
147-
CONNECTION_AUTH_TOKEN: {}
153+
REMOTE_USER_DATA_URI: this.environmentService.webUserDataHome.with({ scheme: Schemas.vscodeRemote }),
154+
PRODUCT_CONFIGURATION: null,
155+
CONNECTION_AUTH_TOKEN: "",
148156
};
149157

150158
Object.keys(options).forEach((key) => {
151159
html = html.replace(`"{{${key}}}"`, `'${JSON.stringify(options[key])}'`);
152160
});
153161

154-
html = html.replace('{{WEBVIEW_ENDPOINT}}', JSON.stringify(options.WEBVIEW_ENDPOINT));
162+
html = html.replace('{{WEBVIEW_ENDPOINT}}', ""); // TODO
155163

156164
return [html, {
157165
"Content-Type": "text/html",

0 commit comments

Comments
 (0)