Implement file provider
Reading, watching, saving, etc all seem to work now.
This commit is contained in:
parent
98f001395c
commit
a0121f2f0c
139
channel.ts
139
channel.ts
@ -1,94 +1,153 @@
|
|||||||
import * as path from "path";
|
import * as path from "path";
|
||||||
|
|
||||||
|
import { VSBuffer } from "vs/base/common/buffer";
|
||||||
import { Emitter, Event } from "vs/base/common/event";
|
import { Emitter, Event } from "vs/base/common/event";
|
||||||
|
import { IDisposable } from "vs/base/common/lifecycle";
|
||||||
|
import { Schemas } from "vs/base/common/network";
|
||||||
import { OS } from "vs/base/common/platform";
|
import { OS } from "vs/base/common/platform";
|
||||||
import { URI } from "vs/base/common/uri";
|
import { URI, UriComponents } from "vs/base/common/uri";
|
||||||
import { IServerChannel } from "vs/base/parts/ipc/common/ipc";
|
import { IServerChannel } from "vs/base/parts/ipc/common/ipc";
|
||||||
import { IDiagnosticInfo } from "vs/platform/diagnostics/common/diagnosticsService";
|
import { IDiagnosticInfo } from "vs/platform/diagnostics/common/diagnosticsService";
|
||||||
import { IEnvironmentService } from "vs/platform/environment/common/environment";
|
import { IEnvironmentService } from "vs/platform/environment/common/environment";
|
||||||
import { FileDeleteOptions, FileOverwriteOptions, FileType, IStat, IWatchOptions, FileOpenOptions } from "vs/platform/files/common/files";
|
import { FileDeleteOptions, FileOverwriteOptions, FileType, IStat, IWatchOptions, FileOpenOptions } from "vs/platform/files/common/files";
|
||||||
|
import { ILogService } from "vs/platform/log/common/log";
|
||||||
import { IRemoteAgentEnvironment } from "vs/platform/remote/common/remoteAgentEnvironment";
|
import { IRemoteAgentEnvironment } from "vs/platform/remote/common/remoteAgentEnvironment";
|
||||||
|
import { DiskFileSystemProvider } from "vs/workbench/services/files/node/diskFileSystemProvider";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend the file provider to allow unwatching.
|
||||||
|
*/
|
||||||
|
class Watcher extends DiskFileSystemProvider {
|
||||||
|
public readonly watches = new Map<number, IDisposable>();
|
||||||
|
|
||||||
|
public dispose(): void {
|
||||||
|
this.watches.forEach((w) => w.dispose());
|
||||||
|
this.watches.clear();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public _watch(req: number, resource: URI, opts: IWatchOptions): void {
|
||||||
|
this.watches.set(req, this.watch(resource, opts));
|
||||||
|
}
|
||||||
|
|
||||||
|
public unwatch(req: number): void {
|
||||||
|
this.watches.get(req)!.dispose();
|
||||||
|
this.watches.delete(req);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* See: src/vs/platform/remote/common/remoteAgentFileSystemChannel.ts.
|
* See: src/vs/platform/remote/common/remoteAgentFileSystemChannel.ts.
|
||||||
*/
|
*/
|
||||||
export class FileProviderChannel implements IServerChannel {
|
export class FileProviderChannel implements IServerChannel {
|
||||||
public listen(_context: any, event: string): Event<any> {
|
private readonly provider: DiskFileSystemProvider;
|
||||||
|
private readonly watchers = new Map<string, Watcher>();
|
||||||
|
|
||||||
|
public constructor(private readonly logService: ILogService) {
|
||||||
|
this.provider = new DiskFileSystemProvider(this.logService);
|
||||||
|
}
|
||||||
|
|
||||||
|
public listen(_: unknown, event: string, args?: any): Event<any> {
|
||||||
switch (event) {
|
switch (event) {
|
||||||
|
// This is where the actual file changes are sent. The watch method just
|
||||||
|
// adds things that will fire here. That means we have to split up
|
||||||
|
// watchers based on the session otherwise sessions would get events for
|
||||||
|
// other sessions. There is also no point in having the watcher unless
|
||||||
|
// something is listening. I'm not sure there is a different way to
|
||||||
|
// dispose, anyway.
|
||||||
case "filechange":
|
case "filechange":
|
||||||
// TODO: not sure what to do here yet
|
const session = args[0];
|
||||||
return new Emitter().event;
|
const emitter = new Emitter({
|
||||||
|
onFirstListenerAdd: () => {
|
||||||
|
const provider = new Watcher(this.logService);
|
||||||
|
this.watchers.set(session, provider);
|
||||||
|
provider.onDidChangeFile((events) => {
|
||||||
|
emitter.fire(events.map((event) => ({
|
||||||
|
...event,
|
||||||
|
resource: event.resource.with({ scheme: Schemas.vscodeRemote }),
|
||||||
|
})));
|
||||||
|
});
|
||||||
|
provider.onDidErrorOccur((event) => emitter.fire(event));
|
||||||
|
},
|
||||||
|
onLastListenerRemove: () => {
|
||||||
|
this.watchers.get(session)!.dispose();
|
||||||
|
this.watchers.delete(session);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return emitter.event;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Invalid listen "${event}"`);
|
throw new Error(`Invalid listen "${event}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
public call(_: unknown, command: string, args?: any): Promise<any> {
|
public call(_: unknown, command: string, args?: any): Promise<any> {
|
||||||
console.log("got call", command, args);
|
|
||||||
switch (command) {
|
switch (command) {
|
||||||
case "stat": return this.stat(args[0]);
|
case "stat": return this.stat(args[0]);
|
||||||
case "open": return this.open(args[0], args[1]);
|
case "open": return this.open(args[0], args[1]);
|
||||||
case "close": return this.close(args[0]);
|
case "close": return this.close(args[0]);
|
||||||
case "read": return this.read(args[0], args[1], args[2], args[3], args[4]);
|
case "read": return this.read(args[0], args[1], args[2]);
|
||||||
case "write": return this.write(args[0], args[1], args[2], args[3], args[4]);
|
case "write": return this.write(args[0], args[1], args[2], args[3], args[4]);
|
||||||
case "delete": return this.delete(args[0], args[1]);
|
case "delete": return this.delete(args[0], args[1]);
|
||||||
case "mkdir": return this.mkdir(args[0]);
|
case "mkdir": return this.mkdir(args[0]);
|
||||||
case "readdir": return this.readdir(args[0]);
|
case "readdir": return this.readdir(args[0]);
|
||||||
case "rename": return this.rename(args[0], args[1], args[2]);
|
case "rename": return this.rename(args[0], args[1], args[2]);
|
||||||
case "copy": return this.copy(args[0], args[1], args[2]);
|
case "copy": return this.copy(args[0], args[1], args[2]);
|
||||||
case "watch": return this.watch(args[0], args[1]);
|
case "watch": return this.watch(args[0], args[1], args[2], args[3]);
|
||||||
case "unwatch": return this.unwatch(args[0]), args[1];
|
case "unwatch": return this.unwatch(args[0], args[1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new Error(`Invalid call "${command}"`);
|
throw new Error(`Invalid call "${command}"`);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async stat(resource: URI): Promise<IStat> {
|
private async stat(resource: UriComponents): Promise<IStat> {
|
||||||
throw new Error("not implemented");
|
return this.provider.stat(URI.from(resource));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async open(resource: URI, opts: FileOpenOptions): Promise<number> {
|
private async open(resource: UriComponents, opts: FileOpenOptions): Promise<number> {
|
||||||
throw new Error("not implemented");
|
return this.provider.open(URI.from(resource), opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async close(fd: number): Promise<void> {
|
private async close(fd: number): Promise<void> {
|
||||||
throw new Error("not implemented");
|
return this.provider.close(fd);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async read(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
|
private async read(fd: number, pos: number, length: number): Promise<[VSBuffer, number]> {
|
||||||
throw new Error("not implemented");
|
const buffer = VSBuffer.alloc(length);
|
||||||
|
const bytesRead = await this.provider.read(fd, pos, buffer.buffer, 0, length);
|
||||||
|
return [buffer, bytesRead];
|
||||||
}
|
}
|
||||||
|
|
||||||
private async write(fd: number, pos: number, data: Uint8Array, offset: number, length: number): Promise<number> {
|
private write(fd: number, pos: number, buffer: VSBuffer, offset: number, length: number): Promise<number> {
|
||||||
throw new Error("not implemented");
|
return this.provider.write(fd, pos, buffer.buffer, offset, length);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async delete(resource: URI, opts: FileDeleteOptions): Promise<void> {
|
private async delete(resource: UriComponents, opts: FileDeleteOptions): Promise<void> {
|
||||||
throw new Error("not implemented");
|
return this.provider.delete(URI.from(resource), opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async mkdir(resource: URI): Promise<void> {
|
private async mkdir(resource: UriComponents): Promise<void> {
|
||||||
throw new Error("not implemented");
|
return this.provider.mkdir(URI.from(resource));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async readdir(resource: URI): Promise<[string, FileType][]> {
|
private async readdir(resource: UriComponents): Promise<[string, FileType][]> {
|
||||||
throw new Error("not implemented");
|
return this.provider.readdir(URI.from(resource));
|
||||||
}
|
}
|
||||||
|
|
||||||
private async rename(resource: URI, target: URI, opts: FileOverwriteOptions): Promise<void> {
|
private async rename(resource: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise<void> {
|
||||||
throw new Error("not implemented");
|
return this.provider.rename(URI.from(resource), URI.from(target), opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
private copy(resource: URI, target: URI, opts: FileOverwriteOptions): Promise<void> {
|
private copy(resource: UriComponents, target: UriComponents, opts: FileOverwriteOptions): Promise<void> {
|
||||||
throw new Error("not implemented");
|
return this.provider.copy(URI.from(resource), URI.from(target), opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
private watch(resource: URI, opts: IWatchOptions): Promise<void> {
|
private async watch(session: string, req: number, resource: UriComponents, opts: IWatchOptions): Promise<void> {
|
||||||
throw new Error("not implemented");
|
this.watchers.get(session)!._watch(req, URI.from(resource), opts);
|
||||||
}
|
}
|
||||||
|
|
||||||
private unwatch(resource: URI): void {
|
private async unwatch(session: string, req: number): Promise<void> {
|
||||||
throw new Error("not implemented");
|
this.watchers.get(session)!.unwatch(req);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -112,16 +171,18 @@ export class ExtensionEnvironmentChannel implements IServerChannel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async getEnvironmentData(): Promise<IRemoteAgentEnvironment> {
|
private async getEnvironmentData(): Promise<IRemoteAgentEnvironment> {
|
||||||
|
// TODO: this `with` stuff feels a bit jank.
|
||||||
|
// Maybe it should already come in like this instead.
|
||||||
return {
|
return {
|
||||||
pid: process.pid,
|
pid: process.pid,
|
||||||
appRoot: URI.file(this.environment.appRoot),
|
appRoot: URI.file(this.environment.appRoot).with({ scheme: Schemas.vscodeRemote }),
|
||||||
appSettingsHome: this.environment.appSettingsHome,
|
appSettingsHome: this.environment.appSettingsHome.with({ scheme: Schemas.vscodeRemote }),
|
||||||
settingsPath: this.environment.machineSettingsHome,
|
settingsPath: this.environment.machineSettingsHome.with({ scheme: Schemas.vscodeRemote }),
|
||||||
logsPath: URI.file(this.environment.logsPath),
|
logsPath: URI.file(this.environment.logsPath).with({ scheme: Schemas.vscodeRemote }),
|
||||||
extensionsPath: URI.file(this.environment.extensionsPath),
|
extensionsPath: URI.file(this.environment.extensionsPath).with({ scheme: Schemas.vscodeRemote }),
|
||||||
extensionHostLogsPath: URI.file(path.join(this.environment.logsPath, "extension-host")), // TODO
|
extensionHostLogsPath: URI.file(path.join(this.environment.logsPath, "extension-host")).with({ scheme: Schemas.vscodeRemote }), // TODO
|
||||||
globalStorageHome: URI.file(this.environment.globalStorageHome),
|
globalStorageHome: URI.file(this.environment.globalStorageHome).with({ scheme: Schemas.vscodeRemote }),
|
||||||
userHome: URI.file(this.environment.userHome),
|
userHome: URI.file(this.environment.userHome).with({ scheme: Schemas.vscodeRemote }),
|
||||||
extensions: [], // TODO
|
extensions: [], // TODO
|
||||||
os: OS,
|
os: OS,
|
||||||
};
|
};
|
||||||
|
36
server.ts
36
server.ts
@ -7,7 +7,9 @@ import * as url from "url";
|
|||||||
|
|
||||||
import { Emitter } from "vs/base/common/event";
|
import { Emitter } from "vs/base/common/event";
|
||||||
import { getMediaMime } from "vs/base/common/mime";
|
import { getMediaMime } from "vs/base/common/mime";
|
||||||
|
import { Schemas } from "vs/base/common/network";
|
||||||
import { extname } from "vs/base/common/path";
|
import { extname } from "vs/base/common/path";
|
||||||
|
import { URI } from "vs/base/common/uri";
|
||||||
import { IPCServer, ClientConnectionEvent } from "vs/base/parts/ipc/common/ipc";
|
import { IPCServer, ClientConnectionEvent } from "vs/base/parts/ipc/common/ipc";
|
||||||
import { validatePaths } from "vs/code/node/paths";
|
import { validatePaths } from "vs/code/node/paths";
|
||||||
import { parseMainProcessArgv } from "vs/platform/environment/node/argvHelper";
|
import { parseMainProcessArgv } from "vs/platform/environment/node/argvHelper";
|
||||||
@ -16,8 +18,10 @@ import { EnvironmentService } from "vs/platform/environment/node/environmentServ
|
|||||||
import { InstantiationService } from "vs/platform/instantiation/common/instantiationService";
|
import { InstantiationService } from "vs/platform/instantiation/common/instantiationService";
|
||||||
import { ConsoleLogMainService } from "vs/platform/log/common/log";
|
import { ConsoleLogMainService } from "vs/platform/log/common/log";
|
||||||
import { LogLevelSetterChannel } from "vs/platform/log/common/logIpc";
|
import { LogLevelSetterChannel } from "vs/platform/log/common/logIpc";
|
||||||
|
import { IProductConfiguration } from "vs/platform/product/common/product";
|
||||||
import { ConnectionType } from "vs/platform/remote/common/remoteAgentConnection";
|
import { ConnectionType } from "vs/platform/remote/common/remoteAgentConnection";
|
||||||
import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from "vs/platform/remote/common/remoteAgentFileSystemChannel";
|
import { REMOTE_FILE_SYSTEM_CHANNEL_NAME } from "vs/platform/remote/common/remoteAgentFileSystemChannel";
|
||||||
|
import { IWorkbenchConstructionOptions } from "vs/workbench/workbench.web.api";
|
||||||
|
|
||||||
import { Connection, Server as IServer } from "vs/server/connection";
|
import { Connection, Server as IServer } from "vs/server/connection";
|
||||||
import { ExtensionEnvironmentChannel, FileProviderChannel } from "vs/server/channel";
|
import { ExtensionEnvironmentChannel, FileProviderChannel } from "vs/server/channel";
|
||||||
@ -29,6 +33,13 @@ export enum HttpCode {
|
|||||||
BadRequest = 400,
|
BadRequest = 400,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Options {
|
||||||
|
WORKBENCH_WEB_CONGIGURATION: IWorkbenchConstructionOptions;
|
||||||
|
REMOTE_USER_DATA_URI: URI;
|
||||||
|
PRODUCT_CONFIGURATION: IProductConfiguration | null;
|
||||||
|
CONNECTION_AUTH_TOKEN: string;
|
||||||
|
}
|
||||||
|
|
||||||
export class HttpError extends Error {
|
export class HttpError extends Error {
|
||||||
public constructor(message: string, public readonly code: number) {
|
public constructor(message: string, public readonly code: number) {
|
||||||
super(message);
|
super(message);
|
||||||
@ -53,6 +64,8 @@ export class Server implements IServer {
|
|||||||
// The web server.
|
// The web server.
|
||||||
private readonly server: http.Server;
|
private readonly server: http.Server;
|
||||||
|
|
||||||
|
private readonly environmentService: EnvironmentService;
|
||||||
|
|
||||||
// Persistent connections. These can reconnect within a timeout. Individual
|
// Persistent connections. These can reconnect within a timeout. Individual
|
||||||
// sockets will add connections made through them to this map and remove them
|
// sockets will add connections made through them to this map and remove them
|
||||||
// when they close.
|
// when they close.
|
||||||
@ -97,7 +110,7 @@ export class Server implements IServer {
|
|||||||
return process.exit(1);
|
return process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
const environmentService = new EnvironmentService(args, process.execPath);
|
this.environmentService = new EnvironmentService(args, process.execPath);
|
||||||
|
|
||||||
// TODO: might want to use spdlog.
|
// TODO: might want to use spdlog.
|
||||||
const logService = new ConsoleLogMainService();
|
const logService = new ConsoleLogMainService();
|
||||||
@ -107,11 +120,11 @@ export class Server implements IServer {
|
|||||||
instantiationService.invokeFunction(() => {
|
instantiationService.invokeFunction(() => {
|
||||||
this.ipc.registerChannel(
|
this.ipc.registerChannel(
|
||||||
REMOTE_FILE_SYSTEM_CHANNEL_NAME,
|
REMOTE_FILE_SYSTEM_CHANNEL_NAME,
|
||||||
new FileProviderChannel(),
|
new FileProviderChannel(logService),
|
||||||
);
|
);
|
||||||
this.ipc.registerChannel(
|
this.ipc.registerChannel(
|
||||||
"remoteextensionsenvironment",
|
"remoteextensionsenvironment",
|
||||||
new ExtensionEnvironmentChannel(environmentService),
|
new ExtensionEnvironmentChannel(this.environmentService),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -133,25 +146,20 @@ export class Server implements IServer {
|
|||||||
|
|
||||||
let html = await util.promisify(fs.readFile)(htmlPath, "utf8");
|
let html = await util.promisify(fs.readFile)(htmlPath, "utf8");
|
||||||
|
|
||||||
const options = {
|
const options: Options = {
|
||||||
WEBVIEW_ENDPOINT: {},
|
|
||||||
WORKBENCH_WEB_CONGIGURATION: {
|
WORKBENCH_WEB_CONGIGURATION: {
|
||||||
remoteAuthority: request.headers.host,
|
remoteAuthority: request.headers.host as string,
|
||||||
},
|
},
|
||||||
REMOTE_USER_DATA_URI: {
|
REMOTE_USER_DATA_URI: this.environmentService.webUserDataHome.with({ scheme: Schemas.vscodeRemote }),
|
||||||
scheme: "http",
|
PRODUCT_CONFIGURATION: null,
|
||||||
authority: request.headers.host,
|
CONNECTION_AUTH_TOKEN: "",
|
||||||
path: "/",
|
|
||||||
},
|
|
||||||
PRODUCT_CONFIGURATION: {},
|
|
||||||
CONNECTION_AUTH_TOKEN: {}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
Object.keys(options).forEach((key) => {
|
Object.keys(options).forEach((key) => {
|
||||||
html = html.replace(`"{{${key}}}"`, `'${JSON.stringify(options[key])}'`);
|
html = html.replace(`"{{${key}}}"`, `'${JSON.stringify(options[key])}'`);
|
||||||
});
|
});
|
||||||
|
|
||||||
html = html.replace('{{WEBVIEW_ENDPOINT}}', JSON.stringify(options.WEBVIEW_ENDPOINT));
|
html = html.replace('{{WEBVIEW_ENDPOINT}}', ""); // TODO
|
||||||
|
|
||||||
return [html, {
|
return [html, {
|
||||||
"Content-Type": "text/html",
|
"Content-Type": "text/html",
|
||||||
|
Reference in New Issue
Block a user