feat: disposable Deno resources (#20845)

This commit implements Symbol.dispose and Symbol.asyncDispose for
the relevant resources.

Closes #20839

---------

Signed-off-by: Bartek Iwańczuk <biwanczuk@gmail.com>
Co-authored-by: Bartek Iwańczuk <biwanczuk@gmail.com>
This commit is contained in:
Luca Casonato 2023-11-01 20:26:12 +01:00 committed by GitHub
parent 1d19b1011b
commit d42f154312
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 342 additions and 35 deletions

View file

@ -256,6 +256,46 @@ Deno.test(
}, },
); );
Deno.test(
{ permissions: { run: true, read: true } },
// deno lint bug, see https://github.com/denoland/deno_lint/issues/1206
// deno-lint-ignore require-await
async function childProcessExplicitResourceManagement() {
let dead = undefined;
{
const command = new Deno.Command(Deno.execPath(), {
args: ["eval", "setTimeout(() => {}, 10000)"],
stdout: "null",
stderr: "null",
});
await using child = command.spawn();
child.status.then(({ signal }) => {
dead = signal;
});
}
if (Deno.build.os == "windows") {
assertEquals(dead, null);
} else {
assertEquals(dead, "SIGTERM");
}
},
);
Deno.test(
{ permissions: { run: true, read: true } },
async function childProcessExplicitResourceManagementManualClose() {
const command = new Deno.Command(Deno.execPath(), {
args: ["eval", "setTimeout(() => {}, 10000)"],
stdout: "null",
stderr: "null",
});
await using child = command.spawn();
child.kill("SIGTERM");
await child.status;
},
);
Deno.test( Deno.test(
{ permissions: { run: true, read: true } }, { permissions: { run: true, read: true } },
async function commandKillFailed() { async function commandKillFailed() {

View file

@ -824,3 +824,30 @@ Deno.test(
assertEquals(res, "hello \uFFFD"); assertEquals(res, "hello \uFFFD");
}, },
); );
Deno.test(
{ permissions: { read: true } },
async function fsFileExplicitResourceManagement() {
let file2: Deno.FsFile;
{
using file = await Deno.open("cli/tests/testdata/assets/hello.txt");
file2 = file;
const stat = file.statSync();
assert(stat.isFile);
}
assertThrows(() => file2.statSync(), Deno.errors.BadResource);
},
);
Deno.test(
{ permissions: { read: true } },
async function fsFileExplicitResourceManagementManualClose() {
using file = await Deno.open("cli/tests/testdata/assets/hello.txt");
file.close();
assertThrows(() => file.statSync(), Deno.errors.BadResource); // definitely closed
// calling [Symbol.dispose] after manual close is a no-op
},
);

View file

@ -107,3 +107,33 @@ Deno.test(
assertEquals(events, []); assertEquals(events, []);
}, },
); );
Deno.test(
{ permissions: { read: true, write: true } },
async function watchFsExplicitResourceManagement() {
let res;
{
const testDir = await makeTempDir();
using iter = Deno.watchFs(testDir);
res = iter[Symbol.asyncIterator]().next();
}
const { done } = await res;
assert(done);
},
);
Deno.test(
{ permissions: { read: true, write: true } },
async function watchFsExplicitResourceManagementManualClose() {
const testDir = await makeTempDir();
using iter = Deno.watchFs(testDir);
const res = iter[Symbol.asyncIterator]().next();
iter.close();
const { done } = await res;
assert(done);
},
);

View file

@ -2817,6 +2817,24 @@ Deno.test({
}, },
}); });
Deno.test(
async function httpConnExplicitResourceManagement() {
let promise;
{
const listen = Deno.listen({ port: listenPort });
promise = fetch(`http://localhost:${listenPort}/`).catch(() => null);
const serverConn = await listen.accept();
listen.close();
using _httpConn = Deno.serveHttp(serverConn);
}
const response = await promise;
assertEquals(response, null);
},
);
function chunkedBodyReader(h: Headers, r: BufReader): Deno.Reader { function chunkedBodyReader(h: Headers, r: BufReader): Deno.Reader {
// Based on https://tools.ietf.org/html/rfc2616#section-19.4.6 // Based on https://tools.ietf.org/html/rfc2616#section-19.4.6
const tp = new TextProtoReader(r); const tp = new TextProtoReader(r);

View file

@ -2100,3 +2100,30 @@ Deno.test({
db.close(); db.close();
}, },
}); });
Deno.test(
{ permissions: { read: true } },
async function kvExplicitResourceManagement() {
let kv2: Deno.Kv;
{
using kv = await Deno.openKv(":memory:");
kv2 = kv;
const res = await kv.get(["a"]);
assertEquals(res.versionstamp, null);
}
await assertRejects(() => kv2.get(["a"]), Deno.errors.BadResource);
},
);
Deno.test(
{ permissions: { read: true } },
async function kvExplicitResourceManagementManualClose() {
using kv = await Deno.openKv(":memory:");
kv.close();
await assertRejects(() => kv.get(["a"]), Deno.errors.BadResource);
// calling [Symbol.dispose] after manual close is a no-op
},
);

