Move language host logic from Node to Go (#901)

* experimental: separate language host from node

* Remove langhost details from the NodeJS SDK runtime

* Cleanup

* Work around an issue where Node sometimes loads the same module twice in two different contexts, resulting in two distinct module objects. Some additional cleanup.

* Add some tests

* Fix up the Windows script

* Fix up the install scripts and Windows build

* Code review feedback

* Code review feedback: error capitalization
This commit is contained in:
Sean Gillespie 2018-02-10 02:15:04 +00:00 committed by GitHub
parent 296151e088
commit e87204d3e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 359 additions and 267 deletions

View file

@ -58,6 +58,18 @@
WorkingDirectory="$(NodeJSSdkDirectory)" />
</Target>
<Target Name="GoCompileNodeSdk">
<ItemGroup>
<GoPackagesToBuild Include="github.com/pulumi/pulumi/sdk/nodejs/cmd/pulumi-langhost-nodejs" />
</ItemGroup>
<Exec Command="git describe --tags 2>nul" ConsoleToMSBuild="true" Condition="'$(Version)' == ''">
<Output TaskParameter="ConsoleOutput" PropertyName="Version" />
</Exec>
<Exec Command="go install -ldflags &quot;-X main.version=$(Version)&quot; %(GoPackagesToBuild.Identity)" />
</Target>
<Target Name="BinplaceNodeSdkProtos">
<ItemGroup>
<NodeSdkProtosForBinplace Include="$(NodeSdkDirectory)\proto\nodejs\**\*" />
@ -99,7 +111,7 @@
</Target>
<Target Name="BuildNodeSdk"
DependsOnTargets="CopyNodeSdkProtos;BuildNativeRuntimeModule;TypeScriptCompileNodeSdk;BinPlaceNodeSdk">
DependsOnTargets="CopyNodeSdkProtos;BuildNativeRuntimeModule;TypeScriptCompileNodeSdk;GoCompileNodeSdk;BinPlaceNodeSdk">
</Target>
<Target Name="BuildGoCmds">
@ -119,13 +131,18 @@
</Target>
<Target Name="IntegrationTest">
<Exec Command="where pulumi-langhost-nodejs.cmd"
<Exec Command="where pulumi-langhost-nodejs-exec.cmd"
IgnoreExitCode="true">
<Output TaskParameter="ExitCode" PropertyName="WhereLangHostExecExitCode" />
</Exec>
<Exec Command="where pulumi-langhost-nodejs"
IgnoreExitCode="true">
<Output TaskParameter="ExitCode" PropertyName="WhereLangHostExitCode" />
</Exec>
<Error Text="Please add &quot;$(NodeJSSdkDirectory)\bin&quot; to your path before running integration tests."
Condition="$(WhereLangHostExitCode) != 0"/>
Condition="$(WhereLangHostExitCode) != 0 Or $(WhereLangHostExecExitCode) != 0"/>
<!-- Ignore the exit code (but retain it) so we can kill all the lingering node processes even when go test
fails. Otherwise, the AppVeyor job would hang until it reached the timeout -->

View file

@ -30,12 +30,12 @@ function CopyPackage($pathToModule, $moduleName) {
RunGoBuild "github.com/pulumi/pulumi"
CopyPackage "$Root\sdk\nodejs\bin" "pulumi"
Copy-Item "$Root\dist\sdk\nodejs\pulumi-langhost-nodejs.cmd" "$PublishDir\bin"
Copy-Item "$Root\dist\sdk\nodejs\pulumi-langhost-nodejs-exec.cmd" "$PublishDir\bin"
New-Item -ItemType Directory -Force -Path "$PublishDir\bin\node" | Out-Null
Copy-Item "$Root\sdk\nodejs\custom_node\node.exe" "$PublishDir\bin\node"
Remove-Item "$PublishDir\node_modules\pulumi\pulumi-langhost-nodejs.cmd"
Remove-Item "$PublishDir\node_modules\pulumi\pulumi-langhost-nodejs-exec.cmd"
Remove-Item "$PublishDir\node_modules\pulumi\pulumi-provider-pulumi-nodejs.cmd"
# By default, if the archive already exists, 7zip will just add files to it, so blow away the existing

View file

@ -47,7 +47,7 @@ copy_package() {
run_go_build "${ROOT}"
# Copy over the langhost and dynamic provider
cp ${ROOT}/sdk/nodejs/pulumi-langhost-nodejs ${PUBDIR}/bin/
cp ${ROOT}/sdk/nodejs/pulumi-langhost-nodejs-exec ${PUBDIR}/bin/
cp ${ROOT}/sdk/nodejs/pulumi-provider-pulumi-nodejs ${PUBDIR}/bin/
# Copy packages

View file

@ -2,6 +2,15 @@ PROJECT_NAME := Pulumi Node.JS SDK
NODE_MODULE_NAME := pulumi
VERSION := $(shell git describe --tags --dirty 2>/dev/null)
LANGUAGE_HOST := github.com/pulumi/pulumi/sdk/nodejs/cmd/pulumi-langhost-nodejs
GOMETALINTERBIN := gometalinter
GOMETALINTER := ${GOMETALINTERBIN} --config=../../Gometalinter.json
PROJECT_PKGS := $(shell go list ./cmd...)
TESTPARALLELISM := 10
TEST_FAST_TIMEOUT := 2m
include ../../build/common.mk
export PATH:=$(shell yarn bin 2>/dev/null):$(PATH)
@ -10,9 +19,11 @@ ensure::
cd runtime/native && ./ensure_node_v8.sh
lint::
$(GOMETALINTER) cmd/pulumi-langhost-nodejs/main.go | sort ; exit "$${PIPESTATUS[0]}"
tslint -c tslint.json -p tsconfig.json
build::
go install -ldflags "-X github.com/pulumi/pulumi/pkg/version.Version=${VERSION}" ${LANGUAGE_HOST}
cd runtime/native && node-gyp configure
cp -R ../proto/nodejs/. proto/
cd runtime/native/ && node-gyp build
@ -27,7 +38,8 @@ build::
find tests/runtime/langhost/cases/* -type d -exec cp -R {} bin/tests/runtime/langhost/cases/ \;
install::
cp pulumi-langhost-nodejs "$(PULUMI_BIN)"
GOBIN=$(PULUMI_BIN) go install -ldflags "-X github.com/pulumi/pulumi/pkg/version.Version=${VERSION}" ${LANGUAGE_HOST}
cp pulumi-langhost-nodejs-exec "$(PULUMI_BIN)"
cp pulumi-provider-pulumi-nodejs "$(PULUMI_BIN)"
rm -rf "$(PULUMI_NODE_MODULES)/$(NODE_MODULE_NAME)/tests"
@ -35,3 +47,4 @@ test_fast::
istanbul cover --print none _mocha -- --timeout 15000 'bin/tests/**/*.spec.js'
istanbul report text-summary
istanbul report text
go test -timeout $(TEST_FAST_TIMEOUT) -cover -parallel ${TESTPARALLELISM} ${PROJECT_PKGS}

View file

@ -8,4 +8,4 @@ REM
REM NOTE: we pass a dummy argument before the actual args because the
REM langhost module expects to be invoked as `node path/to/langhost args`,
REM but we are invoking it with `-e`.
"%~dp0\..\custom_node\node.exe" -e "require('%REQUIRE_ROOT%./cmd/langhost');" dummy_argument %*
"%~dp0\..\custom_node\node.exe" -e "require('%REQUIRE_ROOT%./cmd/run');" dummy_argument %*

View file

@ -1,40 +0,0 @@
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
// This is the primary entrypoint for all Pulumi programs that are being watched by the resource planning
// monitor. It creates the "host" that is responsible for wiring up gRPC connections to and from the monitor,
// and drives execution of a Node.js program, communicating back as required to track all resource allocations.
import * as minimist from "minimist";
import * as runtime from "../../runtime";
export function main(rawArgs: string[]): void {
// Parse command line flags
const argv: minimist.ParsedArgs = minimist(rawArgs, {
string: [ "tracing" ],
});
// Extract the real arguments containing the monitor and optional server addresses
const args = argv._.slice(2);
// The program requires a single argument: the address of the RPC endpoint for the resource monitor. It
// optionally also takes a second argument, a reference back to the engine, but this may be missing.
if (args.length === 0) {
console.error("fatal: Missing <monitor> address");
process.exit(-1);
return;
}
const monitorAddr: string = args[0];
let serverAddr: string | undefined;
if (args.length > 1) {
serverAddr = args[1];
}
// Finally connect up the gRPC client/server and listen for incoming requests.
const { server, port } = runtime.serveLanguageHost(monitorAddr, serverAddr, argv["logging"]);
// Emit the address so the monitor can read it to connect. The gRPC server will keep the message loop alive.
console.log(port);
}
main(process.argv);

View file

@ -0,0 +1,226 @@
// Copyright 2016-2018, Pulumi Corporation. All rights reserved.
// pulumi-langhost-nodejs serves as the "language host" for Pulumi
// programs written in NodeJS. It is ultimately responsible for spawning the
// language runtime that executes the program.
//
// The program being executed is executed by a shim script called
// `pulumi-langhost-nodejs-exec`. This script is written in the hosted
// language (in this case, node) and is responsible for initiating RPC
// links to the resource monitor and engine.
//
// It's therefore the responsibility of this program to implement
// the LanguageHostServer endpoint by spawning instances of
// `pulumi-langhost-nodejs-exec` and forwarding the RPC request arguments
// to the command-line.
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"os/exec"
"strings"
"syscall"
"github.com/golang/glog"
pbempty "github.com/golang/protobuf/ptypes/empty"
"github.com/pkg/errors"
"github.com/pulumi/pulumi/pkg/util/cmdutil"
"github.com/pulumi/pulumi/pkg/util/rpcutil"
"github.com/pulumi/pulumi/pkg/version"
pulumirpc "github.com/pulumi/pulumi/sdk/proto/go"
"google.golang.org/grpc"
)
const (
// By convention, the executor is the name of the current program
// (pulumi-langhost-nodejs) plus this suffix.
nodeExecSuffix = "-exec" // the exec shim for Pulumi to run Node programs.
// The runtime expects the config object to be saved to this environment variable.
pulumiConfigVar = "PULUMI_CONFIG"
)
// Launches the language host RPC endpoint, which in turn fires
// up an RPC server implementing the LanguageRuntimeServer RPC
// endpoint.
func main() {
var tracing string
var givenExecutor string
flag.StringVar(&tracing, "tracing", "",
"Emit tracing to a Zipkin-compatible tracing endpoint")
// You can use the below flag to request that the language host load
// a specific executor instead of probing the PATH. This is used specifically
// in run.spec.ts to work around some unfortunate Node module loading behavior.
flag.StringVar(&givenExecutor, "use-executor", "",
"Use the given program as the executor instead of looking for one on PATH")
flag.Parse()
args := flag.Args()
cmdutil.InitLogging(false, 0, false)
cmdutil.InitTracing(os.Args[0], tracing)
var nodeExec string
if givenExecutor == "" {
pathExec, err := exec.LookPath(os.Args[0] + nodeExecSuffix)
if err != nil {
err = errors.Wrapf(err, "could not find `%s` on the $PATH", os.Args[0]+nodeExecSuffix)
cmdutil.Exit(err)
}
glog.V(3).Infof("language host identified executor from path: `%s`", pathExec)
nodeExec = pathExec
} else {
glog.V(3).Infof("language host asked to use specific executor: `%s`", givenExecutor)
nodeExec = givenExecutor
}
if len(args) == 0 {
cmdutil.Exit(errors.New("missing host engine RPC address as first argument"))
}
monitorAddress := args[0]
// Optionally pluck out the engine so we can do logging, etc.
var engineAddress string
if len(args) > 1 {
engineAddress = args[1]
}
// Fire up a gRPC server, letting the kernel choose a free port.
port, done, err := rpcutil.Serve(0, nil, []func(*grpc.Server) error{
func(srv *grpc.Server) error {
host := newLanguageHost(nodeExec, monitorAddress, engineAddress, tracing)
pulumirpc.RegisterLanguageRuntimeServer(srv, host)
return nil
},
})
if err != nil {
cmdutil.Exit(errors.Wrapf(err, "could not start language host RPC server"))
}
// Otherwise, print out the port so that the spawner knows how to reach us.
fmt.Printf("%d\n", port)
// And finally wait for the server to stop serving.
if err := <-done; err != nil {
cmdutil.Exit(errors.Wrapf(err, "language host RPC stopped serving"))
}
}
// nodeLanguageHost implements the LanguageRuntimeServer interface
// for use as an API endpoint.
type nodeLanguageHost struct {
exec string
monitorAddress string
engineAddress string
tracing string
}
func newLanguageHost(exec, monitorAddress, engineAddress, tracing string) pulumirpc.LanguageRuntimeServer {
return &nodeLanguageHost{
exec: exec,
monitorAddress: monitorAddress,
engineAddress: engineAddress,
tracing: tracing,
}
}
// constructArguments constructs a command-line for `pulumi-langhost-nodejs`
// by enumerating all of the optional and non-optional arguments present
// in a RunRequest.
func (host *nodeLanguageHost) constructArguments(req *pulumirpc.RunRequest) []string {
var args []string
maybeAppendArg := func(k, v string) {
if v != "" {
args = append(args, "--"+k, v)
}
}
maybeAppendArg("monitor", host.monitorAddress)
maybeAppendArg("engine", host.engineAddress)
maybeAppendArg("project", req.GetProject())
maybeAppendArg("stack", req.GetStack())
maybeAppendArg("pwd", req.GetPwd())
if req.GetDryRun() {
args = append(args, "--dry-run")
}
maybeAppendArg("parallel", fmt.Sprint(req.GetParallel()))
maybeAppendArg("tracing", host.tracing)
if req.GetProgram() == "" {
// If the program path is empty, just use "."; this will cause Node to try to load the default module
// file, by default ./index.js, but possibly overridden in the "main" element inside of package.json.
args = append(args, ".")
} else {
args = append(args, req.GetProgram())
}
args = append(args, req.GetArgs()...)
return args
}
// constructConfig json-serializes the configuration data given as part of
// a RunRequest.
func (host *nodeLanguageHost) constructConfig(req *pulumirpc.RunRequest) (string, error) {
configMap := req.GetConfig()
if configMap == nil {
return "{}", nil
}
configJSON, err := json.Marshal(configMap)
if err != nil {
return "", err
}
return string(configJSON), nil
}
// RPC endpoint for LanguageRuntimeServer::Run
func (host *nodeLanguageHost) Run(ctx context.Context, req *pulumirpc.RunRequest) (*pulumirpc.RunResponse, error) {
args := host.constructArguments(req)
config, err := host.constructConfig(req)
if err != nil {
err = errors.Wrap(err, "failed to serialize configuration")
return nil, err
}
if glog.V(5) {
commandStr := strings.Join(args, " ")
glog.V(5).Infoln("Language host launching process: ", host.exec, commandStr)
}
// Now simply spawn a process to execute the requested program, wiring up stdout/stderr directly.
var errResult string
cmd := exec.Command(host.exec, args...) // nolint: gas, intentionally running dynamic program name.
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
cmd.Env = append(os.Environ(), pulumiConfigVar+"="+string(config))
if err := cmd.Run(); err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
// If the program ran, but exited with a non-zero error code. This will happen often, since user
// errors will trigger this. So, the error message should look as nice as possible.
if status, stok := exiterr.Sys().(syscall.WaitStatus); stok {
err = errors.Errorf("Program exited with non-zero exit code: %d", status.ExitStatus())
} else {
err = errors.Wrapf(exiterr, "Program exited unexpectedly")
}
} else {
// Otherwise, we didn't even get to run the program. This ought to never happen unless there's
// a bug or system condition that prevented us from running the language exec. Issue a scarier error.
err = errors.Wrapf(err, "Problem executing program (could not run language executor)")
}
errResult = err.Error()
}
return &pulumirpc.RunResponse{Error: errResult}, nil
}
func (host *nodeLanguageHost) GetPluginInfo(ctx context.Context, req *pbempty.Empty) (*pulumirpc.PluginInfo, error) {
return &pulumirpc.PluginInfo{
Version: version.Version,
}, nil
}

