Extension host (#20)

* Implement net.Server

* Move Socket class into Client

This way we don't need to expose anything.

* Remove some unused imports

* Pass environment variables to bootstrap fork

* Add debug log for when socket disconnects from server

* Use VSCODE_ALLOW_IO for shared process only

* Extension host can send messages now

* Support callback for logging

This lets us do potentially expensive operations which will only be
performed if the log level is sufficiently low.

* Stop extension host from committing suicide

* Blank line

* Add static serve (#21)

* Add extension URLs

* how did i remove this

* Fix writing an empty string

* Implement dialogs on window service
This commit is contained in:
Asher 2019-01-25 18:18:21 -06:00 committed by Kyle Carberry
parent e43e7b36e7
commit c6d35d098a
No known key found for this signature in database
GPG Key ID: A0409BDB6B0B3EDB
27 changed files with 431 additions and 793 deletions

View File

@ -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,

View File

@ -42,7 +42,12 @@ export class Time {
}
export type FieldArray = Array<Field<any>>; // tslint:disable-line no-any
// tslint:disable-next-line no-any
export type FieldArray = Array<Field<any>>;
// 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<Field<any>>): void;
/**
@ -184,7 +190,9 @@ export class BrowserFormatter extends Formatter {
this.args.push(arg);
}
// tslint:disable-next-line no-any
public fields(fields: Array<Field<any>>): 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<Field<any>>): 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;

View File

@ -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<EvalDoneMessage> = new Emitter();
private evalFailedEmitter: Emitter<EvalFailedMessage> = 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);
}
}

View File

@ -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(
@ -316,4 +342,5 @@ export class ServerListener extends events.EventEmitter implements Server {
return this;
}
}

View File

@ -40,19 +40,38 @@ export class CP {
);
});
// @ts-ignore
// @ts-ignore TODO: not fully implemented
return childProcess;
}
public fork = (modulePath: string, args?: ReadonlyArray<string> | 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<string> | 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,
);
}
}

View File

@ -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);

View File

@ -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 {

View File

@ -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[]) => (<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);
};

View File

@ -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);
}
}

View File

@ -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";
(<any>global).TextDecoder = TextDecoder;
(<any>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;
}
@ -149,7 +154,7 @@ describe("createConnection", () => {
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();
});
});
});
});

View File

@ -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"
},

View File

@ -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, {

View File

@ -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,

View File

@ -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"],
};

View File

@ -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({

View File

@ -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"

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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<HTMLDivElement>;
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<T>(
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<T>(
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<HTMLDivElement>;
private readonly manager: ContextMenuManager;
private cachedActive: HTMLElement;
private domNodeToItemMap: Map<HTMLElement, IMenuItem>;
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));
}
}

View File

@ -0,0 +1,9 @@
class Watchdog {
public start(): void {
// TODO: Should it do something?
}
}
export = new Watchdog();

View File

@ -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",

View File

@ -51,16 +51,31 @@ class WindowsService implements IWindowsService {
throw new Error("not implemented");
}
public showMessageBox(_windowId: number, _options: MessageBoxOptions): Promise<IMessageBoxResult> {
throw new Error("not implemented");
public showMessageBox(windowId: number, options: MessageBoxOptions): Promise<IMessageBoxResult> {
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<string> {
throw new Error("not implemented");
public showSaveDialog(windowId: number, options: SaveDialogOptions): Promise<string> {
return new Promise((resolve): void => {
electron.dialog.showSaveDialog(this.getWindowById(windowId), options, (filename, _bookmark) => {
resolve(filename);
});
});
}
public showOpenDialog(_windowId: number, _options: OpenDialogOptions): Promise<string[]> {
throw new Error("not implemented");
public showOpenDialog(windowId: number, options: OpenDialogOptions): Promise<string[]> {
return new Promise((resolve): void => {
electron.dialog.showOpenDialog(this.getWindowById(windowId), options, (filePaths, _bookmarks) => {
resolve(filePaths);
});
});
}
public reloadWindow(windowId: number, _args?: ParsedArgs): Promise<void> {

View File

@ -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"),

1
packages/web/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
out

View File

@ -0,0 +1,6 @@
{
"name": "@coder/web",
"scripts": {
"build": "../../node_modules/.bin/webpack --config ./webpack.dev.config.js"
}
}

View File

@ -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"),

View File

@ -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"