diff --git a/packages/ide/src/client.ts b/packages/ide/src/client.ts index ac48317e..6961f739 100644 --- a/packages/ide/src/client.ts +++ b/packages/ide/src/client.ts @@ -1,5 +1,6 @@ +import { Event } from "@coder/events"; import { field, logger, time, Time } from "@coder/logger"; -import { InitData } from "@coder/protocol"; +import { InitData, ISharedProcessData } from "@coder/protocol"; import { retry, Retry } from "./retry"; import { client } from "./fill/client"; import { Clipboard, clipboard } from "./fill/clipboard"; @@ -167,6 +168,10 @@ export abstract class Client { return client.initData; } + public get onSharedProcessActive(): Event { + return client.onSharedProcessActive; + } + /** * Initialize the IDE. */ diff --git a/packages/ide/src/fill/net.ts b/packages/ide/src/fill/net.ts index ab534f6a..89affd7e 100644 --- a/packages/ide/src/fill/net.ts +++ b/packages/ide/src/fill/net.ts @@ -1,3 +1,4 @@ import { Net } from "@coder/protocol"; +import { client } from "./client"; -export = new Net(); +export = new Net(client); diff --git a/packages/protocol/src/browser/client.ts b/packages/protocol/src/browser/client.ts index 91250336..4fda7ea5 100644 --- a/packages/protocol/src/browser/client.ts +++ b/packages/protocol/src/browser/client.ts @@ -1,6 +1,6 @@ -import { ReadWriteConnection, InitData, OperatingSystem } from "../common/connection"; +import { ReadWriteConnection, InitData, OperatingSystem, ISharedProcessData } from "../common/connection"; import { NewEvalMessage, ServerMessage, EvalDoneMessage, EvalFailedMessage, TypedValue, ClientMessage, NewSessionMessage, TTYDimensions, SessionOutputMessage, CloseSessionInputMessage, WorkingInitMessage, NewConnectionMessage } from "../proto"; -import { Emitter } from "@coder/events"; +import { Emitter, Event } from "@coder/events"; import { logger, field } from "@coder/logger"; import { ChildProcess, SpawnOptions, ServerProcess, ServerSocket, Socket } from "./command"; @@ -19,17 +19,17 @@ export class Client { private readonly connections: Map = new Map(); private _initData: InitData | undefined; - private initDataEmitter: Emitter = new Emitter(); + private initDataEmitter = new Emitter(); private initDataPromise: Promise; + private sharedProcessActiveEmitter = new Emitter(); + /** * @param connection Established connection to the server */ public constructor( private readonly connection: ReadWriteConnection, ) { - this.initDataEmitter = new Emitter(); - connection.onMessage((data) => { try { this.handleMessage(ServerMessage.deserializeBinary(data)); @@ -47,6 +47,10 @@ export class Client { return this.initDataPromise; } + public get onSharedProcessActive(): Event { + return this.sharedProcessActiveEmitter.event; + } + public evaluate(func: () => R | Promise): Promise; public evaluate(func: (a1: T1) => R | Promise, a1: T1): Promise; public evaluate(func: (a1: T1, a2: T2) => R | Promise, a1: T1, a2: T2): Promise; @@ -315,6 +319,10 @@ export class Client { } c.emit("end"); this.connections.delete(message.getConnectionFailure()!.getId()); + } else if (message.hasSharedProcessActive()) { + this.sharedProcessActiveEmitter.emit({ + socketPath: message.getSharedProcessActive()!.getSocketPath(), + }); } } } diff --git a/packages/protocol/src/browser/modules/net.ts b/packages/protocol/src/browser/modules/net.ts index ace3faf4..a243fd37 100644 --- a/packages/protocol/src/browser/modules/net.ts +++ b/packages/protocol/src/browser/modules/net.ts @@ -1,5 +1,5 @@ import * as net from "net"; -import { Client } from '../client'; +import { Client } from "../client"; type NodeNet = typeof net; @@ -24,6 +24,7 @@ export class Net implements NodeNet { throw new Error("not implemented"); } + // tslint:disable-next-line no-any public createConnection(...args: any[]): net.Socket { //@ts-ignore return this.client.createConnection(...args) as net.Socket; diff --git a/packages/protocol/src/common/connection.ts b/packages/protocol/src/common/connection.ts index 1badf50f..699389df 100644 --- a/packages/protocol/src/common/connection.ts +++ b/packages/protocol/src/common/connection.ts @@ -20,4 +20,8 @@ export interface InitData { readonly workingDirectory: string; readonly homeDirectory: string; readonly tmpDirectory: string; -} \ No newline at end of file +} + +export interface ISharedProcessData { + readonly socketPath: string; +} diff --git a/packages/server/src/cli.ts b/packages/server/src/cli.ts index bca6ee22..1622461b 100644 --- a/packages/server/src/cli.ts +++ b/packages/server/src/cli.ts @@ -117,9 +117,11 @@ export class Entry extends Command { app.wss.on("connection", (ws, req) => { const id = clientId++; - if (sharedProcess.state === SharedProcessState.Ready) { - sendSharedProcessReady(ws); - } + ws.on("open", () => { + if (sharedProcess.state === SharedProcessState.Ready) { + sendSharedProcessReady(ws); + } + }); logger.info(`WebSocket opened \u001B[0m${req.url}`, field("client", id), field("ip", req.socket.remoteAddress)); diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index a6377b3c..efae772b 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -25,12 +25,6 @@ export const createApp = (registerMiddleware?: (app: express.Application) => voi }; wss.on("connection", (ws: WebSocket, req) => { - const spm = (req).sharedProcessInit as SharedProcessInitMessage; - if (!spm) { - ws.close(); - return; - } - const connection: ReadWriteConnection = { onMessage: (cb): void => { ws.addEventListener("message", (event) => cb(event.data)); @@ -44,7 +38,7 @@ export const createApp = (registerMiddleware?: (app: express.Application) => voi ...options, forkProvider: (message: NewSessionMessage): ChildProcess => { let proc: ChildProcess; - + if (message.getIsBootstrapFork()) { proc = forkModule(message.getCommand()); } else { diff --git a/packages/vscode/src/client.ts b/packages/vscode/src/client.ts index 7b51b05c..5b826f06 100644 --- a/packages/vscode/src/client.ts +++ b/packages/vscode/src/client.ts @@ -1,10 +1,12 @@ import "./fill/require"; import "./fill/storageDatabase"; import "./fill/windowsService"; +import * as paths from "./fill/paths"; +import "./fill/dom"; +import "./vscode.scss"; -import { fork } from "child_process"; +import { createConnection } from "net"; import { Client as IDEClient, IURI, IURIFactory } from "@coder/ide"; -import { logger } from "@coder/logger"; import { registerContextMenuListener } from "vs/base/parts/contextmenu/electron-main/contextmenu"; import { LogLevel } from "vs/platform/log/common/log"; @@ -12,36 +14,42 @@ import { toLocalISOString } from "vs/base/common/date"; // import { RawContextKey, IContextKeyService } from "vs/platform/contextkey/common/contextkey"; import { URI } from "vs/base/common/uri"; -import { Protocol, ISharedProcessInitData } from "./protocol"; -import * as paths from "./fill/paths"; -import "./firefox"; +import { Protocol } from "vs/base/parts/ipc/node/ipc.net"; export class Client extends IDEClient { - private readonly sharedProcessLogger = logger.named("shr proc"); private readonly windowId = parseInt(toLocalISOString(new Date()).replace(/[-:.TZ]/g, ""), 10); - private readonly version = "hello"; // TODO: pull from package.json probably - private readonly bootstrapForkLocation = "/bootstrap"; // TODO: location. + public readonly protocolPromise: Promise; - private protoResolve: ((protocol: Protocol) => void) | undefined; + public protoResolve: ((protocol: Protocol) => void) | undefined; public constructor() { super(); - process.env.VSCODE_LOGS = "/tmp/vscode-online/logs"; // TODO: use tmpdir or get log directory from init data. this.protocolPromise = new Promise((resolve): void => { this.protoResolve = resolve; }); } protected initialize(): Promise { - this.task("Start shared process", 5, async () => { - const protocol = await this.forkSharedProcess(); - this.protoResolve!(protocol); + this.task("Connect to shared process", 5, async () => { + await new Promise((resolve, reject): void => { + const listener = this.onSharedProcessActive((data) => { + listener.dispose(); + const socket = createConnection(data.socketPath, resolve); + socket.once("error", () => { + reject(); + }); + this.protoResolve!(new Protocol(socket)); + }); + }); }).catch(() => undefined); registerContextMenuListener(); return this.task("Start workbench", 1000, async (initData) => { + paths.paths.appData = initData.dataDirectory; + paths.paths.defaultUserData = initData.dataDirectory; + const { startup } = require("./startup"); await startup({ machineId: "1", @@ -57,6 +65,33 @@ export class Client extends IDEClient { folderUri: URI.file(initData.dataDirectory), }); + // TODO: Set notification service for retrying. + // this.retry.setNotificationService({ + // prompt: (severity, message, buttons, onCancel) => { + // const handle = getNotificationService().prompt(severity, message, buttons, onCancel); + // return { + // close: () => handle.close(), + // updateMessage: (message) => handle.updateMessage(message), + // updateButtons: (buttons) => handle.updateActions({ + // primary: buttons.map((button) => ({ + // id: undefined, + // label: button.label, + // tooltip: undefined, + // class: undefined, + // enabled: true, + // checked: false, + // radio: false, + // dispose: () => undefined, + // run: () => { + // button.run(); + // return Promise.resolve(); + // }, + // })), + // }), + // }; + // } + // }); + // TODO: Set up clipboard context. // const workbench = workbenchShell.workbench; // const contextKeys = workbench.workbenchParams.serviceCollection.get(IContextKeyService) as IContextKeyService; @@ -69,50 +104,6 @@ export class Client extends IDEClient { }, this.initData); } - public async forkSharedProcess(): Promise { - const childProcess = fork(this.bootstrapForkLocation, ["--shared"], { - env: { - "VSCODE_ALLOW_IO": "true", - "AMD_ENTRYPOINT": "vs/code/electron-browser/sharedProcess/sharedProcessClient", - }, - }); - - childProcess.stderr.on("data", (data) => { - this.sharedProcessLogger.error("stderr: " + data); - }); - - const protocol = Protocol.fromProcess(childProcess); - await new Promise((resolve, reject): void => { - protocol.onClose(() => { - reject(new Error("unable to establish connection to shared process")); - }); - - const listener = protocol.onMessage((message) => { - const messageStr = message.toString(); - this.sharedProcessLogger.debug(messageStr); - switch (messageStr) { - case "handshake:hello": - protocol.send(Buffer.from(JSON.stringify({ - // Using the version so if we get a new mount, it spins up a new - // shared process. - socketPath: `/tmp/vscode-online/shared-${this.version}.sock`, - serviceUrl: "", // TODO - logsDir: process.env.VSCODE_LOGS, - windowId: this.windowId, - logLevel: LogLevel.Info, - } as ISharedProcessInitData))); - break; - case "handshake:ready": - listener.dispose(); - resolve(); - break; - } - }); - }); - - return protocol; - } - protected createUriFactory(): IURIFactory { return { // TODO: not sure why this is an error. @@ -126,8 +117,3 @@ export class Client extends IDEClient { } export const client = new Client(); - -client.initData.then((initData) => { - paths.appData = initData.dataDirectory; - paths.defaultUserData = initData.dataDirectory; -}); \ No newline at end of file diff --git a/packages/vscode/src/firefox.ts b/packages/vscode/src/fill/dom.ts similarity index 89% rename from packages/vscode/src/firefox.ts rename to packages/vscode/src/fill/dom.ts index 7c5f2783..d176c41b 100644 --- a/packages/vscode/src/firefox.ts +++ b/packages/vscode/src/fill/dom.ts @@ -1,5 +1,4 @@ -import "./firefox.scss"; - +// Firefox has no implementation of toElement. if (!("toElement" in MouseEvent.prototype)) { Object.defineProperty(MouseEvent.prototype, "toElement", { get: function (): EventTarget | null { diff --git a/packages/vscode/src/fill/paths.ts b/packages/vscode/src/fill/paths.ts index a3f838f7..1a4a3f4c 100644 --- a/packages/vscode/src/fill/paths.ts +++ b/packages/vscode/src/fill/paths.ts @@ -1,4 +1,4 @@ -const paths = { +export const paths = { appData: "/tmp", defaultUserData: "/tmp", }; diff --git a/packages/vscode/src/protocol.ts b/packages/vscode/src/protocol.ts deleted file mode 100644 index cec762ac..00000000 --- a/packages/vscode/src/protocol.ts +++ /dev/null @@ -1,129 +0,0 @@ -import { ChildProcess } from "child_process"; -import { EventEmitter } from "events"; -import { Protocol as VSProtocol } from "vs/base/parts/ipc/node/ipc.net"; -import { LogLevel } from "vs/platform/log/common/log"; - -export interface ISharedProcessInitData { - socketPath: string; - serviceUrl: string; - logsDir: string; - windowId: number; - logLevel: LogLevel; -} - -export interface IStdio { - onMessage: (cb: (data: string | Buffer) => void) => void; - sendMessage: (data: string | Buffer) => void; - onExit?: (cb: () => void) => void; -} - -/** - * An implementation of net.Socket that uses stdio streams. - */ -class Socket { - - private readonly emitter: EventEmitter; - - public constructor(private readonly stdio: IStdio, ignoreFirst: boolean = false) { - this.emitter = new EventEmitter(); - - let first = true; - stdio.onMessage((data) => { - if (ignoreFirst && first) { - first = false; - - return; - } - this.emitter.emit("data", Buffer.from(data.toString())); - }); - if (stdio.onExit) { - stdio.onExit(() => { - this.emitter.emit("close"); - }); - } - } - - public removeListener(event: string, listener: () => void): void { - this.emitter.removeListener(event, listener); - } - - public once(event: string, listener: () => void): void { - this.emitter.once(event, listener); - } - - public on(event: string, listener: () => void): void { - this.emitter.on(event, listener); - } - - public end(): void { - // TODO: figure it out - } - - public get destroyed(): boolean { - return false; - } - - public write(data: string | Buffer): void { - this.stdio.sendMessage(data); - } - -} - -/** - * A protocol around a process, stream, or worker. - */ -export class Protocol extends VSProtocol { - - public static fromProcess(childProcess: ChildProcess): Protocol { - return Protocol.fromStdio({ - onMessage: (cb): void => { - childProcess.stdout.on("data", (data: string | Buffer) => { - cb(data); - }); - }, - sendMessage: (data): void => { - childProcess.stdin.write(data); - }, - onExit: (cb): void => { - childProcess.on("exit", cb); - }, - }); - } - - public static fromStream( - inStream: { on: (event: "data", cb: (b: string | Buffer) => void) => void }, - outStream: { write: (b: string | Buffer) => void }, - ): Protocol { - return Protocol.fromStdio({ - onMessage: (cb): void => { - inStream.on("data", (data) => { - cb(data); - }); - }, - sendMessage: (data): void => { - outStream.write(data); - }, - }); - } - - public static fromWorker(worker: { - onmessage: (event: MessageEvent) => void; - postMessage: (data: string, origin?: string | string[]) => void; - }, ignoreFirst: boolean = false): Protocol { - return Protocol.fromStdio({ - onMessage: (cb): void => { - worker.onmessage = (event: MessageEvent): void => { - cb(event.data); - }; - }, - sendMessage: (data): void => { - worker.postMessage(data.toString()); - }, - }, ignoreFirst); - } - - public static fromStdio(stdio: IStdio, ignoreFirst?: boolean): Protocol { - return new Protocol(new Socket(stdio, ignoreFirst)); - } - -} diff --git a/packages/vscode/src/firefox.scss b/packages/vscode/src/vscode.scss similarity index 57% rename from packages/vscode/src/firefox.scss rename to packages/vscode/src/vscode.scss index 3bea5767..3a03ccb6 100644 --- a/packages/vscode/src/firefox.scss +++ b/packages/vscode/src/vscode.scss @@ -1,7 +1,14 @@ +// These use -webkit-margin-before/after which don't work. +.monaco-workbench > .part > .title > .title-label h2, +.monaco-panel-view .panel > .panel-header h3.title { + margin-top: 0; + margin-bottom: 0; +} + // Using @supports to keep the Firefox fixes completely separate from vscode's // CSS that is tailored for Chrome. @supports (-moz-appearance:none) { - /* Fix buttons getting cut off on notifications. */ + // Fix buttons getting cut off on notifications. .monaco-workbench .notifications-list-container .notification-list-item .notification-list-item-buttons-container .monaco-button.monaco-text-button { max-width: 100%; width: auto; diff --git a/packages/vscode/webpack.config.bootstrap.js b/packages/vscode/webpack.config.bootstrap.js index 731e758f..1c0ad9c9 100644 --- a/packages/vscode/webpack.config.bootstrap.js +++ b/packages/vscode/webpack.config.bootstrap.js @@ -44,37 +44,6 @@ module.exports = (env) => { }, ], }, - }, { - loader: "string-replace-loader", - test: /vs\/loader\.js/, - options: { - multiple: [ - { - search: "var recorder = moduleManager.getRecorder\\(\\);", - replace: ` -var recorder = moduleManager.getRecorder(); -const context = require.context("../", true, /.*/); -if (scriptSrc.indexOf("file:///") !== -1) { - const vsSrc = scriptSrc.split("file:///")[1].split(".js")[0]; - if (vsSrc && vsSrc.startsWith("vs/")) { - scriptSrc = \`node|./\${vsSrc}\`; - } -} -`, - flags: "g", - }, - { - search: "nodeRequire\\(", - replace: "require(", - flags: "g", - }, - { - search: "moduleExports_1 = require\\(", - replace: "moduleExports_1 = context(", - flags: "g", - }, - ], - }, }, { test: /\.wasm$/, type: "javascript/auto", @@ -120,4 +89,4 @@ if (scriptSrc.indexOf("file:///") !== -1) { }), ], }); -}; \ No newline at end of file +}; diff --git a/packages/web/webpack.common.config.js b/packages/web/webpack.common.config.js index aae2f4b4..3c5b1c37 100644 --- a/packages/web/webpack.common.config.js +++ b/packages/web/webpack.common.config.js @@ -38,6 +38,11 @@ module.exports = merge({ }, }], }, + node: { + module: "empty", + crypto: "empty", + tls: "empty", + }, resolve: { alias: { "gc-signals": path.join(fills, "empty.ts"), @@ -51,6 +56,10 @@ module.exports = merge({ "vscode-sqlite3": path.join(fills, "empty.ts"), "tls": path.join(fills, "empty.ts"), "native-is-elevated": path.join(fills, "empty.ts"), + "native-watchdog": path.join(fills, "empty.ts"), + "dns": path.join(fills, "empty.ts"), + "console": path.join(fills, "empty.ts"), + "readline": path.join(fills, "empty.ts"), "crypto": "crypto-browserify", "http": "http-browserify", diff --git a/scripts/vscode.patch b/scripts/vscode.patch index 6f179c40..55801b30 100644 --- a/scripts/vscode.patch +++ b/scripts/vscode.patch @@ -1,3 +1,99 @@ +diff --git a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +index 457818a975..ad45ffe58a 100644 +--- a/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts ++++ b/src/vs/code/electron-browser/sharedProcess/sharedProcessMain.ts +@@ -194,3 +194,5 @@ async function handshake(configuration: ISharedProcessConfiguration): Promise { + mark('willInitGlobalStorage'); + ++ // TODO: shouldn't reject ++ return Promise.reject(new Error("nope")); ++ + return this.globalStorage.init().then(() => { + mark('didInitGlobalStorage'); + }, error => { +@@ -605,4 +608,4 @@ export class DelegatingStorageService extends Disposable implements IStorageServ + private convertScope(scope: StorageScope): StorageLegacyScope { + return scope === StorageScope.GLOBAL ? StorageLegacyScope.GLOBAL : StorageLegacyScope.WORKSPACE; + } +-} +\ No newline at end of file ++} +diff --git a/src/vs/workbench/electron-browser/main.ts b/src/vs/workbench/electron-browser/main.ts +index a43d63aa51..4c6df2fcd9 100644 +--- a/src/vs/workbench/electron-browser/main.ts ++++ b/src/vs/workbench/electron-browser/main.ts +@@ -147,13 +147,14 @@ function openWorkbench(configuration: IWindowConfiguration): Promise { + shell.open(); + + // Inform user about loading issues from the loader +- (self).require.config({ +- onError: err => { +- if (err.errorCode === 'load') { +- shell.onUnexpectedError(new Error(nls.localize('loaderErrorNative', "Failed to load a required file. Please restart the application to try again. Details: {0}", JSON.stringify(err)))); +- } +- } +- }); ++ // TODO: how to make this work ++ // (self).require.config({ ++ // onError: err => { ++ // if (err.errorCode === 'load') { ++ // shell.onUnexpectedError(new Error(nls.localize('loaderErrorNative', "Failed to load a required file. Please restart the application to try again. Details: {0}", JSON.stringify(err)))); ++ // } ++ // } ++ // }); + }); + }); + }); diff --git a/src/vs/workbench/parts/welcome/walkThrough/node/walkThroughContentProvider.ts b/src/vs/workbench/parts/welcome/walkThrough/node/walkThroughContentProvider.ts index 7b4e8721ac..8f26dc2f28 100644 --- a/src/vs/workbench/parts/welcome/walkThrough/node/walkThroughContentProvider.ts