View file

@ -1242,3 +1242,34 @@ Deno.test({
const listener = Deno.listen({ hostname: "localhost", port: "0" }); const listener = Deno.listen({ hostname: "localhost", port: "0" });
listener.close(); listener.close();
}); });
Deno.test(
{ permissions: { net: true } },
async function listenerExplicitResourceManagement() {
let done: Promise<Deno.errors.BadResource>;
{
using listener = Deno.listen({ port: listenPort });
done = assertRejects(
() => listener.accept(),
Deno.errors.BadResource,
);
}
await done;
},
);
Deno.test(
{ permissions: { net: true } },
async function listenerExplicitResourceManagementManualClose() {
using listener = Deno.listen({ port: listenPort });
listener.close();
await assertRejects( // definitely closed
() => listener.accept(),
Deno.errors.BadResource,
);
// calling [Symbol.dispose] after manual close is a no-op
},
);

View file

@ -48,7 +48,12 @@ function onListen<T>(
async function makeServer( async function makeServer(
handler: (req: Request) => Response | Promise<Response>, handler: (req: Request) => Response | Promise<Response>,
): Promise< ): Promise<
{ finished: Promise<void>; abort: () => void; shutdown: () => Promise<void> } {
finished: Promise<void>;
abort: () => void;
shutdown: () => Promise<void>;
[Symbol.asyncDispose](): PromiseLike<void>;
}
> { > {
const ac = new AbortController(); const ac = new AbortController();
const listeningPromise = deferred(); const listeningPromise = deferred();
@ -69,6 +74,9 @@ async function makeServer(
async shutdown() { async shutdown() {
await server.shutdown(); await server.shutdown();
}, },
[Symbol.asyncDispose]() {
return server[Symbol.asyncDispose]();
},
}; };
} }
@ -296,6 +304,42 @@ Deno.test(
}, },
); );
Deno.test(
{ permissions: { net: true, write: true, read: true } },
async function httpServerExplicitResourceManagement() {
let dataPromise;
{
await using _server = await makeServer(async (_req) => {
return new Response((await makeTempFile(1024 * 1024)).readable);
});
const resp = await fetch(`http://localhost:${servePort}`);
dataPromise = resp.arrayBuffer();
}
assertEquals((await dataPromise).byteLength, 1048576);
},
);
Deno.test(
{ permissions: { net: true, write: true, read: true } },
async function httpServerExplicitResourceManagementManualClose() {
await using server = await makeServer(async (_req) => {
return new Response((await makeTempFile(1024 * 1024)).readable);
});
const resp = await fetch(`http://localhost:${servePort}`);
const [_, data] = await Promise.all([
server.shutdown(),
resp.arrayBuffer(),
]);
assertEquals(data.byteLength, 1048576);
},
);
Deno.test( Deno.test(
{ permissions: { read: true, run: true } }, { permissions: { read: true, run: true } },
async function httpServerUnref() { async function httpServerUnref() {

View file

@ -1138,6 +1138,7 @@ delete Object.prototype.__proto__;
`${ASSETS_URL_PREFIX}${specifier}`, `${ASSETS_URL_PREFIX}${specifier}`,
ts.ScriptTarget.ESNext, ts.ScriptTarget.ESNext,
), ),
`failed to load '${ASSETS_URL_PREFIX}${specifier}'`,
); );
} }
// this helps ensure as much as possible is in memory that is re-usable // this helps ensure as much as possible is in memory that is re-usable
@ -1148,7 +1149,10 @@ delete Object.prototype.__proto__;
options: SNAPSHOT_COMPILE_OPTIONS, options: SNAPSHOT_COMPILE_OPTIONS,
host, host,
}); });
assert(ts.getPreEmitDiagnostics(TS_SNAPSHOT_PROGRAM).length === 0); assert(
ts.getPreEmitDiagnostics(TS_SNAPSHOT_PROGRAM).length === 0,
"lib.d.ts files have errors",
);
// remove this now that we don't need it anymore for warming up tsc // remove this now that we don't need it anymore for warming up tsc
sourceFileCache.delete(buildSpecifier); sourceFileCache.delete(buildSpecifier);

