code-server/packages/server/src/cli.ts

267 lines
8.9 KiB
TypeScript
Raw Normal View History

2019-01-31 04:40:01 +07:00
import { field, logger } from "@coder/logger";
import { ServerMessage, SharedProcessActiveMessage } from "@coder/protocol/src/proto";
2019-03-19 23:53:05 +07:00
import { ChildProcess, fork, ForkOptions, spawn } from "child_process";
import { randomFillSync } from "crypto";
import * as fs from "fs";
2019-03-19 23:53:05 +07:00
import * as fse from "fs-extra";
import * as os from "os";
import * as path from "path";
import * as WebSocket from "ws";
2019-03-19 23:53:05 +07:00
import { buildDir, cacheHome, dataHome, isCli, serveStatic } from "./constants";
import { setup as setupNativeModules } from "./modules";
import { createApp } from "./server";
2019-03-19 23:53:05 +07:00
import { forkModule, requireFork, requireModule } from "./vscode/bootstrapFork";
import { SharedProcess, SharedProcessState } from "./vscode/sharedProcess";
import opn = require("opn");
2019-03-27 03:21:03 +07:00
import * as commander from "commander";
commander.version(process.env.VERSION || "development")
.name("code-server")
.description("Run VS Code on a remote server.")
2019-03-27 20:56:05 +07:00
.option("--cert <value>")
.option("--cert-key <value>")
2019-03-27 03:21:03 +07:00
.option("-d, --data-dir <value>", "Customize where user-data is stored.")
.option("-h, --host <value>", "Customize the hostname.", "0.0.0.0")
.option("-o, --open", "Open in the browser on startup.", false)
.option("-p, --port <number>", "Port to bind on.", 8443)
.option("-N, --no-auth", "Start without requiring authentication.", undefined)
.option("-H, --allow-http", "Allow http connections.", false)
.option("-P, --password <value>", "Specify a password for authentication.")
.option("--bootstrap-fork <name>", "Used for development. Never set.")
.option("--fork <name>", "Used for development. Never set.")
.option("--extra-args <args>", "Used for development. Never set.")
.arguments("Specify working directory.")
.parse(process.argv);
Error.stackTraceLimit = Infinity;
if (isCli) {
require("nbin").shimNativeFs(buildDir);
}
(async (): Promise<void> => {
const args = commander.args;
const options = commander.opts() as {
noAuth: boolean;
readonly allowHttp: boolean;
readonly host: string;
readonly port: number;
readonly dataDir?: string;
readonly password?: string;
readonly open?: boolean;
readonly cert?: string;
readonly certKey?: string;
readonly bootstrapFork?: string;
readonly fork?: string;
readonly extraArgs?: string;
};
2019-03-27 03:21:03 +07:00
// Commander has an exception for `--no` prefixes. Here we'll adjust that.
// tslint:disable-next-line:no-any
const noAuthValue = (commander as any).auth;
options.noAuth = !noAuthValue;
2019-03-27 03:21:03 +07:00
const dataDir = path.resolve(options.dataDir || path.join(dataHome, "code-server"));
const workingDir = path.resolve(args[0] || process.cwd());
2019-03-27 03:21:03 +07:00
if (!fs.existsSync(dataDir)) {
const oldDataDir = path.resolve(path.join(os.homedir(), ".code-server"));
if (fs.existsSync(oldDataDir)) {
await fse.move(oldDataDir, dataDir);
logger.info(`Moved data directory from ${oldDataDir} to ${dataDir}`);
}
}
2019-03-27 03:21:03 +07:00
await Promise.all([
fse.mkdirp(cacheHome),
fse.mkdirp(dataDir),
fse.mkdirp(workingDir),
]);
setupNativeModules(dataDir);
const builtInExtensionsDir = path.resolve(buildDir || path.join(__dirname, ".."), "build/extensions");
if (options.bootstrapFork) {
const modulePath = options.bootstrapFork;
if (!modulePath) {
logger.error("No module path specified to fork!");
process.exit(1);
}
2019-03-27 03:21:03 +07:00
((options.extraArgs ? JSON.parse(options.extraArgs) : []) as string[]).forEach((arg, i) => {
// [0] contains the binary running the script (`node` for example) and
// [1] contains the script name, so the arguments come after that.
process.argv[i + 2] = arg;
});
2019-03-27 03:21:03 +07:00
return requireModule(modulePath, dataDir, builtInExtensionsDir);
}
Make everything use active evals (#30) * Add trace log level * Use active eval to implement spdlog * Split server/client active eval interfaces Since all properties are *not* valid on both sides * +200% fire resistance * Implement exec using active evaluations * Fully implement child process streams * Watch impl, move child_process back to explicitly adding events Automatically forwarding all events might be the right move, but wanna think/discuss it a bit more because it didn't come out very cleanly. * Would you like some args with that callback? * Implement the rest of child_process using active evals * Rampant memory leaks Emit "kill" to active evaluations when client disconnects in order to kill processes. Most likely won't be the final solution. * Resolve some minor issues with output panel * Implement node-pty with active evals * Provide clearTimeout to vm sandbox * Implement socket with active evals * Extract some callback logic Also remove some eval interfaces, need to re-think those. * Implement net.Server and remainder of net.Socket using active evals * Implement dispose for active evaluations * Use trace for express requests * Handle sending buffers through evaluation events * Make event logging a bit more clear * Fix some errors due to us not actually instantiating until connect/listen * is this a commit message? * We can just create the evaluator in the ctor Not sure what I was thinking. * memory leak for you, memory leak for everyone * it's a ternary now * Don't dispose automatically on close or error The code may or may not be disposable at that point. * Handle parsing buffers on the client side as well * Remove unused protobuf * Remove TypedValue * Remove unused forkProvider and test * Improve dispose pattern for active evals * Socket calls close after error; no need to bind both * Improve comment * Comment is no longer wishy washy due to explicit boolean * Simplify check for sendHandle and options * Replace _require with __non_webpack_require__ Webpack will then replace this with `require` which we then provide to the vm sandbox. * Provide path.parse * Prevent original-fs from loading * Start with a pid of -1 vscode immediately checks the PID to see if the debug process launch correctly, but of course we don't get the pid synchronously. * Pass arguments to bootstrap-fork * Fully implement streams Was causing errors because internally the stream would set this.writing to true and it would never become false, so subsequent messages would never send. * Fix serializing errors and streams emitting errors multiple times * Was emitting close to data * Fix missing path for spawned processes * Move evaluation onDispose call Now it's accurate and runs when the active evaluation has actually disposed. * Fix promisifying fs.exists * Fix some active eval callback issues * Patch existsSync in debug adapter
2019-02-19 23:17:03 +07:00
2019-03-27 03:21:03 +07:00
if (options.fork) {
const modulePath = options.fork;
return requireFork(modulePath, JSON.parse(options.extraArgs!), builtInExtensionsDir);
}
2019-03-27 03:21:03 +07:00
const logDir = path.join(cacheHome, "code-server/logs", new Date().toISOString().replace(/[-:.TZ]/g, ""));
process.env.VSCODE_LOGS = logDir;
2019-03-27 03:21:03 +07:00
const certPath = options.cert ? path.resolve(options.cert) : undefined;
const certKeyPath = options.certKey ? path.resolve(options.certKey) : undefined;
2019-03-27 03:21:03 +07:00
if (certPath && !certKeyPath) {
logger.error("'--cert-key' flag is required when specifying a certificate!");
process.exit(1);
}
2019-03-27 03:21:03 +07:00
if (!certPath && certKeyPath) {
logger.error("'--cert' flag is required when specifying certificate key!");
process.exit(1);
}
2019-03-27 03:21:03 +07:00
let certData: Buffer | undefined;
let certKeyData: Buffer | undefined;
if (typeof certPath !== "undefined" && typeof certKeyPath !== "undefined") {
try {
certData = fs.readFileSync(certPath);
} catch (ex) {
logger.error(`Failed to read certificate: ${ex.message}`);
process.exit(1);
}
2019-03-27 03:21:03 +07:00
try {
certKeyData = fs.readFileSync(certKeyPath);
} catch (ex) {
logger.error(`Failed to read certificate key: ${ex.message}`);
process.exit(1);
}
2019-03-27 03:21:03 +07:00
}
2019-03-27 03:21:03 +07:00
logger.info(`\u001B[1mcode-server ${process.env.VERSION ? `v${process.env.VERSION}` : "development"}`);
// TODO: fill in appropriate doc url
logger.info("Additional documentation: http://github.com/codercom/code-server");
logger.info("Initializing", field("data-dir", dataDir), field("working-dir", workingDir), field("log-dir", logDir));
const sharedProcess = new SharedProcess(dataDir, builtInExtensionsDir);
const sendSharedProcessReady = (socket: WebSocket): void => {
const active = new SharedProcessActiveMessage();
active.setSocketPath(sharedProcess.socketPath);
active.setLogPath(logDir);
const serverMessage = new ServerMessage();
serverMessage.setSharedProcessActive(active);
socket.send(serverMessage.serializeBinary());
};
sharedProcess.onState((event) => {
if (event.state === SharedProcessState.Ready) {
app.wss.clients.forEach((c) => sendSharedProcessReady(c));
}
2019-03-27 03:21:03 +07:00
});
let password = options.password;
if (!password) {
// Generate a random password with a length of 24.
const buffer = Buffer.alloc(12);
randomFillSync(buffer);
password = buffer.toString("hex");
}
2019-03-27 03:21:03 +07:00
const hasCustomHttps = certData && certKeyData;
const app = await createApp({
allowHttp: options.allowHttp,
bypassAuth: options.noAuth,
registerMiddleware: (app): void => {
app.use((req, res, next) => {
res.on("finish", () => {
logger.trace(`\u001B[1m${req.method} ${res.statusCode} \u001B[0m${req.url}`, field("host", req.hostname), field("ip", req.ip));
});
2019-03-27 03:21:03 +07:00
next();
});
// If we're not running from the binary and we aren't serving the static
// pre-built version, use webpack to serve the web files.
if (!isCli && !serveStatic) {
const webpackConfig = require(path.resolve(__dirname, "..", "..", "web", "webpack.config.js"));
const compiler = require("webpack")(webpackConfig);
app.use(require("webpack-dev-middleware")(compiler, {
logger,
publicPath: webpackConfig.output.publicPath,
stats: webpackConfig.stats,
}));
app.use(require("webpack-hot-middleware")(compiler));
}
},
serverOptions: {
builtInExtensionsDirectory: builtInExtensionsDir,
dataDirectory: dataDir,
workingDirectory: workingDir,
cacheDirectory: cacheHome,
fork: (modulePath: string, args?: string[], options?: ForkOptions): ChildProcess => {
if (options && options.env && options.env.AMD_ENTRYPOINT) {
return forkModule(options.env.AMD_ENTRYPOINT, args, options, dataDir);
}
2019-03-05 00:46:34 +07:00
2019-03-27 03:21:03 +07:00
if (isCli) {
return spawn(process.execPath, [path.join(buildDir, "out", "cli.js"), "--fork", modulePath, "--extra-args", JSON.stringify(args), "--data-dir", dataDir], {
...options,
stdio: [null, null, null, "ipc"],
});
2019-03-27 03:21:03 +07:00
} else {
return fork(modulePath, args, options);
}
},
2019-03-27 03:21:03 +07:00
},
password,
httpsOptions: hasCustomHttps ? {
key: certKeyData,
cert: certData,
} : undefined,
});
logger.info("Starting webserver...", field("host", options.host), field("port", options.port));
app.server.listen(options.port, options.host);
let clientId = 1;
app.wss.on("connection", (ws, req) => {
const id = clientId++;
if (sharedProcess.state === SharedProcessState.Ready) {
sendSharedProcessReady(ws);
}
2019-03-27 03:21:03 +07:00
logger.info(`WebSocket opened \u001B[0m${req.url}`, field("client", id), field("ip", req.socket.remoteAddress));
2019-03-27 03:21:03 +07:00
ws.on("close", (code) => {
logger.info(`WebSocket closed \u001B[0m${req.url}`, field("client", id), field("code", code));
});
2019-03-27 03:21:03 +07:00
});
2019-03-27 03:21:03 +07:00
if (!options.certKey && !options.cert) {
logger.warn("No certificate specified. \u001B[1mThis could be insecure.");
// TODO: fill in appropriate doc url
2019-03-27 21:36:32 +07:00
logger.warn("Documentation on securing your setup: https://github.com/codercom/code-server/blob/master/doc/security/ssl.md");
2019-03-27 03:21:03 +07:00
}
2019-03-27 03:21:03 +07:00
if (!options.noAuth) {
logger.info(" ");
2019-03-27 03:21:03 +07:00
logger.info(`Password:\u001B[1m ${password}`);
} else {
logger.warn("Launched without authentication.");
}
2019-03-27 03:21:03 +07:00
const url = `http://localhost:${options.port}/`;
logger.info(" ");
logger.info("Started (click the link below to open):");
logger.info(url);
logger.info(" ");
if (options.open) {
try {
await opn(url);
} catch (e) {
logger.warn("Url couldn't be opened automatically.", field("url", url), field("exception", e));
}
}
2019-03-27 03:21:03 +07:00
})().catch((ex) => {
logger.error(ex);
});