2019-01-13 02:44:29 +07:00
|
|
|
import * as cp from "child_process";
|
2019-01-19 04:46:40 +07:00
|
|
|
import * as net from "net";
|
2019-01-13 02:44:29 +07:00
|
|
|
import * as nodePty from "node-pty";
|
|
|
|
import * as stream from "stream";
|
|
|
|
import { TextEncoder } from "text-encoding";
|
2019-01-26 07:18:21 +07:00
|
|
|
import { Logger, logger, field } from "@coder/logger";
|
2019-01-24 07:00:38 +07:00
|
|
|
import { NewSessionMessage, ServerMessage, SessionDoneMessage, SessionOutputMessage, IdentifySessionMessage, NewConnectionMessage, ConnectionEstablishedMessage, NewConnectionFailureMessage, ConnectionCloseMessage, ConnectionOutputMessage, NewServerMessage, ServerEstablishedMessage, NewServerFailureMessage, ServerCloseMessage, ServerConnectionEstablishedMessage } from "../proto";
|
2019-01-13 02:44:29 +07:00
|
|
|
import { SendableConnection } from "../common/connection";
|
2019-01-19 06:08:44 +07:00
|
|
|
import { ServerOptions } from "./server";
|
2019-01-13 02:44:29 +07:00
|
|
|
|
|
|
|
export interface Process {
|
2019-01-24 00:52:58 +07:00
|
|
|
stdio?: Array<stream.Readable | stream.Writable>;
|
2019-01-13 02:44:29 +07:00
|
|
|
stdin?: stream.Writable;
|
|
|
|
stdout?: stream.Readable;
|
|
|
|
stderr?: stream.Readable;
|
2019-01-30 07:23:30 +07:00
|
|
|
send?: (message: string) => void;
|
2019-01-13 02:44:29 +07:00
|
|
|
|
|
|
|
pid: number;
|
|
|
|
killed?: boolean;
|
|
|
|
|
2019-01-30 07:23:30 +07:00
|
|
|
on(event: "data" | "message", cb: (data: string) => void): void;
|
2019-01-23 07:28:54 +07:00
|
|
|
on(event: "exit", listener: (exitCode: number, signal?: number) => void): void;
|
2019-01-13 02:44:29 +07:00
|
|
|
write(data: string | Uint8Array): void;
|
|
|
|
resize?(cols: number, rows: number): void;
|
|
|
|
kill(signal?: string): void;
|
|
|
|
title?: number;
|
|
|
|
}
|
|
|
|
|
2019-01-19 06:08:44 +07:00
|
|
|
export const handleNewSession = (connection: SendableConnection, newSession: NewSessionMessage, serverOptions: ServerOptions | undefined, onExit: () => void): Process => {
|
2019-01-26 07:18:21 +07:00
|
|
|
const childLogger = getChildLogger(newSession.getCommand());
|
|
|
|
childLogger.debug(() => [
|
|
|
|
newSession.getIsFork() ? "Forking" : "Spawning",
|
|
|
|
field("command", newSession.getCommand()),
|
|
|
|
field("args", newSession.getArgsList()),
|
|
|
|
field("env", newSession.getEnvMap().toObject()),
|
|
|
|
]);
|
|
|
|
|
2019-01-13 02:44:29 +07:00
|
|
|
let process: Process;
|
2019-01-29 00:14:06 +07:00
|
|
|
let processTitle: string | undefined;
|
2019-01-13 02:44:29 +07:00
|
|
|
|
2019-01-26 07:18:21 +07:00
|
|
|
const env: { [key: string]: string } = {};
|
|
|
|
newSession.getEnvMap().forEach((value, key) => {
|
2019-01-13 02:44:29 +07:00
|
|
|
env[key] = value;
|
|
|
|
});
|
|
|
|
if (newSession.getTtyDimensions()) {
|
|
|
|
// Spawn with node-pty
|
2019-01-29 00:14:06 +07:00
|
|
|
const ptyProc = nodePty.spawn(newSession.getCommand(), newSession.getArgsList(), {
|
2019-01-13 02:44:29 +07:00
|
|
|
cols: newSession.getTtyDimensions()!.getWidth(),
|
|
|
|
rows: newSession.getTtyDimensions()!.getHeight(),
|
|
|
|
cwd: newSession.getCwd(),
|
|
|
|
env,
|
|
|
|
});
|
2019-01-29 00:14:06 +07:00
|
|
|
|
|
|
|
const timer = setInterval(() => {
|
|
|
|
if (ptyProc.process !== processTitle) {
|
|
|
|
processTitle = ptyProc.process;
|
|
|
|
const id = new IdentifySessionMessage();
|
|
|
|
id.setId(newSession.getId());
|
|
|
|
id.setTitle(processTitle);
|
|
|
|
const sm = new ServerMessage();
|
|
|
|
sm.setIdentifySession(id);
|
|
|
|
connection.send(sm.serializeBinary());
|
|
|
|
}
|
|
|
|
}, 200);
|
2019-01-30 07:23:30 +07:00
|
|
|
|
2019-01-29 00:14:06 +07:00
|
|
|
ptyProc.on("exit", () => {
|
|
|
|
clearTimeout(timer);
|
|
|
|
});
|
|
|
|
|
|
|
|
process = ptyProc;
|
|
|
|
processTitle = ptyProc.process;
|
2019-01-13 02:44:29 +07:00
|
|
|
} else {
|
|
|
|
const options = {
|
|
|
|
cwd: newSession.getCwd(),
|
|
|
|
env,
|
|
|
|
};
|
|
|
|
let proc: cp.ChildProcess;
|
|
|
|
if (newSession.getIsFork()) {
|
2019-01-19 06:08:44 +07:00
|
|
|
if (!serverOptions) {
|
|
|
|
throw new Error("No forkProvider set for bootstrap-fork request");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!serverOptions.forkProvider) {
|
|
|
|
throw new Error("No forkProvider set for server options");
|
|
|
|
}
|
|
|
|
|
|
|
|
proc = serverOptions.forkProvider(newSession);
|
2019-01-13 02:44:29 +07:00
|
|
|
} else {
|
|
|
|
proc = cp.spawn(newSession.getCommand(), newSession.getArgsList(), options);
|
|
|
|
}
|
|
|
|
|
|
|
|
process = {
|
|
|
|
stdin: proc.stdin,
|
|
|
|
stderr: proc.stderr,
|
|
|
|
stdout: proc.stdout,
|
2019-01-24 02:43:20 +07:00
|
|
|
stdio: proc.stdio,
|
2019-01-30 07:23:30 +07:00
|
|
|
send: (message): void => {
|
|
|
|
proc.send(message);
|
|
|
|
},
|
2019-01-26 07:18:21 +07:00
|
|
|
on: (...args: any[]): void => ((proc as any).on)(...args), // tslint:disable-line no-any
|
|
|
|
write: (d): boolean => proc.stdin.write(d),
|
|
|
|
kill: (s): void => proc.kill(s || "SIGTERM"),
|
2019-01-13 02:44:29 +07:00
|
|
|
pid: proc.pid,
|
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2019-01-24 00:52:58 +07:00
|
|
|
const sendOutput = (_source: SessionOutputMessage.Source, msg: string | Uint8Array): void => {
|
2019-01-26 07:18:21 +07:00
|
|
|
childLogger.debug(() => {
|
|
|
|
|
|
|
|
let data = msg.toString();
|
|
|
|
if (_source === SessionOutputMessage.Source.IPC) {
|
2019-01-30 07:23:30 +07:00
|
|
|
// data = Buffer.from(msg.toString(), "base64").toString();
|
2019-01-26 07:18:21 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
return [
|
|
|
|
_source === SessionOutputMessage.Source.STDOUT
|
|
|
|
? "stdout"
|
|
|
|
: (_source === SessionOutputMessage.Source.STDERR ? "stderr" : "ipc"),
|
|
|
|
field("id", newSession.getId()),
|
|
|
|
field("data", data),
|
|
|
|
];
|
|
|
|
});
|
2019-01-13 02:44:29 +07:00
|
|
|
const serverMsg = new ServerMessage();
|
|
|
|
const d = new SessionOutputMessage();
|
|
|
|
d.setId(newSession.getId());
|
|
|
|
d.setData(typeof msg === "string" ? new TextEncoder().encode(msg) : msg);
|
2019-01-24 00:52:58 +07:00
|
|
|
d.setSource(_source);
|
2019-01-13 02:44:29 +07:00
|
|
|
serverMsg.setSessionOutput(d);
|
|
|
|
connection.send(serverMsg.serializeBinary());
|
|
|
|
};
|
|
|
|
|
|
|
|
if (process.stdout && process.stderr) {
|
|
|
|
process.stdout.on("data", (data) => {
|
2019-01-24 00:52:58 +07:00
|
|
|
sendOutput(SessionOutputMessage.Source.STDOUT, data);
|
2019-01-13 02:44:29 +07:00
|
|
|
});
|
|
|
|
|
|
|
|
process.stderr.on("data", (data) => {
|
2019-01-24 00:52:58 +07:00
|
|
|
sendOutput(SessionOutputMessage.Source.STDERR, data);
|
2019-01-13 02:44:29 +07:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
process.on("data", (data) => {
|
2019-01-24 00:52:58 +07:00
|
|
|
sendOutput(SessionOutputMessage.Source.STDOUT, Buffer.from(data));
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-01-30 07:23:30 +07:00
|
|
|
// IPC.
|
|
|
|
if (process.send) {
|
|
|
|
process.on("message", (data) => {
|
|
|
|
sendOutput(SessionOutputMessage.Source.IPC, JSON.stringify(data));
|
2019-01-13 02:44:29 +07:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
const id = new IdentifySessionMessage();
|
|
|
|
id.setId(newSession.getId());
|
|
|
|
id.setPid(process.pid);
|
2019-01-29 00:14:06 +07:00
|
|
|
if (processTitle) {
|
|
|
|
id.setTitle(processTitle);
|
|
|
|
}
|
2019-01-13 02:44:29 +07:00
|
|
|
const sm = new ServerMessage();
|
|
|
|
sm.setIdentifySession(id);
|
|
|
|
connection.send(sm.serializeBinary());
|
|
|
|
|
2019-01-19 04:46:40 +07:00
|
|
|
process.on("exit", (code) => {
|
2019-01-26 07:18:21 +07:00
|
|
|
childLogger.debug("Exited", field("id", newSession.getId()));
|
2019-01-13 02:44:29 +07:00
|
|
|
const serverMsg = new ServerMessage();
|
|
|
|
const exit = new SessionDoneMessage();
|
|
|
|
exit.setId(newSession.getId());
|
|
|
|
exit.setExitStatus(code);
|
|
|
|
serverMsg.setSessionDone(exit);
|
|
|
|
connection.send(serverMsg.serializeBinary());
|
|
|
|
|
|
|
|
onExit();
|
|
|
|
});
|
|
|
|
|
|
|
|
return process;
|
|
|
|
};
|
2019-01-19 04:46:40 +07:00
|
|
|
|
|
|
|
export const handleNewConnection = (connection: SendableConnection, newConnection: NewConnectionMessage, onExit: () => void): net.Socket => {
|
2019-01-26 07:18:21 +07:00
|
|
|
const target = newConnection.getPath() || `${newConnection.getPort()}`;
|
|
|
|
const childLogger = getChildLogger(target, ">");
|
|
|
|
|
2019-01-19 04:46:40 +07:00
|
|
|
const id = newConnection.getId();
|
2019-01-19 06:08:44 +07:00
|
|
|
let socket: net.Socket;
|
2019-01-19 04:46:40 +07:00
|
|
|
let didConnect = false;
|
2019-01-26 07:18:21 +07:00
|
|
|
const connectCallback = (): void => {
|
|
|
|
childLogger.debug("Connected", field("id", newConnection.getId()), field("target", target));
|
2019-01-19 04:46:40 +07:00
|
|
|
didConnect = true;
|
|
|
|
const estab = new ConnectionEstablishedMessage();
|
|
|
|
estab.setId(id);
|
|
|
|
const servMsg = new ServerMessage();
|
|
|
|
servMsg.setConnectionEstablished(estab);
|
|
|
|
connection.send(servMsg.serializeBinary());
|
|
|
|
};
|
|
|
|
|
|
|
|
if (newConnection.getPath()) {
|
|
|
|
socket = net.createConnection(newConnection.getPath(), connectCallback);
|
|
|
|
} else if (newConnection.getPort()) {
|
|
|
|
socket = net.createConnection(newConnection.getPort(), undefined, connectCallback);
|
|
|
|
} else {
|
|
|
|
throw new Error("No path or port provided for new connection");
|
|
|
|
}
|
|
|
|
|
|
|
|
socket.addListener("error", (err) => {
|
2019-01-26 07:18:21 +07:00
|
|
|
childLogger.debug("Error", field("id", newConnection.getId()), field("error", err));
|
2019-01-19 04:46:40 +07:00
|
|
|
if (!didConnect) {
|
|
|
|
const errMsg = new NewConnectionFailureMessage();
|
|
|
|
errMsg.setId(id);
|
|
|
|
errMsg.setMessage(err.message);
|
|
|
|
const servMsg = new ServerMessage();
|
|
|
|
servMsg.setConnectionFailure(errMsg);
|
|
|
|
connection.send(servMsg.serializeBinary());
|
2019-01-19 06:08:44 +07:00
|
|
|
|
2019-01-19 04:46:40 +07:00
|
|
|
onExit();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
socket.addListener("close", () => {
|
2019-01-26 07:18:21 +07:00
|
|
|
childLogger.debug("Closed", field("id", newConnection.getId()));
|
2019-01-19 04:46:40 +07:00
|
|
|
if (didConnect) {
|
|
|
|
const closed = new ConnectionCloseMessage();
|
|
|
|
closed.setId(id);
|
|
|
|
const servMsg = new ServerMessage();
|
|
|
|
servMsg.setConnectionClose(closed);
|
|
|
|
connection.send(servMsg.serializeBinary());
|
|
|
|
|
|
|
|
onExit();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
socket.addListener("data", (data) => {
|
2019-01-26 07:18:21 +07:00
|
|
|
childLogger.debug(() => [
|
|
|
|
"ipc",
|
|
|
|
field("id", newConnection.getId()),
|
|
|
|
field("data", data),
|
|
|
|
]);
|
2019-01-19 04:46:40 +07:00
|
|
|
const dataMsg = new ConnectionOutputMessage();
|
|
|
|
dataMsg.setId(id);
|
|
|
|
dataMsg.setData(data);
|
|
|
|
const servMsg = new ServerMessage();
|
|
|
|
servMsg.setConnectionOutput(dataMsg);
|
|
|
|
connection.send(servMsg.serializeBinary());
|
|
|
|
});
|
|
|
|
|
|
|
|
return socket;
|
2019-01-23 07:28:54 +07:00
|
|
|
};
|
2019-01-24 07:00:38 +07:00
|
|
|
|
2019-01-26 07:18:21 +07:00
|
|
|
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, "|");
|
|
|
|
|
2019-01-24 07:00:38 +07:00
|
|
|
const s = net.createServer();
|
|
|
|
|
|
|
|
try {
|
|
|
|
s.listen(newServer.getPath() ? newServer.getPath() : newServer.getPort(), () => {
|
2019-01-26 07:18:21 +07:00
|
|
|
childLogger.debug("Listening", field("id", newServer.getId()), field("target", target));
|
2019-01-24 07:00:38 +07:00
|
|
|
const se = new ServerEstablishedMessage();
|
|
|
|
se.setId(newServer.getId());
|
|
|
|
const sm = new ServerMessage();
|
|
|
|
sm.setServerEstablished(se);
|
|
|
|
connection.send(sm.serializeBinary());
|
|
|
|
});
|
|
|
|
} catch (ex) {
|
2019-01-26 07:18:21 +07:00
|
|
|
childLogger.debug("Failed to listen", field("id", newServer.getId()), field("target", target));
|
2019-01-24 07:00:38 +07:00
|
|
|
const sf = new NewServerFailureMessage();
|
|
|
|
sf.setId(newServer.getId());
|
|
|
|
const sm = new ServerMessage();
|
|
|
|
sm.setServerFailure(sf);
|
|
|
|
connection.send(sm.serializeBinary());
|
2019-01-25 04:11:50 +07:00
|
|
|
|
2019-01-24 07:00:38 +07:00
|
|
|
onExit();
|
|
|
|
}
|
|
|
|
|
|
|
|
s.on("close", () => {
|
2019-01-26 07:18:21 +07:00
|
|
|
childLogger.debug("Stopped listening", field("id", newServer.getId()), field("target", target));
|
2019-01-24 07:00:38 +07:00
|
|
|
const sc = new ServerCloseMessage();
|
|
|
|
sc.setId(newServer.getId());
|
|
|
|
const sm = new ServerMessage();
|
|
|
|
sm.setServerClose(sc);
|
|
|
|
connection.send(sm.serializeBinary());
|
|
|
|
|
|
|
|
onExit();
|
|
|
|
});
|
|
|
|
|
|
|
|
s.on("connection", (socket) => {
|
|
|
|
const socketId = addSocket(socket);
|
2019-01-26 07:18:21 +07:00
|
|
|
childLogger.debug("Got connection", field("id", newServer.getId()), field("socketId", socketId));
|
2019-01-24 07:00:38 +07:00
|
|
|
|
|
|
|
const sock = new ServerConnectionEstablishedMessage();
|
|
|
|
sock.setServerId(newServer.getId());
|
|
|
|
sock.setConnectionId(socketId);
|
|
|
|
const sm = new ServerMessage();
|
|
|
|
sm.setServerConnectionEstablished(sock);
|
|
|
|
connection.send(sm.serializeBinary());
|
2019-01-26 07:18:21 +07:00
|
|
|
|
|
|
|
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);
|
|
|
|
});
|
2019-01-24 07:00:38 +07:00
|
|
|
});
|
|
|
|
|
|
|
|
return s;
|
|
|
|
};
|
2019-01-26 07:18:21 +07:00
|
|
|
|
|
|
|
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);
|
|
|
|
};
|