Skip to content

Commit

Permalink
fix: cleanup temporary directory after shutting down workerd
Browse files Browse the repository at this point in the history
  • Loading branch information
mrbbot committed Nov 8, 2023
1 parent 35be459 commit 691e1cf
Show file tree
Hide file tree
Showing 3 changed files with 78 additions and 10 deletions.
12 changes: 12 additions & 0 deletions .changeset/tricky-masks-accept.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"miniflare": patch
---

fix: cleanup temporary directory after shutting down `workerd`

Previously on exit, Miniflare would attempt to remove its temporary directory
before shutting down `workerd`. This could lead to `EBUSY` errors on Windows.
This change ensures we shutdown `workerd` before removing the directory.
Since we can only clean up on a best effort basis when exiting, it also catches
any errors thrown when removing the directory, in case the runtime doesn't
shutdown fast enough.
24 changes: 15 additions & 9 deletions packages/miniflare/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ export class Miniflare {
#log: Log;

readonly #runtime?: Runtime;
readonly #removeRuntimeExitHook?: () => void;
readonly #removeExitHook?: () => void;
#runtimeEntryURL?: URL;
#socketPorts?: SocketPorts;
#runtimeDispatcher?: Dispatcher;
Expand All @@ -573,7 +573,6 @@ export class Miniflare {
// Object storage. Note this may not exist, it's up to the consumers to
// create this if needed. Deleted on `dispose()`.
readonly #tmpPath: string;
readonly #removeTmpPathExitHook: () => void;

// Mutual exclusion lock for runtime operations (i.e. initialisation and
// updating config). This essentially puts initialisation and future updates
Expand Down Expand Up @@ -637,13 +636,21 @@ export class Miniflare {
os.tmpdir(),
`miniflare-${crypto.randomBytes(16).toString("hex")}`
);
this.#removeTmpPathExitHook = exitHook(() => {
fs.rmSync(this.#tmpPath, { force: true, recursive: true });
});

// Setup runtime
this.#runtime = new Runtime();
this.#removeRuntimeExitHook = exitHook(() => void this.#runtime?.dispose());
this.#removeExitHook = exitHook(() => {
void this.#runtime?.dispose();
try {
fs.rmSync(this.#tmpPath, { force: true, recursive: true });
} catch (e) {
// `rmSync` may fail on Windows with `EBUSY` if `workerd` is still
// running. `Runtime#dispose()` should kill the runtime immediately.
// `exitHook`s must be synchronous, so we can only clean up on a best
// effort basis.
this.#log.debug(`Unable to remove temporary directory: ${String(e)}`);
}
});

this.#disposeController = new AbortController();
this.#runtimeMutex = new Mutex();
Expand Down Expand Up @@ -1506,9 +1513,8 @@ export class Miniflare {
try {
await this.#waitForReady(/* disposing */ true);
} finally {
// Remove exit hooks, we're cleaning up what they would've cleaned up now
this.#removeTmpPathExitHook();
this.#removeRuntimeExitHook?.();
// Remove exit hook, we're cleaning up what they would've cleaned up now
this.#removeExitHook?.();

// Cleanup as much as possible even if `#init()` threw
await this.#proxyClient?.dispose();
Expand Down
52 changes: 51 additions & 1 deletion packages/miniflare/test/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
// noinspection TypeScriptValidateJSTypes

import assert from "assert";
import childProcess from "child_process";
import { once } from "events";
import fs from "fs/promises";
import http from "http";
import { AddressInfo } from "net";
import path from "path";
import { Writable } from "stream";
import { json } from "stream/consumers";
import { json, text } from "stream/consumers";
import util from "util";
import {
D1Database,
Expand Down Expand Up @@ -1088,6 +1090,54 @@ unixSerialTest(
}
);

test("Miniflare: exits cleanly", async (t) => {
const miniflarePath = require.resolve("miniflare");
const result = childProcess.spawn(
process.execPath,
[
"--no-warnings", // Hide experimental warnings
"-e",
`
const { Miniflare, Log, LogLevel } = require(${JSON.stringify(
miniflarePath
)});
const mf = new Miniflare({
verbose: true,
modules: true,
script: \`export default {
fetch() {
return new Response("body");
}
}\`
});
(async () => {
const res = await mf.dispatchFetch("http://placeholder/");
const text = await res.text();
process.send(text);
process.disconnect();
})();
`,
],
{
stdio: [/* in */ "ignore", /* out */ "pipe", /* error */ "pipe", "ipc"],
}
);

// Make sure workerd started
const [message] = await once(result, "message");
t.is(message, "body");

// Check exit doesn't output anything
const closePromise = once(result, "close");
result.kill("SIGINT");
assert(result.stdout !== null && result.stderr !== null);
const stdout = await text(result.stdout);
const stderr = await text(result.stderr);
await closePromise;
t.is(stdout, "");
t.is(stderr, "");
});

test("Miniflare: allows the use of unsafe eval bindings", async (t) => {
const log = new TestLog(t);

Expand Down

0 comments on commit 691e1cf

Please sign in to comment.