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) {
|
if (this.options.buttons && this.options.buttons.length > 0) {
|
||||||
this.buttons = this.options.buttons.map((buttonText, buttonIndex) => {
|
this.buttons = this.options.buttons.map((buttonText, buttonIndex) => {
|
||||||
const button = document.createElement("button");
|
const button = document.createElement("button");
|
||||||
button.innerText = buttonText;
|
// TODO: support mnemonics.
|
||||||
|
button.innerText = buttonText.replace("_", "");
|
||||||
button.addEventListener("click", () => {
|
button.addEventListener("click", () => {
|
||||||
this.actionEmitter.emit({
|
this.actionEmitter.emit({
|
||||||
buttonIndex,
|
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
|
* 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: string, color?: string, weight?: string): void;
|
||||||
public abstract push(arg: any): void; // tslint:disable-line no-any
|
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;
|
public abstract fields(fields: Array<Field<any>>): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -184,7 +190,9 @@ export class BrowserFormatter extends Formatter {
|
||||||
this.args.push(arg);
|
this.args.push(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tslint:disable-next-line no-any
|
||||||
public fields(fields: Array<Field<any>>): void {
|
public fields(fields: Array<Field<any>>): void {
|
||||||
|
// tslint:disable-next-line no-console
|
||||||
console.groupCollapsed(...this.flush());
|
console.groupCollapsed(...this.flush());
|
||||||
fields.forEach((field) => {
|
fields.forEach((field) => {
|
||||||
this.push(field.identifier, "#3794ff", "bold");
|
this.push(field.identifier, "#3794ff", "bold");
|
||||||
|
@ -193,8 +201,10 @@ export class BrowserFormatter extends Formatter {
|
||||||
}
|
}
|
||||||
this.push(": ");
|
this.push(": ");
|
||||||
this.push(field.value);
|
this.push(field.value);
|
||||||
|
// tslint:disable-next-line no-console
|
||||||
console.log(...this.flush());
|
console.log(...this.flush());
|
||||||
});
|
});
|
||||||
|
// tslint:disable-next-line no-console
|
||||||
console.groupEnd();
|
console.groupEnd();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -229,8 +239,10 @@ export class ServerFormatter extends Formatter {
|
||||||
this.args.push(arg);
|
this.args.push(arg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// tslint:disable-next-line no-any
|
||||||
public fields(fields: Array<Field<any>>): void {
|
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";
|
this.format += "\u001B[38;2;140;140;140m";
|
||||||
fields.forEach((field) => {
|
fields.forEach((field) => {
|
||||||
obj[field.identifier] = field.value;
|
obj[field.identifier] = field.value;
|
||||||
|
@ -284,57 +296,61 @@ export class Logger {
|
||||||
/**
|
/**
|
||||||
* Outputs information.
|
* Outputs information.
|
||||||
*/
|
*/
|
||||||
public info(msg: string, ...fields: FieldArray): void {
|
public info(fn: LogCallback): void;
|
||||||
if (this.level <= Level.Info) {
|
public info(message: string, ...fields: FieldArray): void;
|
||||||
this.handle({
|
public info(message: LogCallback | string, ...fields: FieldArray): void {
|
||||||
type: "info",
|
this.handle({
|
||||||
message: msg,
|
type: "info",
|
||||||
fields,
|
message,
|
||||||
tagColor: "#008FBF",
|
fields,
|
||||||
});
|
tagColor: "#008FBF",
|
||||||
}
|
level: Level.Info,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Outputs a warning.
|
* Outputs a warning.
|
||||||
*/
|
*/
|
||||||
public warn(msg: string, ...fields: FieldArray): void {
|
public warn(fn: LogCallback): void;
|
||||||
if (this.level <= Level.Warn) {
|
public warn(message: string, ...fields: FieldArray): void;
|
||||||
this.handle({
|
public warn(message: LogCallback | string, ...fields: FieldArray): void {
|
||||||
type: "warn",
|
this.handle({
|
||||||
message: msg,
|
type: "warn",
|
||||||
fields,
|
message,
|
||||||
tagColor: "#FF9D00",
|
fields,
|
||||||
});
|
tagColor: "#FF9D00",
|
||||||
}
|
level: Level.Warn,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Outputs a debug message.
|
* Outputs a debug message.
|
||||||
*/
|
*/
|
||||||
public debug(msg: string, ...fields: FieldArray): void {
|
public debug(fn: LogCallback): void;
|
||||||
if (this.level <= Level.Debug) {
|
public debug(message: string, ...fields: FieldArray): void;
|
||||||
this.handle({
|
public debug(message: LogCallback | string, ...fields: FieldArray): void {
|
||||||
type: "debug",
|
this.handle({
|
||||||
message: msg,
|
type: "debug",
|
||||||
fields,
|
message,
|
||||||
tagColor: "#84009E",
|
fields,
|
||||||
});
|
tagColor: "#84009E",
|
||||||
}
|
level: Level.Debug,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Outputs an error.
|
* Outputs an error.
|
||||||
*/
|
*/
|
||||||
public error(msg: string, ...fields: FieldArray): void {
|
public error(fn: LogCallback): void;
|
||||||
if (this.level <= Level.Error) {
|
public error(message: string, ...fields: FieldArray): void;
|
||||||
this.handle({
|
public error(message: LogCallback | string, ...fields: FieldArray): void {
|
||||||
type: "error",
|
this.handle({
|
||||||
message: msg,
|
type: "error",
|
||||||
fields,
|
message,
|
||||||
tagColor: "#B00000",
|
fields,
|
||||||
});
|
tagColor: "#B00000",
|
||||||
}
|
level: Level.Error,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -355,15 +371,22 @@ export class Logger {
|
||||||
*/
|
*/
|
||||||
private handle(options: {
|
private handle(options: {
|
||||||
type: "info" | "warn" | "debug" | "error";
|
type: "info" | "warn" | "debug" | "error";
|
||||||
message: string;
|
message: string | LogCallback;
|
||||||
fields?: FieldArray;
|
fields?: FieldArray;
|
||||||
|
level: Level;
|
||||||
tagColor: string;
|
tagColor: string;
|
||||||
}): void {
|
}): void {
|
||||||
if (this.muted) {
|
if (this.level > options.level || this.muted) {
|
||||||
return;
|
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
|
const fields = this.defaultFields
|
||||||
? passedFields.concat(this.defaultFields)
|
? passedFields.concat(this.defaultFields)
|
||||||
: passedFields;
|
: passedFields;
|
||||||
|
|
|
@ -1,13 +1,16 @@
|
||||||
import { ReadWriteConnection, InitData, OperatingSystem, ISharedProcessData } from "../common/connection";
|
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 { Emitter, Event } from "@coder/events";
|
||||||
import { logger, field } from "@coder/logger";
|
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.
|
* Client accepts an arbitrary connection intended to communicate with the Server.
|
||||||
*/
|
*/
|
||||||
export class Client {
|
export class Client {
|
||||||
|
|
||||||
|
public Socket: typeof ServerSocket;
|
||||||
|
|
||||||
private evalId: number = 0;
|
private evalId: number = 0;
|
||||||
private evalDoneEmitter: Emitter<EvalDoneMessage> = new Emitter();
|
private evalDoneEmitter: Emitter<EvalDoneMessage> = new Emitter();
|
||||||
private evalFailedEmitter: Emitter<EvalFailedMessage> = 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.initDataPromise = new Promise((resolve): void => {
|
||||||
this.initDataEmitter.event(resolve);
|
this.initDataEmitter.event(resolve);
|
||||||
});
|
});
|
||||||
|
@ -77,7 +89,7 @@ export class Client {
|
||||||
const newEval = new NewEvalMessage();
|
const newEval = new NewEvalMessage();
|
||||||
const id = this.evalId++;
|
const id = this.evalId++;
|
||||||
newEval.setId(id);
|
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());
|
newEval.setFunction(func.toString());
|
||||||
|
|
||||||
const clientMsg = new ClientMessage();
|
const clientMsg = new ClientMessage();
|
||||||
|
@ -158,7 +170,7 @@ export class Client {
|
||||||
* @param args Args to add for the module
|
* @param args Args to add for the module
|
||||||
* @param options Options to execute
|
* @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);
|
return this.doSpawn(modulePath, args, options, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,27 +179,17 @@ export class Client {
|
||||||
* Forks a module from bootstrap-fork
|
* Forks a module from bootstrap-fork
|
||||||
* @param modulePath Path of the module
|
* @param modulePath Path of the module
|
||||||
*/
|
*/
|
||||||
public bootstrapFork(modulePath: string): ChildProcess {
|
public bootstrapFork(modulePath: string, args: string[] = [], options?: ForkOptions): ChildProcess {
|
||||||
return this.doSpawn(modulePath, [], undefined, true, true);
|
return this.doSpawn(modulePath, args, options, true, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public createConnection(path: string, callback?: () => void): Socket;
|
public createConnection(path: string, callback?: Function): Socket;
|
||||||
public createConnection(port: number, callback?: () => void): Socket;
|
public createConnection(port: number, callback?: Function): Socket;
|
||||||
public createConnection(target: string | number, callback?: () => void): Socket {
|
public createConnection(target: string | number, callback?: Function): Socket;
|
||||||
|
public createConnection(target: string | number, callback?: Function): Socket {
|
||||||
const id = this.connectionId++;
|
const id = this.connectionId++;
|
||||||
const newCon = new NewConnectionMessage();
|
const socket = new ServerSocket(this.connection, id, this.registerConnection);
|
||||||
newCon.setId(id);
|
socket.connect(target, callback);
|
||||||
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);
|
|
||||||
|
|
||||||
return socket;
|
return socket;
|
||||||
}
|
}
|
||||||
|
@ -214,7 +216,9 @@ export class Client {
|
||||||
}
|
}
|
||||||
if (options.env) {
|
if (options.env) {
|
||||||
Object.keys(options.env).forEach((envKey) => {
|
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) {
|
if (options.tty) {
|
||||||
|
@ -356,9 +360,9 @@ export class Client {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const conId = message.getServerConnectionEstablished()!.getConnectionId();
|
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");
|
serverSocket.emit("connect");
|
||||||
this.connections.set(conId, serverSocket);
|
|
||||||
s.emit("connection", serverSocket);
|
s.emit("connection", serverSocket);
|
||||||
} else if (message.getServerFailure()) {
|
} else if (message.getServerFailure()) {
|
||||||
const s = this.servers.get(message.getServerFailure()!.getId());
|
const s = this.servers.get(message.getServerFailure()!.getId());
|
||||||
|
@ -376,4 +380,12 @@ export class Client {
|
||||||
this.servers.delete(message.getServerClose()!.getId());
|
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 events from "events";
|
||||||
import * as stream from "stream";
|
import * as stream from "stream";
|
||||||
import { ReadWriteConnection } from "../common/connection";
|
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 {
|
export interface TTYDimensions {
|
||||||
readonly columns: number;
|
readonly columns: number;
|
||||||
|
@ -10,10 +10,15 @@ export interface TTYDimensions {
|
||||||
|
|
||||||
export interface SpawnOptions {
|
export interface SpawnOptions {
|
||||||
cwd?: string;
|
cwd?: string;
|
||||||
env?: { readonly [key: string]: string };
|
env?: { [key: string]: string };
|
||||||
tty?: TTYDimensions;
|
tty?: TTYDimensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ForkOptions {
|
||||||
|
cwd?: string;
|
||||||
|
env?: { [key: string]: string };
|
||||||
|
}
|
||||||
|
|
||||||
export interface ChildProcess {
|
export interface ChildProcess {
|
||||||
readonly stdin: stream.Writable;
|
readonly stdin: stream.Writable;
|
||||||
readonly stdout: stream.Readable;
|
readonly stdout: stream.Readable;
|
||||||
|
@ -119,6 +124,9 @@ export interface Socket {
|
||||||
write(buffer: Buffer): void;
|
write(buffer: Buffer): void;
|
||||||
end(): 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: "data", listener: (data: Buffer) => void): this;
|
||||||
addListener(event: "close", listener: (hasError: boolean) => void): this;
|
addListener(event: "close", listener: (hasError: boolean) => void): this;
|
||||||
addListener(event: "connect", listener: () => void): this;
|
addListener(event: "connect", listener: () => void): this;
|
||||||
|
@ -151,21 +159,37 @@ export class ServerSocket extends events.EventEmitter implements Socket {
|
||||||
public readable: boolean = true;
|
public readable: boolean = true;
|
||||||
|
|
||||||
private _destroyed: boolean = false;
|
private _destroyed: boolean = false;
|
||||||
private _connecting: boolean = true;
|
private _connecting: boolean = false;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
private readonly connection: ReadWriteConnection,
|
private readonly connection: ReadWriteConnection,
|
||||||
private readonly id: number,
|
private readonly id: number,
|
||||||
connectCallback?: () => void,
|
private readonly beforeConnect: (id: number, socket: ServerSocket) => void,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
if (connectCallback) {
|
public connect(target: string | number, callback?: Function): void {
|
||||||
this.once("connect", () => {
|
this._connecting = true;
|
||||||
this._connecting = false;
|
this.beforeConnect(this.id, this);
|
||||||
connectCallback();
|
|
||||||
});
|
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 {
|
public get destroyed(): boolean {
|
||||||
|
@ -236,6 +260,7 @@ export class ServerSocket extends events.EventEmitter implements Socket {
|
||||||
public setDefaultEncoding(encoding: string): this {
|
public setDefaultEncoding(encoding: string): this {
|
||||||
throw new Error("Method not implemented.");
|
throw new Error("Method not implemented.");
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Server {
|
export interface Server {
|
||||||
|
@ -266,6 +291,7 @@ export interface Server {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ServerListener extends events.EventEmitter implements Server {
|
export class ServerListener extends events.EventEmitter implements Server {
|
||||||
|
|
||||||
private _listening: boolean = false;
|
private _listening: boolean = false;
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
|
@ -316,4 +342,5 @@ export class ServerListener extends events.EventEmitter implements Server {
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
|
@ -40,19 +40,38 @@ export class CP {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore TODO: not fully implemented
|
||||||
return childProcess;
|
return childProcess;
|
||||||
}
|
}
|
||||||
|
|
||||||
public fork = (modulePath: string, args?: ReadonlyArray<string> | cp.ForkOptions, options?: cp.ForkOptions): cp.ChildProcess => {
|
public fork = (modulePath: string, args?: string[] | cp.ForkOptions, options?: cp.ForkOptions): cp.ChildProcess => {
|
||||||
//@ts-ignore
|
if (options && options.env && options.env.AMD_ENTRYPOINT) {
|
||||||
return this.client.bootstrapFork(options && options.env && options.env.AMD_ENTRYPOINT || modulePath);
|
// @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 => {
|
public spawn = (command: string, args?: string[] | cp.SpawnOptions, options?: cp.SpawnOptions): cp.ChildProcess => {
|
||||||
// TODO: fix this ignore. Should check for args or options here
|
// @ts-ignore TODO: not fully implemented
|
||||||
//@ts-ignore
|
return this.client.spawn(
|
||||||
return this.client.spawn(command, args, options);
|
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 util.promisify(fs.read)(fd, buffer, 0, length, position).then((resp) => {
|
||||||
return {
|
return {
|
||||||
bytesRead: resp.bytesRead,
|
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) => {
|
}, fd, length, position).then((resp) => {
|
||||||
const newBuf = Buffer.from(resp.content, "utf8");
|
const newBuf = Buffer.from(resp.content, "utf8");
|
||||||
buffer.set(newBuf, offset);
|
buffer.set(newBuf, offset);
|
||||||
|
|
|
@ -13,7 +13,8 @@ export class Net implements NodeNet {
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
public get Socket(): typeof net.Socket {
|
public get Socket(): typeof net.Socket {
|
||||||
throw new Error("not implemented");
|
// @ts-ignore
|
||||||
|
return this.client.Socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
public get Server(): typeof net.Server {
|
public get Server(): typeof net.Server {
|
||||||
|
@ -24,10 +25,12 @@ export class Net implements NodeNet {
|
||||||
throw new Error("not implemented");
|
throw new Error("not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
// tslint:disable-next-line no-any
|
public createConnection(target: string | number | net.NetConnectOpts, host?: string | Function, callback?: Function): net.Socket {
|
||||||
public createConnection(...args: any[]): net.Socket {
|
if (typeof target === "object") {
|
||||||
//@ts-ignore
|
throw new Error("not implemented");
|
||||||
return this.client.createConnection(...args) as net.Socket;
|
}
|
||||||
|
|
||||||
|
return this.client.createConnection(target, typeof host === "function" ? host : callback) as net.Socket;
|
||||||
}
|
}
|
||||||
|
|
||||||
public isIP(_input: string): number {
|
public isIP(_input: string): number {
|
||||||
|
|
|
@ -3,6 +3,7 @@ import * as net from "net";
|
||||||
import * as nodePty from "node-pty";
|
import * as nodePty from "node-pty";
|
||||||
import * as stream from "stream";
|
import * as stream from "stream";
|
||||||
import { TextEncoder } from "text-encoding";
|
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 { NewSessionMessage, ServerMessage, SessionDoneMessage, SessionOutputMessage, IdentifySessionMessage, NewConnectionMessage, ConnectionEstablishedMessage, NewConnectionFailureMessage, ConnectionCloseMessage, ConnectionOutputMessage, NewServerMessage, ServerEstablishedMessage, NewServerFailureMessage, ServerCloseMessage, ServerConnectionEstablishedMessage } from "../proto";
|
||||||
import { SendableConnection } from "../common/connection";
|
import { SendableConnection } from "../common/connection";
|
||||||
import { ServerOptions } from "./server";
|
import { ServerOptions } from "./server";
|
||||||
|
@ -25,10 +26,18 @@ export interface Process {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const handleNewSession = (connection: SendableConnection, newSession: NewSessionMessage, serverOptions: ServerOptions | undefined, onExit: () => void): 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;
|
let process: Process;
|
||||||
|
|
||||||
const env = {} as any;
|
const env: { [key: string]: string } = {};
|
||||||
newSession.getEnvMap().forEach((value: any, key: any) => {
|
newSession.getEnvMap().forEach((value, key) => {
|
||||||
env[key] = value;
|
env[key] = value;
|
||||||
});
|
});
|
||||||
if (newSession.getTtyDimensions()) {
|
if (newSession.getTtyDimensions()) {
|
||||||
|
@ -64,14 +73,29 @@ export const handleNewSession = (connection: SendableConnection, newSession: New
|
||||||
stderr: proc.stderr,
|
stderr: proc.stderr,
|
||||||
stdout: proc.stdout,
|
stdout: proc.stdout,
|
||||||
stdio: proc.stdio,
|
stdio: proc.stdio,
|
||||||
on: (...args: any[]) => (<any>proc.on)(...args),
|
on: (...args: any[]): void => ((proc as any).on)(...args), // tslint:disable-line no-any
|
||||||
write: (d) => proc.stdin.write(d),
|
write: (d): boolean => proc.stdin.write(d),
|
||||||
kill: (s) => proc.kill(s || "SIGTERM"),
|
kill: (s): void => proc.kill(s || "SIGTERM"),
|
||||||
pid: proc.pid,
|
pid: proc.pid,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendOutput = (_source: SessionOutputMessage.Source, msg: string | Uint8Array): void => {
|
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 serverMsg = new ServerMessage();
|
||||||
const d = new SessionOutputMessage();
|
const d = new SessionOutputMessage();
|
||||||
d.setId(newSession.getId());
|
d.setId(newSession.getId());
|
||||||
|
@ -110,6 +134,7 @@ export const handleNewSession = (connection: SendableConnection, newSession: New
|
||||||
connection.send(sm.serializeBinary());
|
connection.send(sm.serializeBinary());
|
||||||
|
|
||||||
process.on("exit", (code) => {
|
process.on("exit", (code) => {
|
||||||
|
childLogger.debug("Exited", field("id", newSession.getId()));
|
||||||
const serverMsg = new ServerMessage();
|
const serverMsg = new ServerMessage();
|
||||||
const exit = new SessionDoneMessage();
|
const exit = new SessionDoneMessage();
|
||||||
exit.setId(newSession.getId());
|
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 => {
|
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();
|
const id = newConnection.getId();
|
||||||
let socket: net.Socket;
|
let socket: net.Socket;
|
||||||
let didConnect = false;
|
let didConnect = false;
|
||||||
const connectCallback = () => {
|
const connectCallback = (): void => {
|
||||||
|
childLogger.debug("Connected", field("id", newConnection.getId()), field("target", target));
|
||||||
didConnect = true;
|
didConnect = true;
|
||||||
const estab = new ConnectionEstablishedMessage();
|
const estab = new ConnectionEstablishedMessage();
|
||||||
estab.setId(id);
|
estab.setId(id);
|
||||||
|
@ -145,6 +174,7 @@ export const handleNewConnection = (connection: SendableConnection, newConnectio
|
||||||
}
|
}
|
||||||
|
|
||||||
socket.addListener("error", (err) => {
|
socket.addListener("error", (err) => {
|
||||||
|
childLogger.debug("Error", field("id", newConnection.getId()), field("error", err));
|
||||||
if (!didConnect) {
|
if (!didConnect) {
|
||||||
const errMsg = new NewConnectionFailureMessage();
|
const errMsg = new NewConnectionFailureMessage();
|
||||||
errMsg.setId(id);
|
errMsg.setId(id);
|
||||||
|
@ -158,6 +188,7 @@ export const handleNewConnection = (connection: SendableConnection, newConnectio
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.addListener("close", () => {
|
socket.addListener("close", () => {
|
||||||
|
childLogger.debug("Closed", field("id", newConnection.getId()));
|
||||||
if (didConnect) {
|
if (didConnect) {
|
||||||
const closed = new ConnectionCloseMessage();
|
const closed = new ConnectionCloseMessage();
|
||||||
closed.setId(id);
|
closed.setId(id);
|
||||||
|
@ -170,6 +201,11 @@ export const handleNewConnection = (connection: SendableConnection, newConnectio
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.addListener("data", (data) => {
|
socket.addListener("data", (data) => {
|
||||||
|
childLogger.debug(() => [
|
||||||
|
"ipc",
|
||||||
|
field("id", newConnection.getId()),
|
||||||
|
field("data", data),
|
||||||
|
]);
|
||||||
const dataMsg = new ConnectionOutputMessage();
|
const dataMsg = new ConnectionOutputMessage();
|
||||||
dataMsg.setId(id);
|
dataMsg.setId(id);
|
||||||
dataMsg.setData(data);
|
dataMsg.setData(data);
|
||||||
|
@ -181,11 +217,15 @@ export const handleNewConnection = (connection: SendableConnection, newConnectio
|
||||||
return socket;
|
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();
|
const s = net.createServer();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
s.listen(newServer.getPath() ? newServer.getPath() : newServer.getPort(), () => {
|
s.listen(newServer.getPath() ? newServer.getPath() : newServer.getPort(), () => {
|
||||||
|
childLogger.debug("Listening", field("id", newServer.getId()), field("target", target));
|
||||||
const se = new ServerEstablishedMessage();
|
const se = new ServerEstablishedMessage();
|
||||||
se.setId(newServer.getId());
|
se.setId(newServer.getId());
|
||||||
const sm = new ServerMessage();
|
const sm = new ServerMessage();
|
||||||
|
@ -193,6 +233,7 @@ export const handleNewServer = (connection: SendableConnection, newServer: NewSe
|
||||||
connection.send(sm.serializeBinary());
|
connection.send(sm.serializeBinary());
|
||||||
});
|
});
|
||||||
} catch (ex) {
|
} catch (ex) {
|
||||||
|
childLogger.debug("Failed to listen", field("id", newServer.getId()), field("target", target));
|
||||||
const sf = new NewServerFailureMessage();
|
const sf = new NewServerFailureMessage();
|
||||||
sf.setId(newServer.getId());
|
sf.setId(newServer.getId());
|
||||||
const sm = new ServerMessage();
|
const sm = new ServerMessage();
|
||||||
|
@ -203,6 +244,7 @@ export const handleNewServer = (connection: SendableConnection, newServer: NewSe
|
||||||
}
|
}
|
||||||
|
|
||||||
s.on("close", () => {
|
s.on("close", () => {
|
||||||
|
childLogger.debug("Stopped listening", field("id", newServer.getId()), field("target", target));
|
||||||
const sc = new ServerCloseMessage();
|
const sc = new ServerCloseMessage();
|
||||||
sc.setId(newServer.getId());
|
sc.setId(newServer.getId());
|
||||||
const sm = new ServerMessage();
|
const sm = new ServerMessage();
|
||||||
|
@ -214,6 +256,7 @@ export const handleNewServer = (connection: SendableConnection, newServer: NewSe
|
||||||
|
|
||||||
s.on("connection", (socket) => {
|
s.on("connection", (socket) => {
|
||||||
const socketId = addSocket(socket);
|
const socketId = addSocket(socket);
|
||||||
|
childLogger.debug("Got connection", field("id", newServer.getId()), field("socketId", socketId));
|
||||||
|
|
||||||
const sock = new ServerConnectionEstablishedMessage();
|
const sock = new ServerConnectionEstablishedMessage();
|
||||||
sock.setServerId(newServer.getId());
|
sock.setServerId(newServer.getId());
|
||||||
|
@ -221,7 +264,54 @@ export const handleNewServer = (connection: SendableConnection, newServer: NewSe
|
||||||
const sm = new ServerMessage();
|
const sm = new ServerMessage();
|
||||||
sm.setServerConnectionEstablished(sock);
|
sm.setServerConnectionEstablished(sock);
|
||||||
connection.send(sm.serializeBinary());
|
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;
|
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 { mkdir, WriteStream } from "fs";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
import { TextDecoder } from "text-encoding";
|
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 { ClientMessage, WorkingInitMessage, ServerMessage, NewSessionMessage, WriteToSessionMessage } from "../proto";
|
||||||
import { evaluate } from "./evaluate";
|
import { evaluate } from "./evaluate";
|
||||||
import { ReadWriteConnection } from "../common/connection";
|
import { ReadWriteConnection } from "../common/connection";
|
||||||
|
@ -93,42 +93,50 @@ export class Server {
|
||||||
|
|
||||||
private handleMessage(message: ClientMessage): void {
|
private handleMessage(message: ClientMessage): void {
|
||||||
if (message.hasNewEval()) {
|
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()) {
|
} else if (message.hasNewSession()) {
|
||||||
const sessionMessage = message.getNewSession()!;
|
const sessionMessage = message.getNewSession()!;
|
||||||
const childLogger = this.getChildLogger(sessionMessage.getCommand());
|
logger.debug("NewSession", field("id", sessionMessage.getId()));
|
||||||
childLogger.debug(sessionMessage.getIsFork() ? "Forking" : "Spawning", field("args", sessionMessage.getArgsList()));
|
|
||||||
const session = handleNewSession(this.connection, sessionMessage, this.options, () => {
|
const session = handleNewSession(this.connection, sessionMessage, this.options, () => {
|
||||||
childLogger.debug("Exited");
|
|
||||||
this.sessions.delete(sessionMessage.getId());
|
this.sessions.delete(sessionMessage.getId());
|
||||||
});
|
});
|
||||||
this.sessions.set(message.getNewSession()!.getId(), session);
|
this.sessions.set(sessionMessage.getId(), session);
|
||||||
} else if (message.hasCloseSessionInput()) {
|
} 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) {
|
if (!s || !s.stdin) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
s.stdin.end();
|
s.stdin.end();
|
||||||
} else if (message.hasResizeSessionTty()) {
|
} 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) {
|
if (!s || !s.resize) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const tty = message.getResizeSessionTty()!.getTtyDimensions()!;
|
const tty = resizeSessionTtyMessage.getTtyDimensions()!;
|
||||||
s.resize(tty.getWidth(), tty.getHeight());
|
s.resize(tty.getWidth(), tty.getHeight());
|
||||||
} else if (message.hasShutdownSession()) {
|
} 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) {
|
if (!s) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
s.kill(message.getShutdownSession()!.getSignal());
|
s.kill(shutdownSessionMessage.getSignal());
|
||||||
} else if (message.hasWriteToSession()) {
|
} 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) {
|
if (!s) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const data = new TextDecoder().decode(message.getWriteToSession()!.getData_asU8());
|
const data = new TextDecoder().decode(writeToSessionMessage.getData_asU8());
|
||||||
const source = message.getWriteToSession()!.getSource();
|
const source = writeToSessionMessage.getSource();
|
||||||
if (source === WriteToSessionMessage.Source.IPC) {
|
if (source === WriteToSessionMessage.Source.IPC) {
|
||||||
if (!s.stdio || !s.stdio[3]) {
|
if (!s.stdio || !s.stdio[3]) {
|
||||||
throw new Error("Cannot send message via IPC to process without IPC");
|
throw new Error("Cannot send message via IPC to process without IPC");
|
||||||
|
@ -139,48 +147,57 @@ export class Server {
|
||||||
}
|
}
|
||||||
} else if (message.hasNewConnection()) {
|
} else if (message.hasNewConnection()) {
|
||||||
const connectionMessage = message.getNewConnection()!;
|
const connectionMessage = message.getNewConnection()!;
|
||||||
const name = connectionMessage.getPath() || `${connectionMessage.getPort()}`;
|
logger.debug("NewConnection", field("id", connectionMessage.getId()));
|
||||||
const childLogger = this.getChildLogger(name, ">");
|
if (this.connections.has(connectionMessage.getId())) {
|
||||||
childLogger.debug("Connecting", field("path", connectionMessage.getPath()), field("port", connectionMessage.getPort()));
|
throw new Error(`connect EISCONN ${connectionMessage.getPath() || connectionMessage.getPort()}`);
|
||||||
|
}
|
||||||
const socket = handleNewConnection(this.connection, connectionMessage, () => {
|
const socket = handleNewConnection(this.connection, connectionMessage, () => {
|
||||||
childLogger.debug("Disconnected");
|
|
||||||
this.connections.delete(connectionMessage.getId());
|
this.connections.delete(connectionMessage.getId());
|
||||||
});
|
});
|
||||||
this.connections.set(connectionMessage.getId(), socket);
|
this.connections.set(connectionMessage.getId(), socket);
|
||||||
} else if (message.hasConnectionOutput()) {
|
} 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) {
|
if (!c) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
c.write(Buffer.from(message.getConnectionOutput()!.getData_asU8()));
|
c.write(Buffer.from(connectionOutputMessage.getData_asU8()));
|
||||||
} else if (message.hasConnectionClose()) {
|
} 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) {
|
if (!c) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
c.end();
|
c.end();
|
||||||
} else if (message.hasNewServer()) {
|
} else if (message.hasNewServer()) {
|
||||||
const serverMessage = message.getNewServer()!;
|
const serverMessage = message.getNewServer()!;
|
||||||
const name = serverMessage.getPath() || `${serverMessage.getPort()}`;
|
logger.debug("NewServer", field("id", serverMessage.getId()));
|
||||||
const childLogger = this.getChildLogger(name);
|
if (this.servers.has(serverMessage.getId())) {
|
||||||
childLogger.debug("Listening", field("path", serverMessage.getPath()), field("port", serverMessage.getPort()));
|
throw new Error("multiple listeners not supported");
|
||||||
|
}
|
||||||
const s = handleNewServer(this.connection, serverMessage, (socket) => {
|
const s = handleNewServer(this.connection, serverMessage, (socket) => {
|
||||||
const id = this.connectionId--;
|
const id = this.connectionId--;
|
||||||
this.connections.set(id, socket);
|
this.connections.set(id, socket);
|
||||||
childLogger.debug("Got connection", field("id", id));
|
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
}, () => {
|
}, () => {
|
||||||
childLogger.debug("Stopped");
|
|
||||||
this.connections.delete(serverMessage.getId());
|
this.connections.delete(serverMessage.getId());
|
||||||
|
}, (id) => {
|
||||||
|
this.connections.delete(id);
|
||||||
});
|
});
|
||||||
this.servers.set(serverMessage.getId(), s);
|
this.servers.set(serverMessage.getId(), s);
|
||||||
} else if (message.hasServerClose()) {
|
} 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) {
|
if (!s) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
s.close();
|
s.close();
|
||||||
|
} else {
|
||||||
|
logger.debug("Received unknown message type");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -196,26 +213,4 @@ export class Server {
|
||||||
return this.sessions.get(id);
|
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 * as path from "path";
|
||||||
import { TextEncoder, TextDecoder } from "text-encoding";
|
import { TextEncoder, TextDecoder } from "text-encoding";
|
||||||
import { createClient } from "./helpers";
|
import { createClient } from "./helpers";
|
||||||
|
import { Net } from "../src/browser/modules/net";
|
||||||
|
|
||||||
(<any>global).TextDecoder = TextDecoder;
|
(global as any).TextDecoder = TextDecoder; // tslint:disable-line no-any
|
||||||
(<any>global).TextEncoder = TextEncoder;
|
(global as any).TextEncoder = TextEncoder; // tslint:disable-line no-any
|
||||||
|
|
||||||
describe("spawn", () => {
|
describe("spawn", () => {
|
||||||
const client = createClient({
|
const client = createClient({
|
||||||
dataDirectory: "",
|
dataDirectory: "",
|
||||||
workingDirectory: "",
|
workingDirectory: "",
|
||||||
forkProvider: (msg) => {
|
forkProvider: (msg): cp.ChildProcess => {
|
||||||
return cp.spawn(msg.getCommand(), msg.getArgsList(), {
|
return cp.spawn(msg.getCommand(), msg.getArgsList(), {
|
||||||
stdio: [null, null, null, "pipe"],
|
stdio: [null, null, null, "pipe"],
|
||||||
});
|
});
|
||||||
|
@ -24,7 +25,7 @@ describe("spawn", () => {
|
||||||
proc.stdout.on("data", (data) => {
|
proc.stdout.on("data", (data) => {
|
||||||
expect(data).toEqual("test\n");
|
expect(data).toEqual("test\n");
|
||||||
});
|
});
|
||||||
proc.on("exit", (code) => {
|
proc.on("exit", (): void => {
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -41,6 +42,7 @@ describe("spawn", () => {
|
||||||
if (first) {
|
if (first) {
|
||||||
// First piece of data is a welcome msg. Second is the prompt
|
// First piece of data is a welcome msg. Second is the prompt
|
||||||
first = false;
|
first = false;
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
expect(data.toString().endsWith("$ ")).toBeTruthy();
|
expect(data.toString().endsWith("$ ")).toBeTruthy();
|
||||||
|
@ -92,6 +94,7 @@ describe("spawn", () => {
|
||||||
|
|
||||||
if (output === 2) {
|
if (output === 2) {
|
||||||
proc.send("tput lines\n");
|
proc.send("tput lines\n");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,6 +109,7 @@ describe("spawn", () => {
|
||||||
columns: 10,
|
columns: 10,
|
||||||
rows: 50,
|
rows: 50,
|
||||||
});
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -116,6 +120,7 @@ describe("spawn", () => {
|
||||||
|
|
||||||
if (output === 6) {
|
if (output === 6) {
|
||||||
proc.send("tput lines\n");
|
proc.send("tput lines\n");
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,7 +154,7 @@ describe("createConnection", () => {
|
||||||
const tmpPath = path.join(os.tmpdir(), Math.random().toString());
|
const tmpPath = path.join(os.tmpdir(), Math.random().toString());
|
||||||
let server: net.Server;
|
let server: net.Server;
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await new Promise((r) => {
|
await new Promise((r): void => {
|
||||||
server = net.createServer().listen(tmpPath, () => {
|
server = net.createServer().listen(tmpPath, () => {
|
||||||
r();
|
r();
|
||||||
});
|
});
|
||||||
|
@ -160,11 +165,23 @@ describe("createConnection", () => {
|
||||||
server.close();
|
server.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should connect to socket", (done) => {
|
it("should connect to socket", async () => {
|
||||||
const socket = client.createConnection(tmpPath, () => {
|
await new Promise((resolve): void => {
|
||||||
socket.end();
|
const socket = client.createConnection(tmpPath, () => {
|
||||||
socket.addListener("close", () => {
|
socket.end();
|
||||||
done();
|
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();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
"build:nexe": "node scripts/nexe.js",
|
"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: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: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"
|
"build": "npm run build:bootstrap-fork && npm run build:webpack && npm run build:nexe"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -17,7 +18,7 @@
|
||||||
"@oclif/plugin-help": "^2.1.4",
|
"@oclif/plugin-help": "^2.1.4",
|
||||||
"express": "^4.16.4",
|
"express": "^4.16.4",
|
||||||
"nexe": "^2.0.0-rc.34",
|
"nexe": "^2.0.0-rc.34",
|
||||||
"node-pty": "^0.8.0",
|
"node-pty": "^0.8.1",
|
||||||
"spdlog": "^0.7.2",
|
"spdlog": "^0.7.2",
|
||||||
"ws": "^6.1.2"
|
"ws": "^6.1.2"
|
||||||
},
|
},
|
||||||
|
|
|
@ -24,6 +24,7 @@ export class Entry extends Command {
|
||||||
|
|
||||||
// Dev flags
|
// Dev flags
|
||||||
"bootstrap-fork": flags.string({ hidden: true }),
|
"bootstrap-fork": flags.string({ hidden: true }),
|
||||||
|
env: flags.string({ hidden: true }),
|
||||||
};
|
};
|
||||||
public static args = [{
|
public static args = [{
|
||||||
name: "workdir",
|
name: "workdir",
|
||||||
|
@ -50,6 +51,10 @@ export class Entry extends Command {
|
||||||
|
|
||||||
const { args, flags } = this.parse(Entry);
|
const { args, flags } = this.parse(Entry);
|
||||||
|
|
||||||
|
if (flags.env) {
|
||||||
|
Object.assign(process.env, JSON.parse(flags.env));
|
||||||
|
}
|
||||||
|
|
||||||
if (flags["bootstrap-fork"]) {
|
if (flags["bootstrap-fork"]) {
|
||||||
const modulePath = flags["bootstrap-fork"];
|
const modulePath = flags["bootstrap-fork"];
|
||||||
if (!modulePath) {
|
if (!modulePath) {
|
||||||
|
@ -95,7 +100,7 @@ export class Entry extends Command {
|
||||||
|
|
||||||
next();
|
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 webpackConfig = require(path.join(__dirname, "..", "..", "web", "webpack.dev.config.js"));
|
||||||
const compiler = require("webpack")(webpackConfig);
|
const compiler = require("webpack")(webpackConfig);
|
||||||
app.use(require("webpack-dev-middleware")(compiler, {
|
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 { ReadWriteConnection } from "@coder/protocol";
|
||||||
import { Server, ServerOptions } from "@coder/protocol/src/node/server";
|
import { Server, ServerOptions } from "@coder/protocol/src/node/server";
|
||||||
import { NewSessionMessage } from '@coder/protocol/src/proto';
|
import { NewSessionMessage } from '@coder/protocol/src/proto';
|
||||||
import { ChildProcess } from "child_process";
|
import { ChildProcess } from "child_process";
|
||||||
import * as express from "express";
|
import * as express from "express";
|
||||||
import * as http from "http";
|
import * as http from "http";
|
||||||
|
import * as path from "path";
|
||||||
import * as ws from "ws";
|
import * as ws from "ws";
|
||||||
import { forkModule } from "./vscode/bootstrapFork";
|
import { forkModule } from "./vscode/bootstrapFork";
|
||||||
|
|
||||||
|
@ -46,7 +47,11 @@ export const createApp = (registerMiddleware?: (app: express.Application) => voi
|
||||||
forkProvider: (message: NewSessionMessage): ChildProcess => {
|
forkProvider: (message: NewSessionMessage): ChildProcess => {
|
||||||
let proc: ChildProcess;
|
let proc: ChildProcess;
|
||||||
if (message.getIsBootstrapFork()) {
|
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 {
|
} else {
|
||||||
throw new Error("No support for non bootstrap-forking yet");
|
throw new Error("No support for non bootstrap-forking yet");
|
||||||
}
|
}
|
||||||
|
@ -56,14 +61,7 @@ export const createApp = (registerMiddleware?: (app: express.Application) => voi
|
||||||
} : undefined);
|
} : undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
app.use(express.static(path.join(__dirname, "../build/web")));
|
||||||
* We should static-serve the `web` package at this point.
|
|
||||||
*/
|
|
||||||
app.get("/", (req, res, next) => {
|
|
||||||
res.write("Example! :)");
|
|
||||||
res.status(200);
|
|
||||||
res.end();
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
express: app,
|
express: app,
|
||||||
|
|
|
@ -5,7 +5,9 @@ import * as path from "path";
|
||||||
|
|
||||||
export const requireModule = (modulePath: string): void => {
|
export const requireModule = (modulePath: string): void => {
|
||||||
process.env.AMD_ENTRYPOINT = modulePath;
|
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) {
|
if (!process.send) {
|
||||||
const socket = new net.Socket({ fd: 3 });
|
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")));
|
* cp.stderr.on("data", (data) => console.log(data.toString("utf8")));
|
||||||
* @param modulePath Path of the VS Code module to load.
|
* @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;
|
let proc: cp.ChildProcess | undefined;
|
||||||
|
|
||||||
const args = ["--bootstrap-fork", modulePath];
|
const args = ["--bootstrap-fork", modulePath];
|
||||||
|
if (env) {
|
||||||
|
args.push("--env", JSON.stringify(env));
|
||||||
|
}
|
||||||
const options: cp.SpawnOptions = {
|
const options: cp.SpawnOptions = {
|
||||||
stdio: [null, null, null, "pipe"],
|
stdio: [null, null, null, "pipe"],
|
||||||
};
|
};
|
||||||
|
|
|
@ -66,7 +66,9 @@ export class SharedProcess {
|
||||||
state: SharedProcessState.Starting,
|
state: SharedProcessState.Starting,
|
||||||
});
|
});
|
||||||
let resolved: boolean = false;
|
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) => {
|
this.activeProcess.on("exit", (err) => {
|
||||||
if (this._state !== SharedProcessState.Stopped) {
|
if (this._state !== SharedProcessState.Stopped) {
|
||||||
this.setState({
|
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"
|
resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab"
|
||||||
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
|
integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=
|
||||||
|
|
||||||
nan@2.10.0:
|
nan@2.12.1, nan@^2.8.0, nan@^2.9.2:
|
||||||
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:
|
|
||||||
version "2.12.1"
|
version "2.12.1"
|
||||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552"
|
resolved "https://registry.yarnpkg.com/nan/-/nan-2.12.1.tgz#7b1aa193e9aa86057e3c7bbd0ac448e770925552"
|
||||||
integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==
|
integrity sha512-JY7V6lRkStKcKTvHO5NVSQRv+RV+FIL5pvDoLiAtSL9pKlC5x9PKQcZDsq7m4FO4d57mkhC6Z+QhAh3Jdk5JFw==
|
||||||
|
@ -2396,12 +2391,12 @@ node-pre-gyp@^0.10.0:
|
||||||
semver "^5.3.0"
|
semver "^5.3.0"
|
||||||
tar "^4"
|
tar "^4"
|
||||||
|
|
||||||
node-pty@^0.8.0:
|
node-pty@^0.8.1:
|
||||||
version "0.8.0"
|
version "0.8.1"
|
||||||
resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.8.0.tgz#08bccb633f49e2e3f7245eb56ea6b40f37ccd64f"
|
resolved "https://registry.yarnpkg.com/node-pty/-/node-pty-0.8.1.tgz#94b457bec013e7a09b8d9141f63b0787fa25c23f"
|
||||||
integrity sha512-g5ggk3gN4gLrDmAllee5ScFyX3YzpOC/U8VJafha4pE7do0TIE1voiIxEbHSRUOPD1xYqmY+uHhOKAd3avbxGQ==
|
integrity sha512-j+/g0Q5dR+vkELclpJpz32HcS3O/3EdPSGPvDXJZVJQLCvgG0toEbfmymxAEyQyZEpaoKHAcoL+PvKM+4N9nlw==
|
||||||
dependencies:
|
dependencies:
|
||||||
nan "2.10.0"
|
nan "2.12.1"
|
||||||
|
|
||||||
nopt@^4.0.1:
|
nopt@^4.0.1:
|
||||||
version "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));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -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",
|
nameLong: "vscode online",
|
||||||
dataFolderName: ".vscode-online",
|
dataFolderName: ".vscode-online",
|
||||||
extensionsGallery: {
|
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: {
|
extensionExecutionEnvironments: {
|
||||||
"wayou.vscode-todo-highlight": "worker",
|
"wayou.vscode-todo-highlight": "worker",
|
||||||
|
|
|
@ -51,16 +51,31 @@ class WindowsService implements IWindowsService {
|
||||||
throw new Error("not implemented");
|
throw new Error("not implemented");
|
||||||
}
|
}
|
||||||
|
|
||||||
public showMessageBox(_windowId: number, _options: MessageBoxOptions): Promise<IMessageBoxResult> {
|
public showMessageBox(windowId: number, options: MessageBoxOptions): Promise<IMessageBoxResult> {
|
||||||
throw new Error("not implemented");
|
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> {
|
public showSaveDialog(windowId: number, options: SaveDialogOptions): Promise<string> {
|
||||||
throw new Error("not implemented");
|
return new Promise((resolve): void => {
|
||||||
|
electron.dialog.showSaveDialog(this.getWindowById(windowId), options, (filename, _bookmark) => {
|
||||||
|
resolve(filename);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public showOpenDialog(_windowId: number, _options: OpenDialogOptions): Promise<string[]> {
|
public showOpenDialog(windowId: number, options: OpenDialogOptions): Promise<string[]> {
|
||||||
throw new Error("not implemented");
|
return new Promise((resolve): void => {
|
||||||
|
electron.dialog.showOpenDialog(this.getWindowById(windowId), options, (filePaths, _bookmarks) => {
|
||||||
|
resolve(filePaths);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public reloadWindow(windowId: number, _args?: ParsedArgs): Promise<void> {
|
public reloadWindow(windowId: number, _args?: ParsedArgs): Promise<void> {
|
||||||
|
|
|
@ -64,6 +64,7 @@ module.exports = (env) => {
|
||||||
"windows-process-tree": path.resolve(fills, "empty.ts"),
|
"windows-process-tree": path.resolve(fills, "empty.ts"),
|
||||||
|
|
||||||
"electron": path.join(vscodeFills, "stdioElectron.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/product": path.resolve(vscodeFills, "product.ts"),
|
||||||
"vs/platform/node/package": path.resolve(vscodeFills, "package.ts"),
|
"vs/platform/node/package": path.resolve(vscodeFills, "package.ts"),
|
||||||
"vs/base/node/paths": path.resolve(vscodeFills, "paths.ts"),
|
"vs/base/node/paths": path.resolve(vscodeFills, "paths.ts"),
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
out
|
|
@ -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"),
|
"vscode-sqlite3": path.join(fills, "empty.ts"),
|
||||||
"tls": path.join(fills, "empty.ts"),
|
"tls": path.join(fills, "empty.ts"),
|
||||||
"native-is-elevated": 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"),
|
"dns": path.join(fills, "empty.ts"),
|
||||||
"console": path.join(fills, "empty.ts"),
|
"console": path.join(fills, "empty.ts"),
|
||||||
"readline": 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"),
|
"node-pty": path.join(vsFills, "node-pty.ts"),
|
||||||
"graceful-fs": path.join(vsFills, "graceful-fs.ts"),
|
"graceful-fs": path.join(vsFills, "graceful-fs.ts"),
|
||||||
"spdlog": path.join(vsFills, "spdlog.ts"),
|
"spdlog": path.join(vsFills, "spdlog.ts"),
|
||||||
|
"native-watchdog": path.join(vsFills, "native-watchdog.ts"),
|
||||||
"iconv-lite": path.join(vsFills, "iconv-lite.ts"),
|
"iconv-lite": path.join(vsFills, "iconv-lite.ts"),
|
||||||
|
|
||||||
"vs/base/node/paths": path.join(vsFills, "paths.ts"),
|
"vs/base/node/paths": path.join(vsFills, "paths.ts"),
|
||||||
|
|
|
@ -1,8 +1,12 @@
|
||||||
|
const path = require("path");
|
||||||
const webpack = require("webpack");
|
const webpack = require("webpack");
|
||||||
const merge = require("webpack-merge");
|
const merge = require("webpack-merge");
|
||||||
|
|
||||||
module.exports = merge(require("./webpack.common.config.js"), {
|
module.exports = merge(require("./webpack.common.config.js"), {
|
||||||
devtool: "cheap-module-eval-source-map",
|
devtool: "cheap-module-eval-source-map",
|
||||||
|
output: {
|
||||||
|
path: path.join(__dirname, "out"),
|
||||||
|
},
|
||||||
entry: [
|
entry: [
|
||||||
"webpack-hot-middleware/client?reload=true&quiet=true",
|
"webpack-hot-middleware/client?reload=true&quiet=true",
|
||||||
"./packages/web/src/index.ts"
|
"./packages/web/src/index.ts"
|
||||||
|
|
Loading…
Reference in New Issue