View file

@ -0,0 +1,62 @@
// Copyright 2016-2018, Pulumi Corporation. All rights reserved.
package main
import (
"strings"
"testing"
pulumirpc "github.com/pulumi/pulumi/sdk/proto/go"
"github.com/stretchr/testify/assert"
)
func TestArgumentConstruction(t *testing.T) {
t.Parallel()
t.Run("DryRun-NoArguments", func(tt *testing.T) {
host := &nodeLanguageHost{}
rr := &pulumirpc.RunRequest{DryRun: true}
args := host.constructArguments(rr)
assert.Contains(tt, args, "--dry-run")
assert.NotContains(tt, args, "true")
})
t.Run("OptionalArgs-PassedIfSpecified", func(tt *testing.T) {
host := &nodeLanguageHost{}
rr := &pulumirpc.RunRequest{Project: "foo"}
args := strings.Join(host.constructArguments(rr), " ")
assert.Contains(tt, args, "--project foo")
})
t.Run("OptionalArgs-NotPassedIfNotSpecified", func(tt *testing.T) {
host := &nodeLanguageHost{}
rr := &pulumirpc.RunRequest{}
args := strings.Join(host.constructArguments(rr), " ")
assert.NotContains(tt, args, "--stack")
})
t.Run("DotIfProgramNotSpecified", func(tt *testing.T) {
host := &nodeLanguageHost{}
rr := &pulumirpc.RunRequest{}
args := strings.Join(host.constructArguments(rr), " ")
assert.Contains(tt, args, ".")
})
t.Run("ProgramIfProgramSpecified", func(tt *testing.T) {
host := &nodeLanguageHost{}
rr := &pulumirpc.RunRequest{Program: "foobar"}
args := strings.Join(host.constructArguments(rr), " ")
assert.Contains(tt, args, "foobar")
})
}
func TestConfig(t *testing.T) {
t.Parallel()
t.Run("Config-Empty", func(tt *testing.T) {
host := &nodeLanguageHost{}
rr := &pulumirpc.RunRequest{Project: "foo"}
str, err := host.constructConfig(rr)
assert.NoError(tt, err)
assert.JSONEq(tt, "{}", str)
})
}

