diff --git a/packages/ide/src/fill/dialog.ts b/packages/ide/src/fill/dialog.ts index 2ea6c48a9..4c2900d4f 100644 --- a/packages/ide/src/fill/dialog.ts +++ b/packages/ide/src/fill/dialog.ts @@ -88,7 +88,8 @@ export class Dialog { if (this.options.buttons && this.options.buttons.length > 0) { this.buttons = this.options.buttons.map((buttonText, buttonIndex) => { const button = document.createElement("button"); - button.innerText = buttonText; + // TODO: support mnemonics. + button.innerText = buttonText.replace("_", ""); button.addEventListener("click", () => { this.actionEmitter.emit({ buttonIndex, diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts index 239b1d0a3..9ecb11c46 100644 --- a/packages/logger/src/logger.ts +++ b/packages/logger/src/logger.ts @@ -42,7 +42,12 @@ export class Time { } -export type FieldArray = Array>; // tslint:disable-line no-any +// tslint:disable-next-line no-any +export type FieldArray = Array>; + +// Functions can be used to remove the need to perform operations when the +// logging level won't output the result anyway. +export type LogCallback = () => [string, ...FieldArray]; /** * Creates a time field @@ -127,6 +132,7 @@ export abstract class Formatter { public abstract push(arg: string, color?: string, weight?: string): void; public abstract push(arg: any): void; // tslint:disable-line no-any + // tslint:disable-next-line no-any public abstract fields(fields: Array>): void; /** @@ -184,7 +190,9 @@ export class BrowserFormatter extends Formatter { this.args.push(arg); } + // tslint:disable-next-line no-any public fields(fields: Array>): void { + // tslint:disable-next-line no-console console.groupCollapsed(...this.flush()); fields.forEach((field) => { this.push(field.identifier, "#3794ff", "bold"); @@ -193,8 +201,10 @@ export class BrowserFormatter extends Formatter { } this.push(": "); this.push(field.value); + // tslint:disable-next-line no-console console.log(...this.flush()); }); + // tslint:disable-next-line no-console console.groupEnd(); } @@ -229,8 +239,10 @@ export class ServerFormatter extends Formatter { this.args.push(arg); } + // tslint:disable-next-line no-any public fields(fields: Array>): void { - const obj = {} as any; + // tslint:disable-next-line no-any + const obj: { [key: string]: any} = {}; this.format += "\u001B[38;2;140;140;140m"; fields.forEach((field) => { obj[field.identifier] = field.value; @@ -284,57 +296,61 @@ export class Logger { /** * Outputs information. */ - public info(msg: string, ...fields: FieldArray): void { - if (this.level <= Level.Info) { - this.handle({ - type: "info", - message: msg, - fields, - tagColor: "#008FBF", - }); - } + public info(fn: LogCallback): void; + public info(message: string, ...fields: FieldArray): void; + public info(message: LogCallback | string, ...fields: FieldArray): void { + this.handle({ + type: "info", + message, + fields, + tagColor: "#008FBF", + level: Level.Info, + }); } /** * Outputs a warning. */ - public warn(msg: string, ...fields: FieldArray): void { - if (this.level <= Level.Warn) { - this.handle({ - type: "warn", - message: msg, - fields, - tagColor: "#FF9D00", - }); - } + public warn(fn: LogCallback): void; + public warn(message: string, ...fields: FieldArray): void; + public warn(message: LogCallback | string, ...fields: FieldArray): void { + this.handle({ + type: "warn", + message, + fields, + tagColor: "#FF9D00", + level: Level.Warn, + }); } /** * Outputs a debug message. */ - public debug(msg: string, ...fields: FieldArray): void { - if (this.level <= Level.Debug) { - this.handle({ - type: "debug", - message: msg, - fields, - tagColor: "#84009E", - }); - } + public debug(fn: LogCallback): void; + public debug(message: string, ...fields: FieldArray): void; + public debug(message: LogCallback | string, ...fields: FieldArray): void { + this.handle({ + type: "debug", + message, + fields, + tagColor: "#84009E", + level: Level.Debug, + }); } /** * Outputs an error. */ - public error(msg: string, ...fields: FieldArray): void { - if (this.level <= Level.Error) { - this.handle({ - type: "error", - message: msg, - fields, - tagColor: "#B00000", - }); - } + public error(fn: LogCallback): void; + public error(message: string, ...fields: FieldArray): void; + public error(message: LogCallback | string, ...fields: FieldArray): void { + this.handle({ + type: "error", + message, + fields, + tagColor: "#B00000", + level: Level.Error, + }); } /** @@ -355,15 +371,22 @@ export class Logger { */ private handle(options: { type: "info" | "warn" | "debug" | "error"; - message: string; + message: string | LogCallback; fields?: FieldArray; + level: Level; tagColor: string; }): void { - if (this.muted) { + if (this.level > options.level || this.muted) { return; } - const passedFields = options.fields || []; + let passedFields = options.fields || []; + if (typeof options.message === "function") { + const values = options.message(); + options.message = values.shift() as string; + passedFields = values as FieldArray; + } + const fields = this.defaultFields ? passedFields.concat(this.defaultFields) : passedFields; diff --git a/packages/protocol/src/browser/client.ts b/packages/protocol/src/browser/client.ts index 7089c82fe..9eb9090e7 100644 --- a/packages/protocol/src/browser/client.ts +++ b/packages/protocol/src/browser/client.ts @@ -1,13 +1,16 @@ import { ReadWriteConnection, InitData, OperatingSystem, ISharedProcessData } from "../common/connection"; -import { NewEvalMessage, ServerMessage, EvalDoneMessage, EvalFailedMessage, TypedValue, ClientMessage, NewSessionMessage, TTYDimensions, SessionOutputMessage, CloseSessionInputMessage, WorkingInitMessage, NewConnectionMessage, NewServerMessage } from "../proto"; +import { NewEvalMessage, ServerMessage, EvalDoneMessage, EvalFailedMessage, TypedValue, ClientMessage, NewSessionMessage, TTYDimensions, SessionOutputMessage, CloseSessionInputMessage, WorkingInitMessage } from "../proto"; import { Emitter, Event } from "@coder/events"; import { logger, field } from "@coder/logger"; -import { ChildProcess, SpawnOptions, ServerProcess, ServerSocket, Socket, ServerListener, Server } from "./command"; +import { ChildProcess, SpawnOptions, ForkOptions, ServerProcess, ServerSocket, Socket, ServerListener, Server } from "./command"; /** * Client accepts an arbitrary connection intended to communicate with the Server. */ export class Client { + + public Socket: typeof ServerSocket; + private evalId: number = 0; private evalDoneEmitter: Emitter = new Emitter(); private evalFailedEmitter: Emitter = new Emitter(); @@ -41,6 +44,15 @@ export class Client { } }); + const that = this; + this.Socket = class extends ServerSocket { + + public constructor() { + super(that.connection, that.connectionId++, that.registerConnection); + } + + }; + this.initDataPromise = new Promise((resolve): void => { this.initDataEmitter.event(resolve); }); @@ -77,7 +89,7 @@ export class Client { const newEval = new NewEvalMessage(); const id = this.evalId++; newEval.setId(id); - newEval.setArgsList([a1, a2, a3, a4, a5, a6].filter(a => a).map(a => JSON.stringify(a))); + newEval.setArgsList([a1, a2, a3, a4, a5, a6].filter(a => typeof a !== "undefined").map(a => JSON.stringify(a))); newEval.setFunction(func.toString()); const clientMsg = new ClientMessage(); @@ -158,7 +170,7 @@ export class Client { * @param args Args to add for the module * @param options Options to execute */ - public fork(modulePath: string, args: string[] = [], options?: SpawnOptions): ChildProcess { + public fork(modulePath: string, args: string[] = [], options?: ForkOptions): ChildProcess { return this.doSpawn(modulePath, args, options, true); } @@ -167,27 +179,17 @@ export class Client { * Forks a module from bootstrap-fork * @param modulePath Path of the module */ - public bootstrapFork(modulePath: string): ChildProcess { - return this.doSpawn(modulePath, [], undefined, true, true); + public bootstrapFork(modulePath: string, args: string[] = [], options?: ForkOptions): ChildProcess { + return this.doSpawn(modulePath, args, options, true, true); } - public createConnection(path: string, callback?: () => void): Socket; - public createConnection(port: number, callback?: () => void): Socket; - public createConnection(target: string | number, callback?: () => void): Socket { + public createConnection(path: string, callback?: Function): Socket; + public createConnection(port: number, callback?: Function): Socket; + public createConnection(target: string | number, callback?: Function): Socket; + public createConnection(target: string | number, callback?: Function): Socket { const id = this.connectionId++; - const newCon = new NewConnectionMessage(); - newCon.setId(id); - if (typeof target === "string") { - newCon.setPath(target); - } else { - newCon.setPort(target); - } - const clientMsg = new ClientMessage(); - clientMsg.setNewConnection(newCon); - this.connection.send(clientMsg.serializeBinary()); - - const socket = new ServerSocket(this.connection, id, callback); - this.connections.set(id, socket); + const socket = new ServerSocket(this.connection, id, this.registerConnection); + socket.connect(target, callback); return socket; } @@ -214,7 +216,9 @@ export class Client { } if (options.env) { Object.keys(options.env).forEach((envKey) => { - newSess.getEnvMap().set(envKey, options.env![envKey]); + if (options.env![envKey]) { + newSess.getEnvMap().set(envKey, options.env![envKey].toString()); + } }); } if (options.tty) { @@ -356,9 +360,9 @@ export class Client { return; } const conId = message.getServerConnectionEstablished()!.getConnectionId(); - const serverSocket = new ServerSocket(this.connection, conId); + const serverSocket = new ServerSocket(this.connection, conId, this.registerConnection); + this.registerConnection(conId, serverSocket); serverSocket.emit("connect"); - this.connections.set(conId, serverSocket); s.emit("connection", serverSocket); } else if (message.getServerFailure()) { const s = this.servers.get(message.getServerFailure()!.getId()); @@ -376,4 +380,12 @@ export class Client { this.servers.delete(message.getServerClose()!.getId()); } } + + private registerConnection = (id: number, socket: ServerSocket): void => { + if (this.connections.has(id)) { + throw new Error(`${id} is already registered`); + } + this.connections.set(id, socket); + } + } diff --git a/packages/protocol/src/browser/command.ts b/packages/protocol/src/browser/command.ts index e1e94fbcd..75590b734 100644 --- a/packages/protocol/src/browser/command.ts +++ b/packages/protocol/src/browser/command.ts @@ -1,7 +1,7 @@ import * as events from "events"; import * as stream from "stream"; import { ReadWriteConnection } from "../common/connection"; -import { ShutdownSessionMessage, ClientMessage, WriteToSessionMessage, ResizeSessionTTYMessage, TTYDimensions as ProtoTTYDimensions, ConnectionOutputMessage, ConnectionCloseMessage, ServerCloseMessage, NewServerMessage } from "../proto"; +import { NewConnectionMessage, ShutdownSessionMessage, ClientMessage, WriteToSessionMessage, ResizeSessionTTYMessage, TTYDimensions as ProtoTTYDimensions, ConnectionOutputMessage, ConnectionCloseMessage, ServerCloseMessage, NewServerMessage } from "../proto"; export interface TTYDimensions { readonly columns: number; @@ -10,10 +10,15 @@ export interface TTYDimensions { export interface SpawnOptions { cwd?: string; - env?: { readonly [key: string]: string }; + env?: { [key: string]: string }; tty?: TTYDimensions; } +export interface ForkOptions { + cwd?: string; + env?: { [key: string]: string }; +} + export interface ChildProcess { readonly stdin: stream.Writable; readonly stdout: stream.Readable; @@ -119,6 +124,9 @@ export interface Socket { write(buffer: Buffer): void; end(): void; + connect(path: string, callback?: () => void): void; + connect(port: number, callback?: () => void): void; + addListener(event: "data", listener: (data: Buffer) => void): this; addListener(event: "close", listener: (hasError: boolean) => void): this; addListener(event: "connect", listener: () => void): this; @@ -151,21 +159,37 @@ export class ServerSocket extends events.EventEmitter implements Socket { public readable: boolean = true; private _destroyed: boolean = false; - private _connecting: boolean = true; + private _connecting: boolean = false; public constructor( private readonly connection: ReadWriteConnection, private readonly id: number, - connectCallback?: () => void, + private readonly beforeConnect: (id: number, socket: ServerSocket) => void, ) { super(); + } - if (connectCallback) { - this.once("connect", () => { - this._connecting = false; - connectCallback(); - }); + public connect(target: string | number, callback?: Function): void { + this._connecting = true; + this.beforeConnect(this.id, this); + + this.once("connect", () => { + this._connecting = false; + if (callback) { + callback(); + } + }); + + const newCon = new NewConnectionMessage(); + newCon.setId(this.id); + if (typeof target === "string") { + newCon.setPath(target); + } else { + newCon.setPort(target); } + const clientMsg = new ClientMessage(); + clientMsg.setNewConnection(newCon); + this.connection.send(clientMsg.serializeBinary()); } public get destroyed(): boolean { @@ -236,6 +260,7 @@ export class ServerSocket extends events.EventEmitter implements Socket { public setDefaultEncoding(encoding: string): this { throw new Error("Method not implemented."); } + } export interface Server { @@ -266,6 +291,7 @@ export interface Server { } export class ServerListener extends events.EventEmitter implements Server { + private _listening: boolean = false; public constructor( @@ -309,11 +335,12 @@ export class ServerListener extends events.EventEmitter implements Server { const clientMsg = new ClientMessage(); clientMsg.setServerClose(closeMsg); this.connection.send(clientMsg.serializeBinary()); - + if (callback) { callback(); } return this; } -} \ No newline at end of file + +} diff --git a/packages/protocol/src/browser/modules/child_process.ts b/packages/protocol/src/browser/modules/child_process.ts index 1bfdb98f5..b64688afb 100644 --- a/packages/protocol/src/browser/modules/child_process.ts +++ b/packages/protocol/src/browser/modules/child_process.ts @@ -40,19 +40,38 @@ export class CP { ); }); - // @ts-ignore + // @ts-ignore TODO: not fully implemented return childProcess; } - public fork = (modulePath: string, args?: ReadonlyArray | cp.ForkOptions, options?: cp.ForkOptions): cp.ChildProcess => { - //@ts-ignore - return this.client.bootstrapFork(options && options.env && options.env.AMD_ENTRYPOINT || modulePath); + public fork = (modulePath: string, args?: string[] | cp.ForkOptions, options?: cp.ForkOptions): cp.ChildProcess => { + if (options && options.env && options.env.AMD_ENTRYPOINT) { + // @ts-ignore TODO: not fully implemented + return this.client.bootstrapFork( + options.env.AMD_ENTRYPOINT, + Array.isArray(args) ? args : [], + // @ts-ignore TODO: env is a different type + Array.isArray(args) || !args ? options : args, + ); + } + + // @ts-ignore TODO: not fully implemented + return this.client.fork( + modulePath, + Array.isArray(args) ? args : [], + // @ts-ignore TODO: env is a different type + Array.isArray(args) || !args ? options : args, + ); } - public spawn = (command: string, args?: ReadonlyArray | cp.SpawnOptions, options?: cp.SpawnOptions): cp.ChildProcess => { - // TODO: fix this ignore. Should check for args or options here - //@ts-ignore - return this.client.spawn(command, args, options); + public spawn = (command: string, args?: string[] | cp.SpawnOptions, options?: cp.SpawnOptions): cp.ChildProcess => { + // @ts-ignore TODO: not fully implemented + return this.client.spawn( + command, + Array.isArray(args) ? args : [], + // @ts-ignore TODO: env is a different type + Array.isArray(args) || !args ? options : args, + ); } } diff --git a/packages/protocol/src/browser/modules/fs.ts b/packages/protocol/src/browser/modules/fs.ts index de71192c0..12e8d54f5 100644 --- a/packages/protocol/src/browser/modules/fs.ts +++ b/packages/protocol/src/browser/modules/fs.ts @@ -358,9 +358,9 @@ export class FS { return util.promisify(fs.read)(fd, buffer, 0, length, position).then((resp) => { return { bytesRead: resp.bytesRead, - content: buffer.toString("utf8"), + content: (resp.bytesRead < buffer.length ? buffer.slice(0, resp.bytesRead) : buffer).toString("utf8"), }; - }): + }); }, fd, length, position).then((resp) => { const newBuf = Buffer.from(resp.content, "utf8"); buffer.set(newBuf, offset); diff --git a/packages/protocol/src/browser/modules/net.ts b/packages/protocol/src/browser/modules/net.ts index 2834a0329..0402573fe 100644 --- a/packages/protocol/src/browser/modules/net.ts +++ b/packages/protocol/src/browser/modules/net.ts @@ -13,7 +13,8 @@ export class Net implements NodeNet { ) {} public get Socket(): typeof net.Socket { - throw new Error("not implemented"); + // @ts-ignore + return this.client.Socket; } public get Server(): typeof net.Server { @@ -24,10 +25,12 @@ 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; + public createConnection(target: string | number | net.NetConnectOpts, host?: string | Function, callback?: Function): net.Socket { + if (typeof target === "object") { + throw new Error("not implemented"); + } + + return this.client.createConnection(target, typeof host === "function" ? host : callback) as net.Socket; } public isIP(_input: string): number { diff --git a/packages/protocol/src/node/command.ts b/packages/protocol/src/node/command.ts index 6945dabe1..4245c81c1 100644 --- a/packages/protocol/src/node/command.ts +++ b/packages/protocol/src/node/command.ts @@ -3,6 +3,7 @@ import * as net from "net"; import * as nodePty from "node-pty"; import * as stream from "stream"; import { TextEncoder } from "text-encoding"; +import { Logger, logger, field } from "@coder/logger"; import { NewSessionMessage, ServerMessage, SessionDoneMessage, SessionOutputMessage, IdentifySessionMessage, NewConnectionMessage, ConnectionEstablishedMessage, NewConnectionFailureMessage, ConnectionCloseMessage, ConnectionOutputMessage, NewServerMessage, ServerEstablishedMessage, NewServerFailureMessage, ServerCloseMessage, ServerConnectionEstablishedMessage } from "../proto"; import { SendableConnection } from "../common/connection"; import { ServerOptions } from "./server"; @@ -25,10 +26,18 @@ export interface Process { } export const handleNewSession = (connection: SendableConnection, newSession: NewSessionMessage, serverOptions: ServerOptions | undefined, onExit: () => void): Process => { + const childLogger = getChildLogger(newSession.getCommand()); + childLogger.debug(() => [ + newSession.getIsFork() ? "Forking" : "Spawning", + field("command", newSession.getCommand()), + field("args", newSession.getArgsList()), + field("env", newSession.getEnvMap().toObject()), + ]); + let process: Process; - const env = {} as any; - newSession.getEnvMap().forEach((value: any, key: any) => { + const env: { [key: string]: string } = {}; + newSession.getEnvMap().forEach((value, key) => { env[key] = value; }); if (newSession.getTtyDimensions()) { @@ -64,14 +73,29 @@ export const handleNewSession = (connection: SendableConnection, newSession: New stderr: proc.stderr, stdout: proc.stdout, stdio: proc.stdio, - on: (...args: any[]) => (proc.on)(...args), - write: (d) => proc.stdin.write(d), - kill: (s) => proc.kill(s || "SIGTERM"), + on: (...args: any[]): void => ((proc as any).on)(...args), // tslint:disable-line no-any + write: (d): boolean => proc.stdin.write(d), + kill: (s): void => proc.kill(s || "SIGTERM"), pid: proc.pid, }; } const sendOutput = (_source: SessionOutputMessage.Source, msg: string | Uint8Array): void => { + childLogger.debug(() => { + + let data = msg.toString(); + if (_source === SessionOutputMessage.Source.IPC) { + data = Buffer.from(msg.toString(), "base64").toString(); + } + + return [ + _source === SessionOutputMessage.Source.STDOUT + ? "stdout" + : (_source === SessionOutputMessage.Source.STDERR ? "stderr" : "ipc"), + field("id", newSession.getId()), + field("data", data), + ]; + }); const serverMsg = new ServerMessage(); const d = new SessionOutputMessage(); d.setId(newSession.getId()); @@ -110,6 +134,7 @@ export const handleNewSession = (connection: SendableConnection, newSession: New connection.send(sm.serializeBinary()); process.on("exit", (code) => { + childLogger.debug("Exited", field("id", newSession.getId())); const serverMsg = new ServerMessage(); const exit = new SessionDoneMessage(); exit.setId(newSession.getId()); @@ -124,10 +149,14 @@ export const handleNewSession = (connection: SendableConnection, newSession: New }; export const handleNewConnection = (connection: SendableConnection, newConnection: NewConnectionMessage, onExit: () => void): net.Socket => { + const target = newConnection.getPath() || `${newConnection.getPort()}`; + const childLogger = getChildLogger(target, ">"); + const id = newConnection.getId(); let socket: net.Socket; let didConnect = false; - const connectCallback = () => { + const connectCallback = (): void => { + childLogger.debug("Connected", field("id", newConnection.getId()), field("target", target)); didConnect = true; const estab = new ConnectionEstablishedMessage(); estab.setId(id); @@ -145,6 +174,7 @@ export const handleNewConnection = (connection: SendableConnection, newConnectio } socket.addListener("error", (err) => { + childLogger.debug("Error", field("id", newConnection.getId()), field("error", err)); if (!didConnect) { const errMsg = new NewConnectionFailureMessage(); errMsg.setId(id); @@ -158,6 +188,7 @@ export const handleNewConnection = (connection: SendableConnection, newConnectio }); socket.addListener("close", () => { + childLogger.debug("Closed", field("id", newConnection.getId())); if (didConnect) { const closed = new ConnectionCloseMessage(); closed.setId(id); @@ -170,6 +201,11 @@ export const handleNewConnection = (connection: SendableConnection, newConnectio }); socket.addListener("data", (data) => { + childLogger.debug(() => [ + "ipc", + field("id", newConnection.getId()), + field("data", data), + ]); const dataMsg = new ConnectionOutputMessage(); dataMsg.setId(id); dataMsg.setData(data); @@ -181,11 +217,15 @@ export const handleNewConnection = (connection: SendableConnection, newConnectio return socket; }; -export const handleNewServer = (connection: SendableConnection, newServer: NewServerMessage, addSocket: (socket: net.Socket) => number, onExit: () => void): net.Server => { +export const handleNewServer = (connection: SendableConnection, newServer: NewServerMessage, addSocket: (socket: net.Socket) => number, onExit: () => void, onSocketExit: (id: number) => void): net.Server => { + const target = newServer.getPath() || `${newServer.getPort()}`; + const childLogger = getChildLogger(target, "|"); + const s = net.createServer(); try { s.listen(newServer.getPath() ? newServer.getPath() : newServer.getPort(), () => { + childLogger.debug("Listening", field("id", newServer.getId()), field("target", target)); const se = new ServerEstablishedMessage(); se.setId(newServer.getId()); const sm = new ServerMessage(); @@ -193,6 +233,7 @@ export const handleNewServer = (connection: SendableConnection, newServer: NewSe connection.send(sm.serializeBinary()); }); } catch (ex) { + childLogger.debug("Failed to listen", field("id", newServer.getId()), field("target", target)); const sf = new NewServerFailureMessage(); sf.setId(newServer.getId()); const sm = new ServerMessage(); @@ -203,6 +244,7 @@ export const handleNewServer = (connection: SendableConnection, newServer: NewSe } s.on("close", () => { + childLogger.debug("Stopped listening", field("id", newServer.getId()), field("target", target)); const sc = new ServerCloseMessage(); sc.setId(newServer.getId()); const sm = new ServerMessage(); @@ -214,6 +256,7 @@ export const handleNewServer = (connection: SendableConnection, newServer: NewSe s.on("connection", (socket) => { const socketId = addSocket(socket); + childLogger.debug("Got connection", field("id", newServer.getId()), field("socketId", socketId)); const sock = new ServerConnectionEstablishedMessage(); sock.setServerId(newServer.getId()); @@ -221,7 +264,54 @@ export const handleNewServer = (connection: SendableConnection, newServer: NewSe const sm = new ServerMessage(); sm.setServerConnectionEstablished(sock); connection.send(sm.serializeBinary()); + + socket.addListener("data", (data) => { + childLogger.debug(() => [ + "ipc", + field("id", newServer.getId()), + field("socketId", socketId), + field("data", data), + ]); + const dataMsg = new ConnectionOutputMessage(); + dataMsg.setId(socketId); + dataMsg.setData(data); + const servMsg = new ServerMessage(); + servMsg.setConnectionOutput(dataMsg); + connection.send(servMsg.serializeBinary()); + }); + + socket.on("error", (error) => { + childLogger.debug("Error", field("id", newServer.getId()), field("socketId", socketId), field("error", error)); + onSocketExit(socketId); + }); + + socket.on("close", () => { + childLogger.debug("Closed", field("id", newServer.getId()), field("socketId", socketId)); + onSocketExit(socketId); + }); }); return s; }; + +const getChildLogger = (command: string, prefix: string = ""): Logger => { + // TODO: Temporary, for debugging. Should probably ask for a name? + let name: string; + if (command.includes("vscode-ipc") || command.includes("extensionHost")) { + name = "exthost"; + } else if (command.includes("vscode-online")) { + name = "shared"; + } else { + const basename = command.split("/").pop()!; + let i = 0; + for (; i < basename.length; i++) { + const character = basename.charAt(i); + if (isNaN(+character) && character === character.toUpperCase()) { + break; + } + } + name = basename.substring(0, i); + } + + return logger.named(prefix + name); +}; diff --git a/packages/protocol/src/node/server.ts b/packages/protocol/src/node/server.ts index 04707ce7f..4dc53eee1 100644 --- a/packages/protocol/src/node/server.ts +++ b/packages/protocol/src/node/server.ts @@ -4,7 +4,7 @@ import * as path from "path"; import { mkdir, WriteStream } from "fs"; import { promisify } from "util"; import { TextDecoder } from "text-encoding"; -import { Logger, logger, field } from "@coder/logger"; +import { logger, field } from "@coder/logger"; import { ClientMessage, WorkingInitMessage, ServerMessage, NewSessionMessage, WriteToSessionMessage } from "../proto"; import { evaluate } from "./evaluate"; import { ReadWriteConnection } from "../common/connection"; @@ -93,42 +93,50 @@ export class Server { private handleMessage(message: ClientMessage): void { if (message.hasNewEval()) { - evaluate(this.connection, message.getNewEval()!); + const evalMessage = message.getNewEval()!; + logger.debug("EvalMessage", field("id", evalMessage.getId())); + evaluate(this.connection, evalMessage); } else if (message.hasNewSession()) { const sessionMessage = message.getNewSession()!; - const childLogger = this.getChildLogger(sessionMessage.getCommand()); - childLogger.debug(sessionMessage.getIsFork() ? "Forking" : "Spawning", field("args", sessionMessage.getArgsList())); + logger.debug("NewSession", field("id", sessionMessage.getId())); const session = handleNewSession(this.connection, sessionMessage, this.options, () => { - childLogger.debug("Exited"); this.sessions.delete(sessionMessage.getId()); }); - this.sessions.set(message.getNewSession()!.getId(), session); + this.sessions.set(sessionMessage.getId(), session); } else if (message.hasCloseSessionInput()) { - const s = this.getSession(message.getCloseSessionInput()!.getId()); + const closeSessionMessage = message.getCloseSessionInput()!; + logger.debug("CloseSessionInput", field("id", closeSessionMessage.getId())); + const s = this.getSession(closeSessionMessage.getId()); if (!s || !s.stdin) { return; } s.stdin.end(); } else if (message.hasResizeSessionTty()) { - const s = this.getSession(message.getResizeSessionTty()!.getId()); + const resizeSessionTtyMessage = message.getResizeSessionTty()!; + logger.debug("ResizeSessionTty", field("id", resizeSessionTtyMessage.getId())); + const s = this.getSession(resizeSessionTtyMessage.getId()); if (!s || !s.resize) { return; } - const tty = message.getResizeSessionTty()!.getTtyDimensions()!; + const tty = resizeSessionTtyMessage.getTtyDimensions()!; s.resize(tty.getWidth(), tty.getHeight()); } else if (message.hasShutdownSession()) { - const s = this.getSession(message.getShutdownSession()!.getId()); + const shutdownSessionMessage = message.getShutdownSession()!; + logger.debug("ShutdownSession", field("id", shutdownSessionMessage.getId())); + const s = this.getSession(shutdownSessionMessage.getId()); if (!s) { return; } - s.kill(message.getShutdownSession()!.getSignal()); + s.kill(shutdownSessionMessage.getSignal()); } else if (message.hasWriteToSession()) { - const s = this.getSession(message.getWriteToSession()!.getId()); + const writeToSessionMessage = message.getWriteToSession()!; + logger.debug("WriteToSession", field("id", writeToSessionMessage.getId())); + const s = this.getSession(writeToSessionMessage.getId()); if (!s) { return; } - const data = new TextDecoder().decode(message.getWriteToSession()!.getData_asU8()); - const source = message.getWriteToSession()!.getSource(); + const data = new TextDecoder().decode(writeToSessionMessage.getData_asU8()); + const source = writeToSessionMessage.getSource(); if (source === WriteToSessionMessage.Source.IPC) { if (!s.stdio || !s.stdio[3]) { throw new Error("Cannot send message via IPC to process without IPC"); @@ -139,48 +147,57 @@ export class Server { } } else if (message.hasNewConnection()) { const connectionMessage = message.getNewConnection()!; - const name = connectionMessage.getPath() || `${connectionMessage.getPort()}`; - const childLogger = this.getChildLogger(name, ">"); - childLogger.debug("Connecting", field("path", connectionMessage.getPath()), field("port", connectionMessage.getPort())); + logger.debug("NewConnection", field("id", connectionMessage.getId())); + if (this.connections.has(connectionMessage.getId())) { + throw new Error(`connect EISCONN ${connectionMessage.getPath() || connectionMessage.getPort()}`); + } const socket = handleNewConnection(this.connection, connectionMessage, () => { - childLogger.debug("Disconnected"); this.connections.delete(connectionMessage.getId()); }); this.connections.set(connectionMessage.getId(), socket); } else if (message.hasConnectionOutput()) { - const c = this.getConnection(message.getConnectionOutput()!.getId()); + const connectionOutputMessage = message.getConnectionOutput()!; + logger.debug("ConnectionOuput", field("id", connectionOutputMessage.getId())); + const c = this.getConnection(connectionOutputMessage.getId()); if (!c) { return; } - c.write(Buffer.from(message.getConnectionOutput()!.getData_asU8())); + c.write(Buffer.from(connectionOutputMessage.getData_asU8())); } else if (message.hasConnectionClose()) { - const c = this.getConnection(message.getConnectionClose()!.getId()); + const connectionCloseMessage = message.getConnectionClose()!; + logger.debug("ConnectionClose", field("id", connectionCloseMessage.getId())); + const c = this.getConnection(connectionCloseMessage.getId()); if (!c) { return; } c.end(); } else if (message.hasNewServer()) { const serverMessage = message.getNewServer()!; - const name = serverMessage.getPath() || `${serverMessage.getPort()}`; - const childLogger = this.getChildLogger(name); - childLogger.debug("Listening", field("path", serverMessage.getPath()), field("port", serverMessage.getPort())); + logger.debug("NewServer", field("id", serverMessage.getId())); + if (this.servers.has(serverMessage.getId())) { + throw new Error("multiple listeners not supported"); + } const s = handleNewServer(this.connection, serverMessage, (socket) => { const id = this.connectionId--; this.connections.set(id, socket); - childLogger.debug("Got connection", field("id", id)); return id; }, () => { - childLogger.debug("Stopped"); this.connections.delete(serverMessage.getId()); + }, (id) => { + this.connections.delete(id); }); this.servers.set(serverMessage.getId(), s); } else if (message.hasServerClose()) { - const s = this.getServer(message.getServerClose()!.getId()); + const serverCloseMessage = message.getServerClose()!; + logger.debug("ServerClose", field("id", serverCloseMessage.getId())); + const s = this.getServer(serverCloseMessage.getId()); if (!s) { return; } s.close(); + } else { + logger.debug("Received unknown message type"); } } @@ -196,26 +213,4 @@ export class Server { return this.sessions.get(id); } - private getChildLogger(command: string, prefix: string = ""): Logger { - // TODO: Temporary, for debugging. Should probably ask for a name? - let name: string; - if (command.includes("vscode-ipc")) { - name = "exthost"; - } else if (command.includes("vscode-online")) { - name = "shared"; - } else { - const basename = command.split("/").pop()!; - let i = 0; - for (; i < basename.length; i++) { - const character = basename.charAt(i); - if (character === character.toUpperCase()) { - break; - } - } - name = basename.substring(0, i); - } - - return logger.named(prefix + name); - } - } diff --git a/packages/protocol/test/command.test.ts b/packages/protocol/test/command.test.ts index ba7a01909..05361b044 100644 --- a/packages/protocol/test/command.test.ts +++ b/packages/protocol/test/command.test.ts @@ -4,15 +4,16 @@ import * as os from "os"; import * as path from "path"; import { TextEncoder, TextDecoder } from "text-encoding"; import { createClient } from "./helpers"; +import { Net } from "../src/browser/modules/net"; -(global).TextDecoder = TextDecoder; -(global).TextEncoder = TextEncoder; +(global as any).TextDecoder = TextDecoder; // tslint:disable-line no-any +(global as any).TextEncoder = TextEncoder; // tslint:disable-line no-any describe("spawn", () => { const client = createClient({ dataDirectory: "", workingDirectory: "", - forkProvider: (msg) => { + forkProvider: (msg): cp.ChildProcess => { return cp.spawn(msg.getCommand(), msg.getArgsList(), { stdio: [null, null, null, "pipe"], }); @@ -24,7 +25,7 @@ describe("spawn", () => { proc.stdout.on("data", (data) => { expect(data).toEqual("test\n"); }); - proc.on("exit", (code) => { + proc.on("exit", (): void => { done(); }); }); @@ -41,6 +42,7 @@ describe("spawn", () => { if (first) { // First piece of data is a welcome msg. Second is the prompt first = false; + return; } expect(data.toString().endsWith("$ ")).toBeTruthy(); @@ -92,6 +94,7 @@ describe("spawn", () => { if (output === 2) { proc.send("tput lines\n"); + return; } @@ -106,6 +109,7 @@ describe("spawn", () => { columns: 10, rows: 50, }); + return; } @@ -116,6 +120,7 @@ describe("spawn", () => { if (output === 6) { proc.send("tput lines\n"); + return; } @@ -123,7 +128,7 @@ describe("spawn", () => { // Echo of tput lines return; } - + if (output === 8) { expect(data.toString().trim()).toEqual("50"); proc.kill(); @@ -132,7 +137,7 @@ describe("spawn", () => { }); proc.on("exit", () => done()); }); - + it("should fork and echo messages", (done) => { const proc = client.fork(path.join(__dirname, "forker.js")); proc.on("message", (msg) => { @@ -146,10 +151,10 @@ describe("spawn", () => { describe("createConnection", () => { const client = createClient(); - const tmpPath = path.join(os.tmpdir(), Math.random().toString()); + const tmpPath = path.join(os.tmpdir(), Math.random().toString()); let server: net.Server; beforeAll(async () => { - await new Promise((r) => { + await new Promise((r): void => { server = net.createServer().listen(tmpPath, () => { r(); }); @@ -160,11 +165,23 @@ describe("createConnection", () => { server.close(); }); - it("should connect to socket", (done) => { - const socket = client.createConnection(tmpPath, () => { - socket.end(); - socket.addListener("close", () => { - done(); + it("should connect to socket", async () => { + await new Promise((resolve): void => { + const socket = client.createConnection(tmpPath, () => { + socket.end(); + socket.addListener("close", () => { + resolve(); + }); + }); + }); + + await new Promise((resolve): void => { + const socket = new (new Net(client)).Socket(); + socket.connect(tmpPath, () => { + socket.end(); + socket.addListener("close", () => { + resolve(); + }); }); }); }); @@ -202,7 +219,7 @@ describe("createConnection", () => { describe("createServer", () => { const client = createClient(); - const tmpPath = path.join(os.tmpdir(), Math.random().toString()); + const tmpPath = path.join(os.tmpdir(), Math.random().toString()); it("should connect to server", (done) => { const s = client.createServer(() => { @@ -233,4 +250,4 @@ describe("createServer", () => { done(); }); }); -}); \ No newline at end of file +}); diff --git a/packages/server/package.json b/packages/server/package.json index 13364aacb..e4594bfd1 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -9,6 +9,7 @@ "build:nexe": "node scripts/nexe.js", "build:bootstrap-fork": "cd ../vscode && npm run build:bootstrap-fork; cp ./bin/bootstrap-fork.js ../server/build/bootstrap-fork.js", "build:default-extensions": "cd ../../lib/vscode && npx gulp vscode-linux-arm && cd ../.. && cp -r ./lib/VSCode-linux-arm/resources/app/extensions/* ./packages/server/build/extensions/", + "build:web": "cd ../web && npm run build; mkdir ../server/build/web && cp ./out/* ../server/build/web", "build": "npm run build:bootstrap-fork && npm run build:webpack && npm run build:nexe" }, "dependencies": { @@ -17,7 +18,7 @@ "@oclif/plugin-help": "^2.1.4", "express": "^4.16.4", "nexe": "^2.0.0-rc.34", - "node-pty": "^0.8.0", + "node-pty": "^0.8.1", "spdlog": "^0.7.2", "ws": "^6.1.2" }, diff --git a/packages/server/src/cli.ts b/packages/server/src/cli.ts index a77e8d7d9..2d555f865 100644 --- a/packages/server/src/cli.ts +++ b/packages/server/src/cli.ts @@ -24,6 +24,7 @@ export class Entry extends Command { // Dev flags "bootstrap-fork": flags.string({ hidden: true }), + env: flags.string({ hidden: true }), }; public static args = [{ name: "workdir", @@ -50,6 +51,10 @@ export class Entry extends Command { const { args, flags } = this.parse(Entry); + if (flags.env) { + Object.assign(process.env, JSON.parse(flags.env)); + } + if (flags["bootstrap-fork"]) { const modulePath = flags["bootstrap-fork"]; if (!modulePath) { @@ -95,7 +100,7 @@ export class Entry extends Command { next(); }); - if (process.env.CLI === "false" || !process.env.CLI) { + if ((process.env.CLI === "false" || !process.env.CLI) && !process.env.SERVE_STATIC) { const webpackConfig = require(path.join(__dirname, "..", "..", "web", "webpack.dev.config.js")); const compiler = require("webpack")(webpackConfig); app.use(require("webpack-dev-middleware")(compiler, { diff --git a/packages/server/src/server.ts b/packages/server/src/server.ts index ccd6d9f93..7fb04ffef 100644 --- a/packages/server/src/server.ts +++ b/packages/server/src/server.ts @@ -1,10 +1,11 @@ -import { field, logger } from "@coder/logger"; +import { logger } from "@coder/logger"; import { ReadWriteConnection } from "@coder/protocol"; import { Server, ServerOptions } from "@coder/protocol/src/node/server"; import { NewSessionMessage } from '@coder/protocol/src/proto'; import { ChildProcess } from "child_process"; import * as express from "express"; import * as http from "http"; +import * as path from "path"; import * as ws from "ws"; import { forkModule } from "./vscode/bootstrapFork"; @@ -46,7 +47,11 @@ export const createApp = (registerMiddleware?: (app: express.Application) => voi forkProvider: (message: NewSessionMessage): ChildProcess => { let proc: ChildProcess; if (message.getIsBootstrapFork()) { - proc = forkModule(message.getCommand()); + const env: NodeJS.ProcessEnv = {}; + message.getEnvMap().forEach((value, key) => { + env[key] = value; + }); + proc = forkModule(message.getCommand(), env); } else { throw new Error("No support for non bootstrap-forking yet"); } @@ -56,14 +61,7 @@ export const createApp = (registerMiddleware?: (app: express.Application) => voi } : undefined); }); - /** - * We should static-serve the `web` package at this point. - */ - app.get("/", (req, res, next) => { - res.write("Example! :)"); - res.status(200); - res.end(); - }); + app.use(express.static(path.join(__dirname, "../build/web"))); return { express: app, diff --git a/packages/server/src/vscode/bootstrapFork.ts b/packages/server/src/vscode/bootstrapFork.ts index c50e5e4a2..265c86d59 100644 --- a/packages/server/src/vscode/bootstrapFork.ts +++ b/packages/server/src/vscode/bootstrapFork.ts @@ -5,7 +5,9 @@ import * as path from "path"; export const requireModule = (modulePath: string): void => { process.env.AMD_ENTRYPOINT = modulePath; - process.env.VSCODE_ALLOW_IO = "true"; + + // Always do this so we can see console.logs. + // process.env.VSCODE_ALLOW_IO = "true"; if (!process.send) { const socket = new net.Socket({ fd: 3 }); @@ -31,10 +33,13 @@ export const requireModule = (modulePath: string): void => { * cp.stderr.on("data", (data) => console.log(data.toString("utf8"))); * @param modulePath Path of the VS Code module to load. */ -export const forkModule = (modulePath: string): cp.ChildProcess => { +export const forkModule = (modulePath: string, env?: NodeJS.ProcessEnv): cp.ChildProcess => { let proc: cp.ChildProcess | undefined; const args = ["--bootstrap-fork", modulePath]; + if (env) { + args.push("--env", JSON.stringify(env)); + } const options: cp.SpawnOptions = { stdio: [null, null, null, "pipe"], }; diff --git a/packages/server/src/vscode/sharedProcess.ts b/packages/server/src/vscode/sharedProcess.ts index 017b7c14a..25e52d890 100644 --- a/packages/server/src/vscode/sharedProcess.ts +++ b/packages/server/src/vscode/sharedProcess.ts @@ -66,7 +66,9 @@ export class SharedProcess { state: SharedProcessState.Starting, }); let resolved: boolean = false; - this.activeProcess = forkModule("vs/code/electron-browser/sharedProcess/sharedProcessMain", true); + this.activeProcess = forkModule("vs/code/electron-browser/sharedProcess/sharedProcessMain", { + VSCODE_ALLOW_IO: "true", + }); this.activeProcess.on("exit", (err) => { if (this._state !== SharedProcessState.Stopped) { this.setState({ diff --git a/packages/server/yarn.lock b/packages/server/yarn.lock index b8b2dd1b1..88cf64eec 100644 --- a/packages/server/yarn.lock +++ b/packages/server/yarn.lock @@ -2313,12 +2313,7 @@ mute-stream@0.0.7: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= -nan@2.10.0: - version "2.10.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.10.0.tgz#96d0cd610ebd58d4b4de9cc0c6828cda99c7548f" - integrity sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA== - -nan@^2.8.0, nan@^2.9.2: +nan@2.12.1, nan@^2.8.0, nan@^2.9.2: version "2.12.1" resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552" integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw== @@ -2396,12 +2391,12 @@ node-pre-gyp@^0.10.0: semver "^5.3.0" tar "^4" -node-pty@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.8.0.tgz#08bccb633f49e2e3f7245eb56ea6b40f37ccd64f" - integrity sha512-g5ggk3gN4gLrDmAllee5ScFyX3YzpOC/U8VJafha4pE7do0TIE1voiIxEbHSRUOPD1xYqmY+uHhOKAd3avbxGQ== +node-pty@^0.8.1: + version "0.8.1" + resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.8.1.tgz#94b457bec013e7a09b8d9141f63b0787fa25c23f" + integrity sha512-j+/g0Q5dR+vkELclpJpz32HcS3O/3EdPSGPvDXJZVJQLCvgG0toEbfmymxAEyQyZEpaoKHAcoL+PvKM+4N9nlw== dependencies: - nan "2.10.0" + nan "2.12.1" nopt@^4.0.1: version "4.0.1" diff --git a/packages/vscode/src/element/augment.ts b/packages/vscode/src/element/augment.ts deleted file mode 100644 index 777399a98..000000000 --- a/packages/vscode/src/element/augment.ts +++ /dev/null @@ -1,277 +0,0 @@ - -export function classSplice(element: HTMLElement, removeClasses: string, addClasses: string): HTMLElement { - if (removeClasses) { removeClasses.split(/\s+/g).forEach((className) => element.classList.remove(className)); } - if (addClasses) { addClasses.split(/\s+/g).forEach((className) => element.classList.add(className)); } - return element; -} - -export type Side = "LEFT" | "RIGHT" | "TOP" | "BOTTOM"; -export type BoundaryPos = [Side, Side]; -export interface IBoundary { - top: number; - left: number; - right: number; - bottom: number; -} - -export type PointPos = ["LEFT" | "CENTER" | "RIGHT", "TOP" | "CENTER" | "BOTTOM"]; - -export class FloaterPositioning { - private static positionClasses = [ - "--boundary_top_left", - "--boundary_top_right", - "--boundary_left_top", - "--boundary_right_top", - "--boundary_left_bottom", - "--boundary_right_bottom", - "--boundary_bottom_left", - "--boundary_bottom_right", - - "--point_top_left", - "--point_top_center", - "--point_top_right", - "--point_center_left", - "--point_center_center", - "--point_center_right", - "--point_bottom_left", - "--point_bottom_center", - "--point_bottom_right", - ].join(" "); - - public readonly target: HTMLElement; - constructor(target: HTMLElement) { - this.target = target; - } - - // this function was surprisingly difficult - public moveToBoundary(boundary: IBoundary, pos: BoundaryPos, keepInBounds: boolean = true) { - if (keepInBounds) { - const height = this.target.offsetHeight; - const width = this.target.offsetWidth; - if (height === 0 && width === 0) { - throw new Error("target must be added to page before it can be in bounds positioned"); - } - const flip = { - BOTTOM: "TOP", - LEFT: "RIGHT", - RIGHT: "LEFT", - TOP: "BOTTOM", - } as any; - - const getOverlap = (side: string, strong: boolean) => { - switch (side) { - case "BOTTOM": return ((strong ? boundary.bottom : boundary.top) + height) - window.innerHeight; - case "TOP": return 0 - (strong ? boundary.top : boundary.bottom) - height; - case "RIGHT": return ((strong ? boundary.right : boundary.left) + width) - window.innerWidth; - case "LEFT": return 0 - (strong ? boundary.left : boundary.right) - width; - } - }; - - const firstA = getOverlap(pos[0], true); - if (firstA > 0) { - const firstB = getOverlap(flip[pos[0]], true); - if (firstB < firstA) { - pos[0] = flip[pos[0]]; - } - } - - const secA = getOverlap(pos[1], false); - if (secA > 0) { - const secB = getOverlap(flip[pos[1]], false); - if (secB < secA) { - pos[1] = flip[pos[1]]; - } - } - - } - - classSplice(this.target, FloaterPositioning.positionClasses, undefined); - this.target.classList.add(`--boundary_${pos.map((val) => val.toLowerCase()).join("_")}`); - - const displayPos: IBoundary = {} as any; - switch (pos[0]) { - case "BOTTOM": displayPos.top = boundary.bottom; break; - case "TOP": displayPos.bottom = window.innerHeight - boundary.top; break; - case "LEFT": displayPos.right = window.innerWidth - boundary.left; break; - case "RIGHT": displayPos.left = boundary.right; break; - } - switch (pos[1]) { - case "BOTTOM": displayPos.top = boundary.top; break; - case "TOP": displayPos.bottom = window.innerHeight - boundary.bottom; break; - case "LEFT": displayPos.right = window.innerWidth - boundary.right; break; - case "RIGHT": displayPos.left = boundary.left; break; - } - this.applyPos(displayPos); - } - - public moveToPoint(point: { top: number, left: number }, pos: PointPos, keepInBounds: boolean = true): void { - if (keepInBounds) { - const height = this.target.offsetHeight; - const width = this.target.offsetWidth; - if (height === 0 && width === 0) { - throw new Error("target must be added to page before it can be in bounds positioned"); - } - const flip = { - BOTTOM: "TOP", - LEFT: "RIGHT", - RIGHT: "LEFT", - TOP: "BOTTOM", - } as any; - - const getOverlap = (side: string) => { - switch (side) { - case "BOTTOM": return (point.top + height) - window.innerHeight; - case "TOP": return -1 * (point.top - height); - case "RIGHT": return (point.left + width) - window.innerWidth; - case "LEFT": return -1 * (point.left - width); - default: return 0; - } - }; - - const xAlign = pos[0]; - const normalXOffset = getOverlap(xAlign); - if (normalXOffset > 0 && normalXOffset > getOverlap(flip[xAlign])) { - pos[0] = flip[xAlign]; - } - - const yAlign = pos[1]; - const normalYOffset = getOverlap(yAlign); - if (normalYOffset > 0 && normalYOffset > getOverlap(flip[yAlign])) { - pos[1] = flip[yAlign]; - } - } - - const displayPos: IBoundary = {} as any; - let centerX = false; - let centerY = false; - switch (pos[0]) { - case "CENTER": centerX = true; - case "RIGHT": displayPos.left = point.left; break; - case "LEFT": displayPos.right = window.innerWidth - point.left; break; - } - switch (pos[1]) { - case "CENTER": centerY = true; - case "BOTTOM": displayPos.top = point.top; break; - case "TOP": displayPos.bottom = window.innerHeight - point.top; break; - } - - classSplice(this.target, FloaterPositioning.positionClasses, undefined); - this.target.classList.add(`--point_${pos.map((val) => val.toLowerCase()).reverse().join("_")}`); - - this.applyPos(displayPos); - this.target.style.transform = `${centerX ? "translateX(-50)" : ""} ${centerY ? "translateY(-50)" : ""}`; - } - - private applyPos(pos: IBoundary) { - this.target.style.top = pos.top !== undefined ? (pos.top + "px") : ""; - this.target.style.bottom = pos.bottom !== undefined ? (pos.bottom + "px") : ""; - this.target.style.left = pos.left !== undefined ? (pos.left + "px") : ""; - this.target.style.right = pos.right !== undefined ? (pos.right + "px") : ""; - } -} - -export type Boolable = ((item: HTMLElement) => boolean) | boolean; - -export interface IMakeChildrenSelectableArgs { - maxSelectable?: number; - selectOnKeyHover?: Boolable; - selectOnMouseHover?: Boolable; - onHover?: (selectedItem: HTMLElement) => void; - onSelect: (selectedItem: HTMLElement, wasAlreadySelected?: boolean) => void; - isItemSelectable?: (item: HTMLElement) => boolean; -} - -export class SelectableChildren { - - public readonly target: HTMLElement; - private keyHoveredItem: HTMLElement; - private _selectedItem: HTMLElement; - private selectOnMouseHover: Boolable; - private onHover: (selectedItem: HTMLElement) => void; - private onSelect: (selectedItem: HTMLElement) => void; - private isItemSelectable: (item: HTMLElement) => boolean; - - constructor(target: HTMLElement, args: IMakeChildrenSelectableArgs) { - this.target = target; - - this.onHover = args.onHover; - this.onSelect = args.onSelect; - this.selectOnMouseHover = args.selectOnMouseHover || false; - this.isItemSelectable = args.isItemSelectable; - - // this.target.addEventListener("keydown", (event) => this.onTargetKeydown(event)); - this.target.addEventListener("mousemove", (event) => this.onTargetMousemove(event)); - - Array.from(this.target.children).forEach((child: HTMLElement) => this.registerChild(child)); - } - - public registerChild(child: HTMLElement) { - child.addEventListener("mouseover", (event) => this.onItemHover(child, event)); - child.addEventListener("mousedown", (event) => this.onItemMousedown(child, event)); - } - - public get selectedItem() { return this._selectedItem; } - - public unsetSelection() { - if (this.selectedItem) { this.selectedItem.classList.remove("--is_selected"); } - this._selectedItem = undefined; - } - - public trySelectItem(item: HTMLElement): boolean { - if (this.checkItemSelectable(item) === false) { return false; } - const alreadySelected = item === this.selectedItem; - if (!alreadySelected) { - this.unsetSelection(); - this._selectedItem = item; - this.selectedItem.classList.add("--is_selected"); - this.onSelect(this.selectedItem); - } - return true; - } - - public updateAllItemIsSelectableStates() { - this.updateItemIsSelectableState(Array.from(this.target.childNodes) as any); - } - - public updateItemIsSelectableState(itemOrItems?: HTMLElement | HTMLElement[]) { - const items: HTMLElement[] = Array.isArray(itemOrItems) ? itemOrItems : [itemOrItems]; - - items.forEach((item) => { - if (!this.isItemSelectable || this.isItemSelectable(item)) { - item.classList.remove("--not_selectable"); - } else { - item.classList.add("--not_selectable"); - } - }); - } - - private checkItemSelectable(item: HTMLElement): boolean { - this.updateItemIsSelectableState(item); - return item.classList.contains("--not_selectable") === false; - } - - private onTargetMousemove(event: MouseEvent) { - classSplice(this.target, "--key_naving", "--mouse_naving"); - if (this.keyHoveredItem) { - this.keyHoveredItem.classList.remove("--key_hovered"); - this.keyHoveredItem = undefined; - } - } - - private onItemHover(item: HTMLElement, event: Event) { - if (this.onHover) { this.onHover(item); } - if ( - this.checkItemSelectable(item) - && typeof this.selectOnMouseHover === "boolean" - ? this.selectOnMouseHover - : (this.selectOnMouseHover as any)(item) - ) { - this.trySelectItem(item); - } - } - - private onItemMousedown(item: HTMLElement, event: Event) { - this.trySelectItem(item); - } - -} diff --git a/packages/vscode/src/element/contextmenu.css b/packages/vscode/src/element/contextmenu.css deleted file mode 100644 index bc2e87d87..000000000 --- a/packages/vscode/src/element/contextmenu.css +++ /dev/null @@ -1,68 +0,0 @@ -.context-menu-overlay { - position: fixed; - top: 0px; - left: 0px; - right: 0px; - bottom: 0px; - z-index: 10; -} - -.command-menu { - position: fixed; - background-color: var(--floater, rgba(67, 67, 61, 1)); - border: 2px solid rgba(66, 66, 60, 1); - color: var(--fg, rgb(216, 216, 216)); - font-size: 14px; - box-shadow: 0px 0px 20px 0px rgba(0, 0, 0, 0.1); - /* border-radius: 4px; */ - overflow: hidden; -} -.command-menu.--boundary_bottom_right, .command-menu.--boundary_right_bottom, .command-menu.--point_bottom_right { - border-top-left-radius: 0px; -} -.command-menu.--boundary_bottom_left, .command-menu.--boundary_left_bottom, .command-menu.--point_bottom_left { - border-top-right-radius: 0px; -} -.command-menu.--boundary_top_right, .command-menu.--boundary_right_top, .command-menu.--point_top_right { - border-bottom-left-radius: 0px; -} -.command-menu.--boundary_top_left, .command-menu.--boundary_left_top, .command-menu.--point_top_left { - border-bottom-right-radius: 0px; -} -.command-menu .menuitem { - white-space: nowrap; - padding: 5px 20px; - cursor: pointer; - min-width: 150px; -} -.command-menu .menuitem:not(.--not_selectable):not(.--is_selected):hover { - background: var(--floaterHover, rgba(0, 0, 0, 0.2)); -} -.command-menu .menuitem.--is_selected { - background: var(--floaterActive, rgba(0, 0, 0, 0.25)); -} -.command-menu .menuitem:not(.--non_selection_item).--not_selectable { - color: var(--fgFade7, rgba(255, 255, 255, 0.3)); - cursor: unset; -} -.command-menu .menuitem.entry { - display: flex; -} -.command-menu .menuitem.spacer { - padding-left: 0px; - padding-right: 0px; -} -.command-menu .menuitem.spacer > hr { - margin: 0px; - border: none; - background: var(--fgFade7, rgba(47, 47, 41, 1)); - opacity: 0.4; - height: 1px; -} - -.command-menu .menuitem.entry > .keybind { - margin-left: auto; - padding-left: 50px; - font-size: 12px; - opacity: 0.6; -} \ No newline at end of file diff --git a/packages/vscode/src/element/contextmenu.ts b/packages/vscode/src/element/contextmenu.ts deleted file mode 100644 index 111521f03..000000000 --- a/packages/vscode/src/element/contextmenu.ts +++ /dev/null @@ -1,250 +0,0 @@ -/** - * SHOULD BE MOVED. THIS IS NOT A UI SECTION - */ - -import * as augment from './augment'; -import "./contextmenu.css"; -import { FastDomNode, createFastDomNode } from 'vs/base/browser/fastDomNode'; - -export enum MenuItemType { - COMMAND, - SUB_MENU, - SPACER, - CUSTOM_ITEM, - GENERATIVE_SUBMENU, -} - -export interface IMenuItem { - type: MenuItemType; - domNode: HTMLElement; - priority: number; - selectOnHover: boolean; - refreshDomNode?: () => void; - isSelectable?: (() => boolean) | boolean; - onSelect?: () => void; -} - -export class ContextMenuManager { - - private readonly domNode: FastDomNode; - - public constructor() { - this.domNode = createFastDomNode(document.createElement("div")); - this.domNode.setClassName("context-menu-overlay"); - // this.display = false; - this.domNode.domNode.addEventListener("mousedown", (event) => { - event.preventDefault(); - if (event.target === this.domNode.domNode) { - this.display = false; - } - }); - this.domNode.domNode.addEventListener("closeAllContextMenus", (event) => { - this.display = false; - event.stopPropagation(); - }); - this.domNode.domNode.addEventListener("contextMenuActive", (event) => { - // this.clearStackTill(event.target as HTMLElement); - event.stopPropagation(); - }); - } - - public onceClose(cb: () => void): void { - const l = () => { - cb(); - this.domNode.domNode.removeEventListener("closed", l); - }; - this.domNode.domNode.addEventListener("closed", l); - } - - public set display(value: boolean) { - if (value) { - document.body.appendChild(this.domNode.domNode); - } else { - this.domNode.domNode.remove(); - this.domNode.domNode.dispatchEvent(new Event("closed")); - } - } - - public displayMenuAtBoundary( - menu: ContextMenu, - boundary: augment.IBoundary, - positioning: augment.BoundaryPos = ["BOTTOM", "RIGHT"], - clearStack: boolean = true, - ): void { - this.displayMenu(menu, clearStack); - menu.positioningAugment.moveToBoundary(boundary, positioning); - } - - public displayMenuAtPoint( - menu: ContextMenu, - point: { top: number, left: number }, - positioning: augment.PointPos = ["RIGHT", "BOTTOM"], - clearStack: boolean = true, - ): void { - this.displayMenu(menu, clearStack); - menu.positioningAugment.moveToPoint(point, positioning); - } - - private displayMenu(menu: ContextMenu, clearStack: boolean) { - while (this.domNode.domNode.lastChild) { - this.domNode.domNode.removeChild(this.domNode.domNode.lastChild); - } - this.domNode.appendChild(menu.domNode); - this.display = true; - } - -} - -export class ContextMenu { - - public readonly id: string; - public readonly positioningAugment: augment.FloaterPositioning; - public readonly selectionAugment: augment.SelectableChildren; - public readonly domNode: FastDomNode; - private readonly manager: ContextMenuManager; - - private cachedActive: HTMLElement; - private domNodeToItemMap: Map; - private items: IMenuItem[]; - - constructor(id: string, manager: ContextMenuManager) { - this.id = id; - this.manager = manager; - this.items = []; - this.domNodeToItemMap = new Map(); - this.domNode = createFastDomNode(document.createElement("div")); - this.domNode.setClassName("command-menu"); - this.positioningAugment = new augment.FloaterPositioning(this.domNode.domNode); - - const selectOnHover = (itemDomNode: HTMLElement) => this.domNodeToItemMap.get(itemDomNode).selectOnHover; - this.selectionAugment = new augment.SelectableChildren(this.domNode.domNode, { - isItemSelectable: (itemDomNode) => { - const item = this.domNodeToItemMap.get(itemDomNode); - return typeof item.isSelectable === "boolean" ? item.isSelectable : item.isSelectable(); - }, - onHover: (itemDomNode) => { - const item = this.domNodeToItemMap.get(itemDomNode); - if (item.type !== MenuItemType.SUB_MENU && item.type !== MenuItemType.GENERATIVE_SUBMENU) { - this.domNode.domNode.dispatchEvent(new Event("contextMenuActive", { bubbles: true })); - this.selectionAugment.unsetSelection(); - } - }, - onSelect: (itemDomNode) => { - const item = this.domNodeToItemMap.get(itemDomNode); - if (item.onSelect) { item.onSelect(); } - }, - selectOnKeyHover: selectOnHover, - selectOnMouseHover: selectOnHover, - }); - } - - public set display(onOff: boolean) { - if (onOff === true) { - this.cachedActive = document.activeElement as HTMLElement; - if (this.cachedActive) { - this.cachedActive.blur(); - } - this.items.forEach((item) => !!item.refreshDomNode ? item.refreshDomNode() : null); - this.selectionAugment.updateAllItemIsSelectableStates(); - } else if (this.cachedActive) { - this.cachedActive.focus(); - this.cachedActive = null; - } - this.domNode.domNode.style.display = onOff ? "" : "none"; - } - - public addSpacer(priority: number) { - const rootNode = createFastDomNode(document.createElement("div")); - rootNode.setClassName("menuitem spacer"); - const hrNode = createFastDomNode(document.createElement("hr")); - rootNode.appendChild(hrNode); - this.appendMenuItem({ - domNode: rootNode.domNode, - isSelectable: false, - priority, - selectOnHover: false, - type: MenuItemType.SPACER, - }); - } - - public addEntry(priority: number, label: string, accelerator: string, enabled: boolean, callback: () => void) { - const domNode = createFastDomNode(document.createElement("div")); - domNode.setClassName("menuitem entry"); - const labelNode = createFastDomNode(document.createElement("div")); - labelNode.setClassName("entrylabel"); - labelNode.domNode.innerText = label; - domNode.appendChild(labelNode); - - if (accelerator) { - const accelNode = createFastDomNode(document.createElement("div")); - accelNode.setClassName("keybind"); - accelNode.domNode.innerText = accelerator; - domNode.appendChild(accelNode); - } - - - const menuItem: IMenuItem = { - domNode: domNode.domNode, - isSelectable: () => enabled, - onSelect: () => { - if (this.cachedActive) { - this.cachedActive.focus(); - this.cachedActive = null; - } - callback(); - domNode.domNode.dispatchEvent(new Event("closeAllContextMenus", { bubbles: true })); - }, - priority, - selectOnHover: false, - type: MenuItemType.COMMAND, - }; - this.appendMenuItem(menuItem); - } - - public addSubMenu(priority: number, subMenu: ContextMenu, label: string, description?: string) { - const rootNode = createFastDomNode(document.createElement("div")); - rootNode.setClassName("menuitem"); - const subLabel = createFastDomNode(document.createElement("div")); - subLabel.setClassName("seg submenulabel"); - subLabel.domNode.innerText = label; - const subArrow = createFastDomNode(document.createElement("div")); - subArrow.setClassName("seg submenuarrow"); - subArrow.domNode.innerText = "->"; - rootNode.appendChild(subLabel); - rootNode.appendChild(subArrow); - this.appendMenuItem({ - domNode: rootNode.domNode, - isSelectable: true, - onSelect: () => { - this.manager.displayMenuAtBoundary(subMenu, rootNode.domNode.getBoundingClientRect(), ["RIGHT", "BOTTOM"], false); - }, - priority, - selectOnHover: true, - type: MenuItemType.SUB_MENU, - }); - } - - // used for generative sub menu... needs to be less public - public removeAllItems() { - while (this.items.length) { - const removeMe = this.items.pop(); - removeMe.domNode.remove(); - } - } - - private appendMenuItem(item: IMenuItem) { - this.items.push(item); - this.domNodeToItemMap.set(item.domNode, item); - this.selectionAugment.registerChild(item.domNode); - this.items = this.items.sort((a, b) => a.priority - b.priority); - this.sortDomNode(); - } - - private sortDomNode() { - while (this.domNode.domNode.lastChild) { - this.domNode.domNode.removeChild(this.domNode.domNode.lastChild); - } - this.items.forEach((item) => this.domNode.domNode.appendChild(item.domNode)); - } - -} diff --git a/packages/vscode/src/fill/native-watchdog.ts b/packages/vscode/src/fill/native-watchdog.ts new file mode 100644 index 000000000..41d8afa0f --- /dev/null +++ b/packages/vscode/src/fill/native-watchdog.ts @@ -0,0 +1,9 @@ +class Watchdog { + + public start(): void { + // TODO: Should it do something? + } + +} + +export = new Watchdog(); diff --git a/packages/vscode/src/fill/product.ts b/packages/vscode/src/fill/product.ts index 1b5b9122d..678f4cfed 100644 --- a/packages/vscode/src/fill/product.ts +++ b/packages/vscode/src/fill/product.ts @@ -5,7 +5,11 @@ const product = { nameLong: "vscode online", dataFolderName: ".vscode-online", extensionsGallery: { - serviceUrl: "", + serviceUrl: "https://marketplace.visualstudio.com/_apis/public/gallery", + cacheUrl: "https://vscode.blob.core.windows.net/gallery/index", + itemUrl: "https://marketplace.visualstudio.com/items", + controlUrl: "https://az764295.vo.msecnd.net/extensions/marketplace.json", + recommendationsUrl: "https://az764295.vo.msecnd.net/extensions/workspaceRecommendations.json.gz", }, extensionExecutionEnvironments: { "wayou.vscode-todo-highlight": "worker", diff --git a/packages/vscode/src/fill/windowsService.ts b/packages/vscode/src/fill/windowsService.ts index 5f49cf9f0..2453b13f6 100644 --- a/packages/vscode/src/fill/windowsService.ts +++ b/packages/vscode/src/fill/windowsService.ts @@ -51,16 +51,31 @@ class WindowsService implements IWindowsService { throw new Error("not implemented"); } - public showMessageBox(_windowId: number, _options: MessageBoxOptions): Promise { - throw new Error("not implemented"); + public showMessageBox(windowId: number, options: MessageBoxOptions): Promise { + return new Promise((resolve): void => { + electron.dialog.showMessageBox(this.getWindowById(windowId), options, (response, checkboxChecked) => { + resolve({ + button: response, + checkboxChecked, + }); + }); + }); } - public showSaveDialog(_windowId: number, _options: SaveDialogOptions): Promise { - throw new Error("not implemented"); + public showSaveDialog(windowId: number, options: SaveDialogOptions): Promise { + return new Promise((resolve): void => { + electron.dialog.showSaveDialog(this.getWindowById(windowId), options, (filename, _bookmark) => { + resolve(filename); + }); + }); } - public showOpenDialog(_windowId: number, _options: OpenDialogOptions): Promise { - throw new Error("not implemented"); + public showOpenDialog(windowId: number, options: OpenDialogOptions): Promise { + return new Promise((resolve): void => { + electron.dialog.showOpenDialog(this.getWindowById(windowId), options, (filePaths, _bookmarks) => { + resolve(filePaths); + }); + }); } public reloadWindow(windowId: number, _args?: ParsedArgs): Promise { diff --git a/packages/vscode/webpack.config.bootstrap.js b/packages/vscode/webpack.config.bootstrap.js index 304e9c38b..c0419d648 100644 --- a/packages/vscode/webpack.config.bootstrap.js +++ b/packages/vscode/webpack.config.bootstrap.js @@ -64,6 +64,7 @@ module.exports = (env) => { "windows-process-tree": path.resolve(fills, "empty.ts"), "electron": path.join(vscodeFills, "stdioElectron.ts"), + "native-watchdog": path.join(vscodeFills, "native-watchdog.ts"), "vs/platform/node/product": path.resolve(vscodeFills, "product.ts"), "vs/platform/node/package": path.resolve(vscodeFills, "package.ts"), "vs/base/node/paths": path.resolve(vscodeFills, "paths.ts"), diff --git a/packages/web/.gitignore b/packages/web/.gitignore new file mode 100644 index 000000000..c585e1938 --- /dev/null +++ b/packages/web/.gitignore @@ -0,0 +1 @@ +out \ No newline at end of file diff --git a/packages/web/package.json b/packages/web/package.json new file mode 100644 index 000000000..5a070bfcf --- /dev/null +++ b/packages/web/package.json @@ -0,0 +1,6 @@ +{ + "name": "@coder/web", + "scripts": { + "build": "../../node_modules/.bin/webpack --config ./webpack.dev.config.js" + } +} \ No newline at end of file diff --git a/packages/web/webpack.common.config.js b/packages/web/webpack.common.config.js index c93dbbcf3..2ae099cee 100644 --- a/packages/web/webpack.common.config.js +++ b/packages/web/webpack.common.config.js @@ -55,7 +55,6 @@ 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"), @@ -74,6 +73,7 @@ module.exports = merge({ "node-pty": path.join(vsFills, "node-pty.ts"), "graceful-fs": path.join(vsFills, "graceful-fs.ts"), "spdlog": path.join(vsFills, "spdlog.ts"), + "native-watchdog": path.join(vsFills, "native-watchdog.ts"), "iconv-lite": path.join(vsFills, "iconv-lite.ts"), "vs/base/node/paths": path.join(vsFills, "paths.ts"), diff --git a/packages/web/webpack.dev.config.js b/packages/web/webpack.dev.config.js index 7f82e9789..2cce680d5 100644 --- a/packages/web/webpack.dev.config.js +++ b/packages/web/webpack.dev.config.js @@ -1,8 +1,12 @@ +const path = require("path"); const webpack = require("webpack"); const merge = require("webpack-merge"); module.exports = merge(require("./webpack.common.config.js"), { devtool: "cheap-module-eval-source-map", + output: { + path: path.join(__dirname, "out"), + }, entry: [ "webpack-hot-middleware/client?reload=true&quiet=true", "./packages/web/src/index.ts"