-
Notifications
You must be signed in to change notification settings - Fork 200
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(compiler): equality and external libraries bugs in simulator (#5554)
This pull request fixes a range of funky JavaScript issues we've had running inflight code in the Wing simulator by switching internally from running JS code in a `vm` to running JS code using Node.js child processes. The main issues with `vm` are: - several third-party JavaScript dependencies behave differently when executed inside a `vm` and outside. This may be related to differences in vm behavior as described in nodejs/node#28823 - we cannot easily stop/kill code executing in a `vm` process, making it impossible to simulate cloud.Function timeouts Fixes #1980 Fixes #4131 Fixes #4118 Fixes #4792 Closes #4725 ## Checklist - [x] Title matches [Winglang's style guide](https://www.winglang.io/contributing/start-here/pull_requests#how-are-pull-request-titles-formatted) - [x] Description explains motivation and solution - [x] Tests added (always) - [ ] Docs updated (only required for features) - [ ] Added `pr/e2e-full` label if this feature requires end-to-end testing *By submitting this pull request, I confirm that my contribution is made under the terms of the [Wing Cloud Contribution License](https://github.com/winglang/wing/blob/main/CONTRIBUTION_LICENSE.md)*.
- Loading branch information
Showing
26 changed files
with
641 additions
and
278 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
import { mkdtemp, readFile } from "node:fs/promises"; | ||
import { tmpdir } from "node:os"; | ||
import path from "node:path"; | ||
import * as util from "node:util"; | ||
import * as vm from "node:vm"; | ||
import { createBundle } from "./bundling"; | ||
import { SandboxOptions } from "./sandbox"; | ||
|
||
export class LegacySandbox { | ||
private createBundlePromise: Promise<void>; | ||
private entrypoint: string; | ||
private code: string | undefined; | ||
private readonly options: SandboxOptions; | ||
private readonly context: any = {}; | ||
|
||
constructor(entrypoint: string, options: SandboxOptions = {}) { | ||
this.entrypoint = entrypoint; | ||
this.options = options; | ||
this.context = this.createContext(); | ||
this.createBundlePromise = this.createBundle(); | ||
} | ||
|
||
private createContext() { | ||
const sandboxProcess = { | ||
...process, | ||
|
||
// override process.exit to throw an exception instead of exiting the process | ||
exit: (exitCode: number) => { | ||
throw new Error("process.exit() was called with exit code " + exitCode); | ||
}, | ||
|
||
env: this.options.env, | ||
}; | ||
|
||
const sandboxConsole: any = {}; | ||
const levels = ["debug", "info", "log", "warn", "error"]; | ||
for (const level of levels) { | ||
sandboxConsole[level] = (...args: any[]) => { | ||
const message = util.format(...args); | ||
this.options.log?.(false, level, message); | ||
// also log to stderr if DEBUG is set | ||
if (process.env.DEBUG) { | ||
console.error(message); | ||
} | ||
}; | ||
} | ||
|
||
const ctx: any = {}; | ||
|
||
// create a copy of all the globals from our current context. | ||
for (const k of Object.getOwnPropertyNames(global)) { | ||
try { | ||
ctx[k] = (global as any)[k]; | ||
} catch { | ||
// ignore unresolvable globals (see https://github.com/winglang/wing/pull/1923) | ||
} | ||
} | ||
|
||
// append the user's context | ||
for (const [k, v] of Object.entries(this.options.context ?? {})) { | ||
ctx[k] = v; | ||
} | ||
|
||
const context = vm.createContext({ | ||
...ctx, | ||
process: sandboxProcess, | ||
console: sandboxConsole, | ||
exports: {}, | ||
require, // to support requiring node.js sdk modules (others will be bundled) | ||
}); | ||
|
||
// emit an explicit error when trying to access `__dirname` and `__filename` because we cannot | ||
// resolve these when bundling (this is true both for simulator and the cloud since we are | ||
// bundling there as well). | ||
const forbidGlobal = (name: string) => { | ||
Object.defineProperty(context, name, { | ||
get: () => { | ||
throw new Error( | ||
`${name} cannot be used within bundled cloud functions` | ||
); | ||
}, | ||
}); | ||
}; | ||
|
||
forbidGlobal("__dirname"); | ||
forbidGlobal("__filename"); | ||
|
||
return context; | ||
} | ||
|
||
private async createBundle() { | ||
// load bundle into context on first run | ||
const workdir = await mkdtemp(path.join(tmpdir(), "wing-bundles-")); | ||
const bundle = createBundle(this.entrypoint, [], workdir); | ||
this.entrypoint = bundle.entrypointPath; | ||
|
||
this.code = await readFile(this.entrypoint, "utf-8"); | ||
|
||
if (process.env.DEBUG) { | ||
const bundleSize = Buffer.byteLength(this.code, "utf-8"); | ||
this.options.log?.(true, "log", `Bundled code (${bundleSize} bytes).`); | ||
} | ||
} | ||
|
||
public async call(fn: string, ...args: any[]): Promise<any> { | ||
// wait for the bundle to finish creation | ||
await this.createBundlePromise; | ||
|
||
if (!this.code) { | ||
throw new Error("Bundle not created yet - please report this as a bug"); | ||
} | ||
|
||
// this will add stuff to the "exports" object within our context | ||
vm.runInContext(this.code!, this.context, { | ||
filename: this.entrypoint, | ||
}); | ||
|
||
return new Promise(($resolve, $reject) => { | ||
const cleanup = () => { | ||
delete this.context.$resolve; | ||
delete this.context.$reject; | ||
}; | ||
|
||
this.context.$resolve = (value: any) => { | ||
cleanup(); | ||
$resolve(value); | ||
}; | ||
|
||
this.context.$reject = (reason?: any) => { | ||
cleanup(); | ||
$reject(reason); | ||
}; | ||
|
||
const code = `exports.${fn}(${args | ||
.map((arg) => JSON.stringify(arg)) | ||
.join(",")}).then($resolve).catch($reject);`; | ||
vm.runInContext(code, this.context, { | ||
filename: this.entrypoint, | ||
timeout: this.options.timeout, | ||
}); | ||
}); | ||
} | ||
} |
Oops, something went wrong.