View file

@ -1,3 +0,0 @@
#!/usr/bin/env node
require("pulumi/cmd/langhost");

View file

@ -0,0 +1,3 @@
#!/usr/bin/env node
require("pulumi/cmd/run");

View file

@ -0,0 +1,3 @@
#!/usr/bin/env node
require("./bin/cmd/run");

View file

@ -3,7 +3,6 @@
export * from "./closure";
export * from "./config";
export * from "./invoke";
export * from "./langhost";
export * from "./resource";
export * from "./rpc";
export * from "./settings";

View file

@ -1,210 +0,0 @@
// Copyright 2016-2017, Pulumi Corporation. All rights reserved.
import * as childprocess from "child_process";
import * as os from "os";
import * as path from "path";
import * as runtime from "../runtime";
import { version } from "../version";
const grpc = require("grpc");
const langproto = require("../proto/language_pb.js");
const langrpc = require("../proto/language_grpc_pb.js");
const plugproto = require("../proto/plugin_pb.js");
/**
* monitorAddr is the current resource monitor address.
*/
let monitorAddr: string | undefined;
/**
* engineAddr is the current resource engine address, if any.
*/
let engineAddr: string | undefined;
/**
* tracingUrl is the current resource engine address, if any.
*/
let tracingUrl: string | undefined;
/**
* serveLanguageHost spawns a language host that connects to the resource monitor and listens on port.
*/
export function serveLanguageHost(monitor: string, engine?: string, tracing?: string): { server: any, port: number } {
if (monitorAddr) {
throw new Error("Already connected to a resource monitor; cannot serve two hosts in one process");
}
monitorAddr = monitor;
engineAddr = engine;
tracingUrl = tracing;
// TODO[pulumi/pulumi#545]: Wire up to OpenTracing. Automatic tracing of gRPC calls themselves is pending
// https://github.com/grpc-ecosystem/grpc-opentracing/issues/11 which is pending
// https://github.com/grpc/grpc-node/pull/59.
// Now fire up the gRPC server and begin serving!
const server = new grpc.Server();
server.addService(langrpc.LanguageRuntimeService, {
run: runRPC,
getPluginInfo: getPluginInfoRPC,
});
const port: number = server.bind(`0.0.0.0:0`, grpc.ServerCredentials.createInsecure());
// Now we're done: the server is started, and gRPC keeps the even loop alive.
server.start();
return { server: server, port: port }; // return the port for callers.
}
/**
* runRPC implements the core "run" logic for both planning and deploying.
*/
function runRPC(call: any, callback: any): void {
// Unpack the request and fire up the program.
// IDEA: stick the monitor address in Run's RPC so that it's per invocation.
const req: any = call.request;
const resp = new langproto.RunResponse();
let proc: childprocess.ChildProcess | undefined;
try {
// Create an args array to pass to spawn, starting with just the run.js program.
const args: string[] = [
path.join(__filename, "..", "..", "cmd", "run"),
];
// Serialize the config args using an environment variable.
const env: {[key: string]: string} = {};
const config: any = req.getConfigMap();
if (config) {
// First flatten the config into a regular (non-RPC) object.
const configForEnv: {[key: string]: string} = {};
for (const entry of config.entries()) {
configForEnv[(entry[0] as string)] = (entry[1] as string);
}
// Now JSON serialize the config into an environment variable.
env[runtime.configEnvKey] = JSON.stringify(configForEnv);
}
const project: string | undefined = req.getProject();
if (project) {
args.push("--project");
args.push(project);
}
const stack: string | undefined = req.getStack();
if (stack) {
args.push("--stack");
args.push(stack);
}
// If this is a dry-run, tell the program so.
if (req.getDryrun()) {
args.push("--dry-run");
}
// If parallel execution has been requested, propagate it.
const parallel: number | undefined = req.getParallel();
if (parallel !== undefined) {
args.push("--parallel");
args.push(parallel.toString());
}
// If a different working directory was requested, make sure to pass it too.
const pwd: string | undefined = req.getPwd();
if (pwd) {
args.push("--pwd");
args.push(pwd);
}
// Push the resource monitor address to connect up to.
if (!monitorAddr) {
throw new Error("No resource monitor known; please ensure the language host is alive");
}
args.push("--monitor");
args.push(monitorAddr);
// Push the resource engine address, for logging, etc., if there is one.
if (engineAddr) {
args.push("--engine");
args.push(engineAddr);
}
// Push the tracing url, if there is one.
if (tracingUrl) {
args.push("--tracing");
args.push(tracingUrl);
}
// Now get a path to the program.
let program: string | undefined = req.getProgram();
if (!program) {
// If the program path is empty, just use "."; this will cause Node to try to load the default module
// file, by default ./index.js, but possibly overridden in the "main" element inside of package.json.
program = ".";
}
args.push(program);
// Serialize the args plainly, following the program.
const argsList: string[] | undefined = req.getArgsList();
if (argsList) {
for (const arg of argsList) {
args.push(arg);
}
}
// We spawn a new process to run the program. This is required because we don't want the run to complete
// until the Node message loop quiesces. It also gives us an extra level of isolation.
proc = childprocess.spawn(process.argv[0], args, {
env: Object.assign({}, process.env, env),
});
proc.stdout.on("data", (data: string | Buffer) => {
console.log(stripEOL(data));
});
proc.stderr.on("data", (data: string | Buffer) => {
console.error(stripEOL(data));
});
// If we got this far, make sure to communicate completion when the process terminates.
proc.on("close", (code: number, signal: string) => {
if (callback !== undefined) {
if (code !== 0) {
if (signal) {
resp.setError(`Program exited due to a signal: ${signal}`);
}
else {
resp.setError(`Program exited with non-zero exit code: ${code}`);
}
}
callback(undefined, resp);
callback = undefined;
}
});
}
catch (err) {
if (callback !== undefined) {
resp.setError(err.message);
callback(undefined, resp);
callback = undefined;
}
}
}
function stripEOL(data: string | Buffer): string {
let dataString: string;
if (typeof data === "string") {
dataString = data;
}
else {
dataString = data.toString("utf-8");
}
const eolIndex = dataString.lastIndexOf(os.EOL);
if (eolIndex !== -1) {
dataString = dataString.substring(0, eolIndex);
}
return dataString;
}
/**
* getPluginInfoRPC implements the RPC interface for plugin introspection.
*/
function getPluginInfoRPC(call: any, callback: any): void {
const resp = new plugproto.PluginInfo();
resp.setVersion(version);
callback(undefined, resp);
}

