Skip to content

Commit

Permalink
Fix support for exec args
Browse files Browse the repository at this point in the history
  • Loading branch information
demurgos committed Nov 25, 2018
1 parent 387cb9c commit bf5a2e5
Show file tree
Hide file tree
Showing 16 changed files with 423 additions and 133 deletions.
38 changes: 30 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,17 +221,20 @@ adhered to, despite best efforts:
## How it works

When you create a new wrapper, the library starts by creating a context
(SwContext). This is a snapshot of the arguments and environment variables
you passed, but also path to the current Node process and library.
(SwContext). This is an object storing the user options (wrapper path, wrapper
data, sameProcess mode) and extra data such as a unique key and the absolute
paths to the Node process, `spawn-wrap`, a few dependencies and the shims.

During the creation of the context, a "shim directory" is written (by default,
it is a unique directory inside `~/.node-spawn-wrap`). This directory contains
executables ("shims") are intended to act as Node but inject the wrapping logic.
executables ("shims") that are intended to intercept system calls to spawn
Node processes and instead trigger the wrapping logic.

These executables are auto-generated and the context is embedded in them:
executing any of them will trigger the wrapping code.
The shims are auto-generated executables. The context is embedded in them.
Their role is to patch of internals of the current Node process, load the
wrapper and execute them.
One shim is created with the name `node`, and eventually another one with the
same name as the root process (for example `iojs` if the root process was
same basename as the root process (for example `iojs` if the root process was
named `iojs` instead of `node`). These shims are executable scripts with a
shebang. For Windows, a `.cmd` file is created for each shim: for example
`node.cmd` to open the `node` shim.
Expand All @@ -253,7 +256,7 @@ The action of rewriting the spawn options is called "munging" in the lib.
The goal is to replace any invocation of the real Node with one of the shims.
The munging has a file-specific step and a general environment patching step.
The munger will use the name (or try to read the shebang) of the spawned file
to try to perform application-specific transformations. It currently detects
to perform application-specific transformations. It currently detects
when you spawn another Node process, `npm`, `cmd` or a known POSIX shell
(`sh`, `bash`, ...).
If you are spawning a shell, it will try to detect if you use the shell to
Expand All @@ -272,7 +275,26 @@ first location in the `PATH` environment variable. It means that any subprocess
inheriting this environment and trying to spawn Node using `node` instead of
an absolute path will default to use the shim executable.

TODO: Explain the magic inside the shim script
### Arguments

The library distinguishes between two types of arguments: execution arguments
(`execArgs` and application arguments (`args`). Execution arguments control the
Node engine. They corresponds to the flags described in `node --help`. For
example `["--eval", "2+2"]`, `["--require", "/foo.js"]` or
`["--experimental-modules"]`. The application arguments are the path to the
executable (e.g. `/usr/bin/node`) and other remaining arguments such as the
path to the script to run.
The execution arguments can be read as `process.execArgv` while the application
arguments are in `process.args`.

The application arguments are only handled by user code so it there are no
real constraint to modify them in the wrapper before running the main module.
On the other hand, execution arguments are only applied when the application
starts. If you want to modify them, you need to pass the updated execution
arguments to a subprocess.

For this reason, in `sameProcess=false` mode, the execution arguments are not
applied to the wrapper but to the child subprocess.

## Migrating from version `1.x` to `2.x`

Expand Down
15 changes: 13 additions & 2 deletions src/lib/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface SwContext<D = any> {
/**
* Absolute system path for the corresponding dependencies.
*/
readonly deps: Readonly<Record<"debug" | "foregroundChild" | "isWindows" | "nodeCli" | "pathEnvVar" | "signalExit", string>>;
readonly deps: Readonly<Record<"debug" | "foregroundChild" | "isWindows" | "pathEnvVar" | "parseNodeOptions" | "signalExit", string>>;

/**
* Unique key identifying this context.
Expand Down Expand Up @@ -79,6 +79,17 @@ export interface SwContext<D = any> {

readonly data: D;

/**
* Run the wrapper and child process in the same process.
*
* Using the same process allows to reduce memory usage and improve speed
* but prevents changing the node engine flags (such as
* `--experimental-modules`) dynamically inside the wrapper.
* If the spawned node process uses node engine flags, multiple processes
* may be used.
*
* Default: `true`.
*/
readonly sameProcess: boolean;

/**
Expand Down Expand Up @@ -246,8 +257,8 @@ function resolvedOptionsToContext(resolved: ResolvedOptions): SwContext {
debug: require.resolve("./debug"),
foregroundChild: require.resolve("demurgos-foreground-child"),
isWindows: require.resolve("is-windows"),
nodeCli: require.resolve("./node-cli"),
pathEnvVar: require.resolve("./path-env-var"),
parseNodeOptions: require.resolve("./parse-node-options"),
signalExit: require.resolve("signal-exit"),
}),
key: resolved.key,
Expand Down
6 changes: 5 additions & 1 deletion src/lib/exe-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,13 @@ export function isCmd(file: string): boolean {
return isWindows() && (file === comspec || /^cmd(?:\.exe)?$/i.test(file));
}

export function isEnv(file: string): boolean {
return file === "env";
}

export function isNode(file: string): boolean {
const cmdname = getExeBasename(process.execPath);
return file === "node" || cmdname === file;
return file === "node" || file === cmdname;
}

export function isNpm(file: string): boolean {
Expand Down
2 changes: 1 addition & 1 deletion src/lib/mungers/cmd.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export function mungeCmd(ctx: SwContext, options: NormalizedOptions): Normalized
const tail: string = nodeMatch[4];

const newArgs: string[] = [...options.args];
newArgs[cmdIndex] = `${startDelimiter}${originalNode}${endDelimiter} "${ctx.shimScript}" ${tail}`;
newArgs[cmdIndex] = `${startDelimiter}${originalNode}${endDelimiter} -- "${ctx.shimScript}" ${tail}`;
return {...options, args: newArgs};
}

Expand Down
54 changes: 16 additions & 38 deletions src/lib/mungers/node.ts
Original file line number Diff line number Diff line change
@@ -1,58 +1,36 @@
import { SwContext } from "../context";
import { debug } from "../debug";
import { getExeBasename } from "../exe-type";
import { ParsedNodeOptions, parseNodeOptions } from "../parse-node-options";
import { NormalizedOptions } from "../types";
import { whichOrUndefined } from "../which-or-undefined";

export function mungeNode(ctx: SwContext, options: NormalizedOptions): NormalizedOptions {
const cmdBasename: string = getExeBasename(options.file);
// make sure it has a main script.
// otherwise, just let it through.
let a = 0;
// tslint:disable-next-line:no-unnecessary-initializer
let mainIndex: number | undefined = undefined;
for (a = 1; mainIndex === undefined && a < options.args.length; a++) {
switch (options.args[a]) {
case "-p":
case "-i":
case "--interactive":
case "--eval":
case "-e":
case "-pe":
mainIndex = undefined;
a = options.args.length;
continue;

case "-r":
case "--require":
a += 1;
continue;

default:
// TODO: Double-check this part
if (options.args[a].match(/^-/)) {
continue;
} else {
mainIndex = a;
a = options.args.length;
break;
}
const parsed: ParsedNodeOptions = parseNodeOptions(options.args);

let newArgs: string[];
if (ctx.sameProcess) {
if (parsed.appArgs.length > 0) {
// Has a script
newArgs = [parsed.execPath, ...parsed.execArgs, "--", ctx.shimScript, ...parsed.appArgs];
} else {
// `--interactive`, `--eval`, `--version`, etc.
// Avoid wrapping these kind of invocations in same-process mode.
newArgs = [...options.args];
}
} else {
// In subProcess mode, the exec args are not applied to the wrapper process.
newArgs = [parsed.execPath, "--", ctx.shimScript, ...parsed.execArgs, ...parsed.appArgs];
}

const newArgs: string[] = [...options.args];
let newFile: string = options.file;

if (mainIndex !== undefined) {
newArgs.splice(mainIndex, 0, ctx.shimScript);
}

// If the file is just something like 'node' then that'll
// resolve to our shim, and so to prevent double-shimming, we need
// to resolve that here first.
// This also handles the case where there's not a main file, like
// `node -e 'program'`, where we want to avoid the shim entirely.
if (cmdBasename === options.file) {
if (options.file === getExeBasename(options.file)) {
const resolvedNode: string | undefined = whichOrUndefined(options.file);
const realNode = resolvedNode !== undefined ? resolvedNode : process.execPath;
newArgs[0] = realNode;
Expand Down
4 changes: 3 additions & 1 deletion src/lib/mungers/sh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function mungeSh(ctx: SwContext, options: NormalizedOptions): NormalizedO

if (isNode(exe)) {
// options.originalNode = command;
newArgs[cmdIndex] = `${prefix}${rawCommand} "${ctx.shimScript}" ${tail}`;
newArgs[cmdIndex] = `${prefix}${rawCommand} -- "${ctx.shimScript}" ${tail}`;
} else if (exe === "npm" && !isWindows()) {
// XXX this will exhibit weird behavior when using /path/to/npm,
// if some other npm is first in the path.
Expand All @@ -51,6 +51,8 @@ export function mungeSh(ctx: SwContext, options: NormalizedOptions): NormalizedO
newArgs[cmdIndex] = c.replace(CMD_RE, `$1 "${ctx.shimExecutable}" "${npmPath}" $3`);
debug("npm munge!", newArgs[cmdIndex]);
}
} else {
return options;
}

return {...options, args: newArgs};
Expand Down
43 changes: 35 additions & 8 deletions src/lib/mungers/shebang.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import fs from "fs";
import { SwContext } from "../context";
import { getExeBasename, isNode } from "../exe-type";
import { getExeBasename, isEnv, isNode } from "../exe-type";
import { NormalizedOptions } from "../types";
import { whichOrUndefined } from "../which-or-undefined";

Expand All @@ -17,19 +17,46 @@ export function mungeShebang(ctx: SwContext, options: NormalizedOptions): Normal
return options;
}

const shebangExe = match[1].split(" ")[0];
const maybeNode = getExeBasename(shebangExe);
if (!isNode(maybeNode)) {
// not a node shebang, leave untouched
return options;
const shebangComponents: ReadonlyArray<string> = match[1].split(" ");
let shebangExe: string;
let shebangTail: ReadonlyArray<string>;
// TODO: Handle shebang args, currently `shebangTail` is always an empty array
switch (shebangComponents.length) {
case 1: {
// Try to recognize `#!/usr/bin/node`
const maybeNode: string = getExeBasename(shebangComponents[0]);
if (!isNode(maybeNode)) {
// not a node shebang, leave untouched
return options;
}
shebangExe = shebangComponents[0];
shebangTail = shebangComponents.slice(1);
break;
}
case 2: {
// Try to recognize `#!/usr/bin/env node`
if (!isEnv(getExeBasename(shebangComponents[0]))) {
return options;
}
const maybeNode: string | undefined = whichOrUndefined(shebangComponents[1]);
if (maybeNode === undefined || !isNode(getExeBasename(maybeNode))) {
// not a node shebang, leave untouched
return options;
}
shebangExe = maybeNode;
shebangTail = shebangComponents.slice(2);
break;
}
default:
return options;
}

// options.originalNode = shebangExe;
// options.basename = maybeNode;
const newFile: string = shebangExe;
const newArgs = [shebangExe, ctx.shimScript]
const newArgs = [shebangExe, "--", ctx.shimScript]
.concat(resolved)
.concat(match[1].split(" ").slice(1))
.concat(shebangTail)
.concat(options.args.slice(1));

return {...options, file: newFile, args: newArgs};
Expand Down
25 changes: 0 additions & 25 deletions src/lib/node-cli.ts

This file was deleted.

Loading

0 comments on commit bf5a2e5

Please sign in to comment.