pulumi/sdk/nodejs/cmd/run/index.ts
joeduffy 67e5750742 Fix a bunch of Linux issues
There's a fair bit of clean up in here, but the meat is:

* Allocate the language runtime gRPC client connection on the
  goroutine that will use it; this eliminates race conditions.

* The biggie: there *appears* to be a bug in gRPC's implementation
  on Linux, where it doesn't implement WaitForReady properly.  The
  behavior I'm observing is that RPC calls will not retry as they
  are supposed to, but will instead spuriously fail during the RPC
  startup.  To work around this, I've added manual retry logic in
  the shared plugin creation function so that we won't even try
  to use the client connection until it is in a well-known state.
  pulumi/pulumi-fabric#337 tracks getting to the bottom of this and,
  ideally, removing the work around.

The other minor things are:

* Separate run.js into its own module, so it doesn't include
  index.js and do a bunch of random stuff it shouldn't be doing.

* Allow run.js to be invoked without a --monitor.  This makes
  testing just the run part of invocation easier (including
  config, which turned out to be super useful as I was debugging).

* Tidy up some messages.
2017-09-08 15:11:09 -07:00

131 lines
4.9 KiB
TypeScript

// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
// This is the entrypoint for running a Node.js program with minimal scaffolding.
import * as minimist from "minimist";
import * as path from "path";
import * as runtime from "../../runtime";
let grpc = require("grpc");
let engrpc = require("../../proto/engine_grpc_pb.js");
let langproto = require("../../proto/languages_pb.js");
let langrpc = require("../../proto/languages_grpc_pb.js");
function usage(): void {
console.error(`usage: RUN <flags> [program] <[arg]...>`);
console.error(``);
console.error(` where [flags] may include`);
console.error(` --config.k=v... set runtime config key k to value v`);
console.error(` --dry-run true to simulate resource changes, but without making them`);
console.error(` --pwd=pwd change the working directory before running the program`);
console.error(` --monitor=addr the RPC address for a resource monitor to connect to`);
console.error(` --engine=addr the RPC address for a resource engine to connect to`);
console.error(``);
console.error(` and [program] is a JavaScript program to run in Node.js, and [arg]... optional args to it.`);
}
export function main(args: string[]): void {
// See usage above for the intended usage of this program, including flags and required args.
let config: {[key: string]: string} = {};
let argv: minimist.ParsedArgs = minimist(args, {
boolean: [ "dry-run" ],
string: [ "pwd", "monitor", "engine" ],
unknown: (arg: string) => {
// If unknown, first see if it's a --config.k=v flag.
let cix = arg.indexOf("-config");
if (cix === 0 || cix === 1) {
let kix = arg.indexOf(".");
let vix = arg.indexOf("=");
if (kix === -1 || vix === -1) {
console.error(`fatal: --config flag malformed (expected '--config.key=val')`);
usage();
process.exit(-1);
}
config[arg.substring(kix+1, vix)] = arg.substring(vix+1);
return false;
} else if (arg.indexOf("-") === 0) {
console.error(`fatal: Unrecognized flag ${arg}`);
usage();
process.exit(-1);
return false;
}
return true;
},
stopEarly: true,
});
// Set any configuration keys/values that were found.
for (let key of Object.keys(config)) {
runtime.setConfig(key, config[key]);
}
// If there is a --pwd directive, switch directories.
let pwd: string | undefined = argv["pwd"];
if (pwd) {
process.chdir(pwd);
}
// If ther is a --dry-run directive, flip the switch. This controls whether we are planning vs. really doing it.
let dryrun = false;
if (argv["dry-run"]) {
dryrun = true;
}
// If there is a monitor argument, connect to it.
let monitor: any | undefined;
let monitorAddr: string | undefined = argv["monitor"];
if (monitorAddr) {
monitor = new langrpc.ResourceMonitorClient(monitorAddr, grpc.credentials.createInsecure());
}
// If there is an engine argument, connect to it too.
let engine: any | undefined;
let engineAddr: string | undefined = argv["engine"];
if (engineAddr) {
engine = new engrpc.EngineClient(engineAddr, grpc.credentials.createInsecure());
}
// Now configure the runtime and get it ready to run the program.
runtime.configure(monitor, engine, dryrun);
// Pluck out the program and arguments.
if (argv._.length === 0) {
console.error("fatal: Missing program to execute");
usage();
process.exit(-1);
}
let program: string = argv._[0];
if (program.indexOf(".") === 0) {
// If there was a pwd change, make this relative to it.
if (pwd) {
program = path.join(pwd, program);
}
} else if (program.indexOf("/") !== 0) {
// Neither absolute nor relative module, we refuse to execute it.
console.error(`fatal: Program path '${program}' must be an absolute or relative path to the program`);
usage();
process.exit(-1);
}
// Now fake out the process-wide argv, to make the program think it was run normally.
let programArgs: string[] = argv._.slice(1);
process.argv = [ process.argv[0], process.argv[1], ...programArgs ];
// Now go ahead and execute the code. This keeps the process alive until the message loop exits.
runtime.Log.debug(`Running program '${program}' in pwd '${process.cwd()}' w/ args: ${programArgs}`);
try {
require(program);
}
catch (err) {
runtime.Log.debug(`Running program '${program}' failed with an unhandled exception:`);
runtime.Log.debug(err);
throw err;
}
finally {
runtime.disconnect();
}
}
main(process.argv.slice(2));