View file

@ -481,9 +481,33 @@ function createMockResourceMonitor(
}
function serveLanguageHostProcess(monitorAddr: string): { proc: childProcess.ChildProcess, addr: Promise<string> } {
// Spawn the language host in a separate process so that each test case gets an isolated heap, globals, etc.
const proc = childProcess.spawn(process.argv[0], [
path.join(__filename, "..", "..", "..", "..", "cmd", "langhost", "index.js"),
// A quick note about this:
//
// Normally, pulumi-langhost-nodejs probes the path in order to
// find the nodejs executor, pulumi-langhost-nodejs-exec. This works
// great in all scenarios other than testing within this file. If the executor
// that it founds resides in the Pulumi install dir (which it will, if these tests
// are being executed by `make`), then Node will execute it by resolving our relative
// path requires to the Pulumi install directory. However, the programs being evaluated
// by the language host are using the current directory to resolve relative path requires.
//
// Normally, this isn't a problem - ostensibly the stuff in the Pulumi install directory
// is the same as the stuff that we are currently testing. However, the "settings.ts" module
// contains some global state that is expected to be shared between the language host stub
// that is connecting to our RPC endpoints (run/index.ts) and the Pulumi program being
// evaluated. Node, when resolving the require, will pick different modules to load depending
// on which context the require occured; if it happened in run/index.ts, it'll load
// from Pulumi install directory, while requires coming from anywhere else will load
// from the source directory. Because these are two different files, Node instantiates two
// separate module objects and the state that we are expecting to share is not actually shared,
// ultimately resulting in extremely wacky errors.
//
// In order to work around this problem, the langhost is explicitly instructed
// (through --use-executor) to use a specific executor which will load modules from
// the source directory and not the install directory.
const proc = childProcess.spawn("pulumi-langhost-nodejs", [
"--use-executor",
path.join(__filename, "..", "..", "..", "..", "..", "pulumi-langhost-nodejs-exec-test"),
monitorAddr,
]);
// Hook the first line so we can parse the address. Then we hook the rest to print for debugging purposes, and

View file

@ -36,14 +36,12 @@
"runtime/config.ts",
"runtime/debuggable.ts",
"runtime/invoke.ts",
"runtime/langhost.ts",
"runtime/resource.ts",
"runtime/rpc.ts",
"runtime/settings.ts",
"runtime/stack.ts",
"cmd/dynamic-provider/index.ts",
"cmd/langhost/index.ts",
"cmd/run/index.ts",
"tests/config.spec.ts",