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:
parent
e43e7b36e7
commit
c6d35d098a
@ -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,
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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, {
|
||||
|
@ -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,
|
||||
|
@ -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"],
|
||||
};
|
||||
|
@ -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({
|
||||
|
@ -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"
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
@ -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));
|
||||
}
|
||||
|
||||
}
|
9
packages/vscode/src/fill/native-watchdog.ts
Normal file
9
packages/vscode/src/fill/native-watchdog.ts
Normal file
@ -0,0 +1,9 @@
|
||||
class Watchdog {
|
||||
|
||||
public start(): void {
|
||||
// TODO: Should it do something?
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export = new Watchdog();
|
@ -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",
|
||||
|
@ -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> {
|
||||
|
@ -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
1
packages/web/.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
out
|
6
packages/web/package.json
Normal file
6
packages/web/package.json
Normal file
@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@coder/web",
|
||||
"scripts": {
|
||||
"build": "../../node_modules/.bin/webpack --config ./webpack.dev.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"),
|
||||
|
@ -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"
|
||||
|
Reference in New Issue
Block a user