View file

@ -2,6 +2,7 @@
/// <reference no-default-lib="true" /> /// <reference no-default-lib="true" />
/// <reference lib="esnext" /> /// <reference lib="esnext" />
/// <reference lib="esnext.disposable" />
/// <reference lib="deno.net" /> /// <reference lib="deno.net" />
/** Deno provides extra properties on `import.meta`. These are included here /** Deno provides extra properties on `import.meta`. These are included here
@ -2177,7 +2178,8 @@ declare namespace Deno {
WriterSync, WriterSync,
Seeker, Seeker,
SeekerSync, SeekerSync,
Closer { Closer,
Disposable {
/** The resource ID associated with the file instance. The resource ID /** The resource ID associated with the file instance. The resource ID
* should be considered an opaque reference to resource. */ * should be considered an opaque reference to resource. */
readonly rid: number; readonly rid: number;
@ -2451,6 +2453,8 @@ declare namespace Deno {
* ``` * ```
*/ */
close(): void; close(): void;
[Symbol.dispose](): void;
} }
/** /**
@ -3831,7 +3835,7 @@ declare namespace Deno {
* *
* @category File System * @category File System
*/ */
export interface FsWatcher extends AsyncIterable<FsEvent> { export interface FsWatcher extends AsyncIterable<FsEvent>, Disposable {
/** The resource id. */ /** The resource id. */
readonly rid: number; readonly rid: number;
/** Stops watching the file system and closes the watcher resource. */ /** Stops watching the file system and closes the watcher resource. */
@ -4284,7 +4288,7 @@ declare namespace Deno {
* *
* @category Sub Process * @category Sub Process
*/ */
export class ChildProcess { export class ChildProcess implements Disposable {
get stdin(): WritableStream<Uint8Array>; get stdin(): WritableStream<Uint8Array>;
get stdout(): ReadableStream<Uint8Array>; get stdout(): ReadableStream<Uint8Array>;
get stderr(): ReadableStream<Uint8Array>; get stderr(): ReadableStream<Uint8Array>;
@ -4307,6 +4311,8 @@ declare namespace Deno {
/** Ensure that the status of the child process does not block the Deno /** Ensure that the status of the child process does not block the Deno
* process from exiting. */ * process from exiting. */
unref(): void; unref(): void;
[Symbol.dispose](): void;
} }
/** /**
@ -5258,7 +5264,7 @@ declare namespace Deno {
* requests on the HTTP server connection. * requests on the HTTP server connection.
* *
* @category HTTP Server */ * @category HTTP Server */
export interface HttpConn extends AsyncIterable<RequestEvent> { export interface HttpConn extends AsyncIterable<RequestEvent>, Disposable {
/** The resource ID associated with this connection. Generally users do not /** The resource ID associated with this connection. Generally users do not
* need to be aware of this identifier. */ * need to be aware of this identifier. */
readonly rid: number; readonly rid: number;
@ -5911,7 +5917,7 @@ declare namespace Deno {
* *
* @category HTTP Server * @category HTTP Server
*/ */
export interface HttpServer { export interface HttpServer extends AsyncDisposable {
/** A promise that resolves once server finishes - eg. when aborted using /** A promise that resolves once server finishes - eg. when aborted using
* the signal passed to {@linkcode ServeOptions.signal}. * the signal passed to {@linkcode ServeOptions.signal}.
*/ */

View file

@ -1749,7 +1749,7 @@ declare namespace Deno {
* *
* @category KV * @category KV
*/ */
export class Kv { export class Kv implements Disposable {
/** /**
* Retrieve the value and versionstamp for the given key from the database * Retrieve the value and versionstamp for the given key from the database
* in the form of a {@linkcode Deno.KvEntryMaybe}. If no value exists for * in the form of a {@linkcode Deno.KvEntryMaybe}. If no value exists for
@ -1945,6 +1945,8 @@ declare namespace Deno {
* operations immediately. * operations immediately.
*/ */
close(): void; close(): void;
[Symbol.dispose](): void;
} }
/** **UNSTABLE**: New API, yet to be vetted. /** **UNSTABLE**: New API, yet to be vetted.

View file

@ -34,7 +34,7 @@ import {
ReadableStreamPrototype, ReadableStreamPrototype,
writableStreamForRid, writableStreamForRid,
} from "ext:deno_web/06_streams.js"; } from "ext:deno_web/06_streams.js";
import { pathFromURL } from "ext:deno_web/00_infra.js"; import { pathFromURL, SymbolDispose } from "ext:deno_web/00_infra.js";
function chmodSync(path, mode) { function chmodSync(path, mode) {
ops.op_fs_chmod_sync(pathFromURL(path), mode); ops.op_fs_chmod_sync(pathFromURL(path), mode);
@ -669,6 +669,10 @@ class FsFile {
} }
return this.#writable; return this.#writable;
} }
[SymbolDispose]() {
core.tryClose(this.rid);
}
} }
function checkOpenOptions(options) { function checkOpenOptions(options) {

View file

@ -36,6 +36,7 @@ import {
} from "ext:deno_web/06_streams.js"; } from "ext:deno_web/06_streams.js";
import { listen, listenOptionApiName, TcpConn } from "ext:deno_net/01_net.js"; import { listen, listenOptionApiName, TcpConn } from "ext:deno_net/01_net.js";
import { listenTls } from "ext:deno_net/02_tls.js"; import { listenTls } from "ext:deno_net/02_tls.js";
import { SymbolAsyncDispose } from "ext:deno_web/00_infra.js";
const { const {
ArrayPrototypePush, ArrayPrototypePush,
ObjectHasOwn, ObjectHasOwn,
@ -343,6 +344,7 @@ class CallbackContext {
fallbackHost; fallbackHost;
serverRid; serverRid;
closed; closed;
/** @type {Promise<void> | undefined} */
closing; closing;
listener; listener;
@ -671,22 +673,25 @@ function serveHttpOn(context, callback) {
PromisePrototypeCatch(callback(req), promiseErrorHandler); PromisePrototypeCatch(callback(req), promiseErrorHandler);
} }
if (!context.closed && !context.closing) { if (!context.closing && !context.closed) {
context.closed = true; context.closing = op_http_close(rid, false);
await op_http_close(rid, false);
context.close(); context.close();
} }
await context.closing;
context.close();
context.closed = true;
})(); })();
return { return {
finished, finished,
async shutdown() { async shutdown() {
if (!context.closed && !context.closing) { if (!context.closing && !context.closed) {
// Shut this HTTP server down gracefully // Shut this HTTP server down gracefully
context.closing = true; context.closing = op_http_close(context.serverRid, true);
await op_http_close(context.serverRid, true);
context.closed = true;
} }
await context.closing;
context.closed = true;
}, },
ref() { ref() {
ref = true; ref = true;
@ -700,6 +705,9 @@ function serveHttpOn(context, callback) {
core.unrefOp(currentPromise[promiseIdSymbol]); core.unrefOp(currentPromise[promiseIdSymbol]);
} }
}, },
[SymbolAsyncDispose]() {
return this.shutdown();
},
}; };
} }

View file

@ -44,6 +44,7 @@ import {
ReadableStreamPrototype, ReadableStreamPrototype,
} from "ext:deno_web/06_streams.js"; } from "ext:deno_web/06_streams.js";
import { serve } from "ext:deno_http/00_serve.js"; import { serve } from "ext:deno_http/00_serve.js";
import { SymbolDispose } from "ext:deno_web/00_infra.js";
const { const {
ArrayPrototypeIncludes, ArrayPrototypeIncludes,
ArrayPrototypeMap, ArrayPrototypeMap,
@ -69,6 +70,9 @@ const {
const connErrorSymbol = Symbol("connError"); const connErrorSymbol = Symbol("connError");
const _deferred = Symbol("upgradeHttpDeferred"); const _deferred = Symbol("upgradeHttpDeferred");
/** @type {(self: HttpConn, rid: number) => boolean} */
let deleteManagedResource;
class HttpConn { class HttpConn {
#rid = 0; #rid = 0;
#closed = false; #closed = false;
@ -79,7 +83,12 @@ class HttpConn {
// that were created during lifecycle of this request. // that were created during lifecycle of this request.
// When the connection is closed these resources should be closed // When the connection is closed these resources should be closed
// as well. // as well.
managedResources = new SafeSet(); #managedResources = new SafeSet();
static {
deleteManagedResource = (self, rid) =>
SetPrototypeDelete(self.#managedResources, rid);
}
constructor(rid, remoteAddr, localAddr) { constructor(rid, remoteAddr, localAddr) {
this.#rid = rid; this.#rid = rid;
@ -123,7 +132,7 @@ class HttpConn {
} }
const { 0: streamRid, 1: method, 2: url } = nextRequest; const { 0: streamRid, 1: method, 2: url } = nextRequest;
SetPrototypeAdd(this.managedResources, streamRid); SetPrototypeAdd(this.#managedResources, streamRid);
/** @type {ReadableStream<Uint8Array> | undefined} */ /** @type {ReadableStream<Uint8Array> | undefined} */
let body = null; let body = null;
@ -167,13 +176,21 @@ class HttpConn {
if (!this.#closed) { if (!this.#closed) {
this.#closed = true; this.#closed = true;
core.close(this.#rid); core.close(this.#rid);
for (const rid of new SafeSetIterator(this.managedResources)) { for (const rid of new SafeSetIterator(this.#managedResources)) {
SetPrototypeDelete(this.managedResources, rid); SetPrototypeDelete(this.#managedResources, rid);
core.close(rid); core.close(rid);
} }
} }
} }
[SymbolDispose]() {
core.tryClose(this.#rid);
for (const rid of new SafeSetIterator(this.#managedResources)) {
SetPrototypeDelete(this.#managedResources, rid);
core.tryClose(rid);
}
}
[SymbolAsyncIterator]() { [SymbolAsyncIterator]() {
// deno-lint-ignore no-this-alias // deno-lint-ignore no-this-alias
const httpConn = this; const httpConn = this;
@ -395,7 +412,7 @@ function createRespondWith(
abortController.abort(error); abortController.abort(error);
throw error; throw error;
} finally { } finally {
if (SetPrototypeDelete(httpConn.managedResources, streamRid)) { if (deleteManagedResource(httpConn, streamRid)) {
core.close(streamRid); core.close(streamRid);
} }
} }

View file

@ -12,6 +12,7 @@ const {
SymbolToStringTag, SymbolToStringTag,
Uint8ArrayPrototype, Uint8ArrayPrototype,
} = globalThis.__bootstrap.primordials; } = globalThis.__bootstrap.primordials;
import { SymbolDispose } from "ext:deno_web/00_infra.js";
const core = Deno.core; const core = Deno.core;
const ops = core.ops; const ops = core.ops;
@ -299,6 +300,10 @@ class Kv {
close() { close() {
core.close(this.#rid); core.close(this.#rid);
} }
[SymbolDispose]() {
core.tryClose(this.#rid);
}
} }
class AtomicOperation { class AtomicOperation {

View file

@ -66,7 +66,7 @@ const MAX_TOTAL_MUTATION_SIZE_BYTES: usize = 800 * 1024;
const MAX_TOTAL_KEY_SIZE_BYTES: usize = 80 * 1024; const MAX_TOTAL_KEY_SIZE_BYTES: usize = 80 * 1024;
deno_core::extension!(deno_kv, deno_core::extension!(deno_kv,
deps = [ deno_console ], deps = [ deno_console, deno_web ],
parameters = [ DBH: DatabaseHandler ], parameters = [ DBH: DatabaseHandler ],
ops = [ ops = [
op_kv_database_open<DBH>, op_kv_database_open<DBH>,

View file

@ -9,6 +9,8 @@ import {
writableStreamForRid, writableStreamForRid,
} from "ext:deno_web/06_streams.js"; } from "ext:deno_web/06_streams.js";
import * as abortSignal from "ext:deno_web/03_abort_signal.js"; import * as abortSignal from "ext:deno_web/03_abort_signal.js";
import { SymbolDispose } from "ext:deno_web/00_infra.js";
const primordials = globalThis.__bootstrap.primordials; const primordials = globalThis.__bootstrap.primordials;
const { const {
ArrayPrototypeFilter, ArrayPrototypeFilter,
@ -160,6 +162,10 @@ class Conn {
(id) => core.unrefOp(id), (id) => core.unrefOp(id),
); );
} }
[SymbolDispose]() {
core.tryClose(this.#rid);
}
} }
class TcpConn extends Conn { class TcpConn extends Conn {
@ -249,6 +255,10 @@ class Listener {
core.close(this.rid); core.close(this.rid);
} }
[SymbolDispose]() {
core.tryClose(this.#rid);
}
[SymbolAsyncIterator]() { [SymbolAsyncIterator]() {
return this; return this;
} }

View file

@ -2,6 +2,7 @@
/// <reference no-default-lib="true" /> /// <reference no-default-lib="true" />
/// <reference lib="esnext" /> /// <reference lib="esnext" />
/// <reference lib="esnext.disposable" />
declare namespace Deno { declare namespace Deno {
/** @category Network */ /** @category Network */
@ -24,7 +25,8 @@ declare namespace Deno {
* *
* @category Network * @category Network
*/ */
export interface Listener<T extends Conn = Conn> extends AsyncIterable<T> { export interface Listener<T extends Conn = Conn>
extends AsyncIterable<T>, Disposable {
/** Waits for and resolves to the next connection to the `Listener`. */ /** Waits for and resolves to the next connection to the `Listener`. */
accept(): Promise<T>; accept(): Promise<T>;
/** Close closes the listener. Any pending accept promises will be rejected /** Close closes the listener. Any pending accept promises will be rejected
@ -57,7 +59,7 @@ declare namespace Deno {
export type TlsListener = Listener<TlsConn>; export type TlsListener = Listener<TlsConn>;
/** @category Network */ /** @category Network */
export interface Conn extends Reader, Writer, Closer { export interface Conn extends Reader, Writer, Closer, Disposable {
/** The local address of the connection. */ /** The local address of the connection. */
readonly localAddr: Addr; readonly localAddr: Addr;
/** The remote address of the connection. */ /** The remote address of the connection. */

View file

@ -32,6 +32,7 @@ const {
StringPrototypeSubstring, StringPrototypeSubstring,
StringPrototypeToLowerCase, StringPrototypeToLowerCase,
StringPrototypeToUpperCase, StringPrototypeToUpperCase,
Symbol,
TypeError, TypeError,
} = primordials; } = primordials;
import { URLPrototype } from "ext:deno_url/00_url.js"; import { URLPrototype } from "ext:deno_url/00_url.js";
@ -458,6 +459,12 @@ function pathFromURL(pathOrUrl) {
// it in unit tests // it in unit tests
internals.pathFromURL = pathFromURL; internals.pathFromURL = pathFromURL;
// deno-lint-ignore prefer-primordials
export const SymbolDispose = Symbol.dispose ?? Symbol("Symbol.dispose");
// deno-lint-ignore prefer-primordials
export const SymbolAsyncDispose = Symbol.asyncDispose ??
Symbol("Symbol.asyncDispose");
export { export {
ASCII_ALPHA, ASCII_ALPHA,
ASCII_ALPHANUMERIC, ASCII_ALPHANUMERIC,

View file

@ -9,6 +9,8 @@ const {
PromiseResolve, PromiseResolve,
SymbolAsyncIterator, SymbolAsyncIterator,
} = primordials; } = primordials;
import { SymbolDispose } from "ext:deno_web/00_infra.js";
class FsWatcher { class FsWatcher {
#rid = 0; #rid = 0;
@ -51,6 +53,10 @@ class FsWatcher {
[SymbolAsyncIterator]() { [SymbolAsyncIterator]() {
return this; return this;
} }
[SymbolDispose]() {
core.tryClose(this.#rid);
}
} }
function watchFs( function watchFs(

View file

@ -18,7 +18,11 @@ const {
} = primordials; } = primordials;
import { FsFile } from "ext:deno_fs/30_fs.js"; import { FsFile } from "ext:deno_fs/30_fs.js";
import { readAll } from "ext:deno_io/12_io.js"; import { readAll } from "ext:deno_io/12_io.js";
import { assert, pathFromURL } from "ext:deno_web/00_infra.js"; import {
assert,
pathFromURL,
SymbolAsyncDispose,
} from "ext:deno_web/00_infra.js";
import * as abortSignal from "ext:deno_web/03_abort_signal.js"; import * as abortSignal from "ext:deno_web/03_abort_signal.js";
import { import {
readableStreamCollectIntoUint8Array, readableStreamCollectIntoUint8Array,
@ -201,7 +205,6 @@ class ChildProcess {
#rid; #rid;
#waitPromiseId; #waitPromiseId;
#waitComplete = false; #waitComplete = false;
#unrefed = false;
#pid; #pid;
get pid() { get pid() {
@ -216,7 +219,6 @@ class ChildProcess {
return this.#stdin; return this.#stdin;
} }
#stdoutRid;
#stdout = null; #stdout = null;
get stdout() { get stdout() {
if (this.#stdout == null) { if (this.#stdout == null) {
@ -225,7 +227,6 @@ class ChildProcess {
return this.#stdout; return this.#stdout;
} }
#stderrRid;
#stderr = null; #stderr = null;
get stderr() { get stderr() {
if (this.#stderr == null) { if (this.#stderr == null) {
@ -254,12 +255,10 @@ class ChildProcess {
} }
if (stdoutRid !== null) { if (stdoutRid !== null) {
this.#stdoutRid = stdoutRid;
this.#stdout = readableStreamForRidUnrefable(stdoutRid); this.#stdout = readableStreamForRidUnrefable(stdoutRid);
} }
if (stderrRid !== null) { if (stderrRid !== null) {
this.#stderrRid = stderrRid;
this.#stderr = readableStreamForRidUnrefable(stderrRid); this.#stderr = readableStreamForRidUnrefable(stderrRid);
} }
@ -324,15 +323,22 @@ class ChildProcess {
ops.op_spawn_kill(this.#rid, signo); ops.op_spawn_kill(this.#rid, signo);
} }
async [SymbolAsyncDispose]() {
try {
ops.op_spawn_kill(this.#rid, "SIGTERM");
} catch {
// ignore errors from killing the process (such as ESRCH or BadResource)
}
await this.#status;
}
ref() { ref() {
this.#unrefed = false;
core.refOp(this.#waitPromiseId); core.refOp(this.#waitPromiseId);
if (this.#stdout) readableStreamForRidUnrefableRef(this.#stdout); if (this.#stdout) readableStreamForRidUnrefableRef(this.#stdout);
if (this.#stderr) readableStreamForRidUnrefableRef(this.#stderr); if (this.#stderr) readableStreamForRidUnrefableRef(this.#stderr);
} }
unref() { unref() {
this.#unrefed = true;
core.unrefOp(this.#waitPromiseId); core.unrefOp(this.#waitPromiseId);
if (this.#stdout) readableStreamForRidUnrefableUnref(this.#stdout); if (this.#stdout) readableStreamForRidUnrefableUnref(this.#stdout);
if (this.#stderr) readableStreamForRidUnrefableUnref(this.#stderr); if (this.#stderr) readableStreamForRidUnrefableUnref(this.#stderr);

View file

@ -70,11 +70,24 @@ import {
windowOrWorkerGlobalScope, windowOrWorkerGlobalScope,
workerRuntimeGlobalProperties, workerRuntimeGlobalProperties,
} from "ext:runtime/98_global_scope.js"; } from "ext:runtime/98_global_scope.js";
import { SymbolAsyncDispose, SymbolDispose } from "ext:deno_web/00_infra.js";
// deno-lint-ignore prefer-primordials // deno-lint-ignore prefer-primordials
Symbol.dispose ??= Symbol("Symbol.dispose"); if (Symbol.dispose) throw "V8 supports Symbol.dispose now, no need to shim it!";
// deno-lint-ignore prefer-primordials ObjectDefineProperties(Symbol, {
Symbol.asyncDispose ??= Symbol("Symbol.asyncDispose"); dispose: {
value: SymbolDispose,
enumerable: false,
writable: false,
configurable: false,
},
asyncDispose: {
value: SymbolAsyncDispose,
enumerable: false,
writable: false,
configurable: false,
},
});
let windowIsClosing = false; let windowIsClosing = false;
let globalThis_; let globalThis_;