Skip to content

computer: Add remoteExec(), a Vite plugin that carries the remote-exec builds - #316

Open
aron-cf wants to merge 2 commits into
mainfrom
remote-exec-vite
Open

aron-cf wants to merge 2 commits into
mainfrom
remote-exec-vite

Conversation

@aron-cf

@aron-cf aron-cf commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

codemode:exec copies the remote-exec binary into the container on first use, so the Worker has to carry it. Getting it there meant downloading the build, checking its hash, and then either importing it, which holds 1.5 MB in memory, or uploading it with wrangler.jsonc module rules, which the Cloudflare Vite plugin ignores. The example did the first half with a shell script and the second with those rules.

remoteExec() from @cloudflare/computer/vite does all of it. It downloads each build from the @cloudflare/computer GitHub release matching the installed package, checks it against the release's SHA256SUMS, caches it in Vite's cache directory, and serves it as a virtual module:

// vite.config.ts
export default defineConfig({ plugins: [cloudflare(), remoteExec()] });

// src/index.ts
import binaries from "computer:remote-exec";
createExecModule({ binaries, target, fs });

vite build emits each build as remote-exec-linux-<arch>.bin next to the Worker without importing it. Wrangler uploads .bin files as data modules, which workerd exposes under /bundle, and the module streams them from there with fileRemoteExec(), so a deployed Worker never holds the build in memory. vite dev only puts the entry under /bundle, so in development the module imports the build instead. The plugin takes arch (x86_64 by default, which is what Cloudflare runs), version, and builds, a directory of local builds to use instead of downloading.

The example now runs under vite dev, with remoteExec() taking the repository's own builds from artifacts/remote-exec when they exist, as they do in CI. The fetch script, bin/ and the wrangler.jsonc rules are gone, and the end-to-end tests start the example with Vite's createServer() instead of spawning wrangler dev. This needs @cloudflare/vite-plugin 1.68: the workerd in 1.63 drops a container's exec() streams sent over RPC before they finish, which hangs every command. Every workspace now asks for 1.68, which also emits source maps, so the computerBundle() build test now ignores .map files.

The plugin's tests serve a fake release locally, then check the download, the checksum and cache handling, and run the result in both vite preview and vite dev. To run the example end to end with Docker:

npm run build
npm run test:e2e --workspace @example/computer-remote-exec

docs/20_remote_exec.md, the package README and the example README describe the plugin, and the unreleased remote-exec changeset now mentions it.

Until a release ships with the remote-exec binaries, remoteExec() with no options fails, because the 0.10.0 release doesn't have them. Its error says to pass version or builds. The example works from a checkout once remote-exec has been built locally.


Devin Review

…c builds

Getting remote-exec into a Worker meant downloading it,
checking it, and either importing it, which holds it in memory, or
uploading it with wrangler.jsonc rules, which the Cloudflare Vite
plugin ignores.

remoteExec() from @cloudflare/computer/vite does it. It downloads
each build from the @cloudflare/computer release matching the
installed package, checks it against the release's SHA256SUMS, and
caches it in Vite's cache directory. It serves them as
`computer:remote-exec`:

    import binaries from "computer:remote-exec";
    createExecModule({ binaries, ... });

vite build emits each build as remote-exec-linux-<arch>.bin beside
the Worker. Nothing imports it. The output wrangler.json has
no_bundle, so wrangler deploy uploads every file its default module
rules match, and .bin files go up as Data modules, which workerd
shows under /bundle. The module streams them from there with
fileRemoteExec(), so the Worker never holds a build in memory. vite
dev puts nothing but its entry under /bundle, so there the module
imports the builds instead.

It takes `arch` (x86_64 by default), `version` and `builds`, a
directory of local builds. The tests serve a release locally, and
run the result in vite preview and vite dev.
…build

The example now builds with Vite, and remoteExec() gets the
remote-exec build: from the release by default, or from
artifacts/remote-exec when the repository has its own, as in CI.
scripts/fetch-remote-exec.sh, bin/ and the wrangler.jsonc rules go.
The tests start the example with Vite's createServer() rather than
wrangler dev.

It needs @cloudflare/vite-plugin 1.68.0. 1.63's workerd drops a
container's exec() streams sent over RPC before they finish, which
hung every command; that's why wrangler moved to 4.158.0. Every
workspace now asks for 1.68.0. It also emits each
chunk's source map, so computerBundle()'s build test counts chunks
without them.
@changeset-bot

changeset-bot Bot commented Nov 10, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 80034a7

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes changesets to release 5 packages
Name Type
@cloudflare/computer Minor
@cloudflare/dofs Minor
@cloudflare/computer-rpc Minor
@cloudflare/computerd Minor
@cloudflare/remote-exec Minor

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new

pkg-pr-new Bot commented Nov 10, 2026

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/@cloudflare/computer@316

commit: 80034a7

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 2 potential issues.

Devin Review

Comment on lines +119 to +123
async generateBundle() {
if (!carriers.has(this.environment.name)) return;
for (const { arch, path } of await prepare()) {
this.emitFile({ type: "asset", fileName: carried(arch), source: await readFile(path) });
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 Deployed Worker lacks its remote-exec build

On deployment, generateBundle emits an unreferenced binary that Wrangler does not upload without additional-module discovery. fileRemoteExec then cannot open its /bundle path, so remote commands fail.

Learn more

Wrangler uploads an additional module only when it is imported or its path matches an enabled additional-module rule. The build branch creates a virtual module containing only a path string, so it does not import the emitted binary. The example removes find_additional_modules, and its emitted configuration has no rule for .bin files; preview can see Vite output assets without proving Wrangler deploy uploads them. The runtime then passes /bundle/remote-exec-linux-x86_64.bin to fileRemoteExec, which opens that path on the first command.

Example: Build and deploy the example Worker, then call /s/demo/run with a program invoking exec(["true"]). The binary exists under dist but is absent from the deployed module bundle, so its file read fails instead of running true.

Recommended fix: Arrange for the built Wrangler upload to include the emitted binary as a named Data module under its fixed path, without importing its bytes into the Worker. Add a deployment-level test that inspects uploaded modules or runs an actual deployed Worker, rather than testing only vite preview.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +159 to +161
const partial = `${path}.${process.pid}.partial`;
await writeFile(partial, bytes, { mode: 0o755 });
await rename(partial, path);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Concurrent builds collide on cached binary

When two builds share a cache and process, writeAtomically gives both downloads the same partial filename. One rename can remove the other's file or publish incomplete bytes, failing a build or corrupting its cached binary.

Learn more

Each plugin instance memoizes its own prepare() call, but two Vite builders can create separate plugin instances while sharing a cache directory. Both can see that the same release binary is missing, download it, and write to the identical ${path}.${process.pid}.partial name. writeFile truncates the shared partial file, and the first rename removes it from the path before the second rename. The second build then fails; overlapping writes can also leave a cached binary that does not match the validated bytes.

Example: In one Node process, start two Vite builds of the same project with an empty cache and the same release. Both target remote-exec-linux-x86_64.bin.<pid>.partial; if the first renames before the second, the second gets ENOENT.

Recommended fix: Use a unique partial filename per write, such as a random suffix, and clean it up on failure. Keep the final rename atomic and consider how concurrent writers to the same release path interact.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant