mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Answer 'in' on native Node handles from their declared members (#1949)
This commit is contained in:
@@ -119,6 +119,13 @@ export function emitChildProcessLibCall(host: LlvmEmitterContext, e: LibCallExpr
|
||||
B.line(`call void @scr_child_on_error(ptr ${child.name}, ptr ${cb.name}, ptr @${adapter})`);
|
||||
return { name: "", type: e.type };
|
||||
}
|
||||
if (e.fn === "child.hasChannel") {
|
||||
const child = host.emitExpr(e.args[0]!);
|
||||
host.declare(`declare zeroext i1 @scr_child_ipc_present(ptr)`);
|
||||
const out = B.tmp();
|
||||
B.line(`${out} = call i1 @scr_child_ipc_present(ptr ${child.name})`);
|
||||
return { name: out, type: e.type };
|
||||
}
|
||||
if (e.fn === "child.connected") {
|
||||
const child = host.emitExpr(e.args[0]!);
|
||||
host.declare(`declare zeroext i1 @scr_child_ipc_connected(ptr)`);
|
||||
|
||||
@@ -1918,6 +1918,30 @@ export function lowerInExpression(
|
||||
};
|
||||
}
|
||||
}
|
||||
// A child process has `send`, `disconnect` and `channel` only when it was
|
||||
// created with an IPC channel (fork, or an "ipc" stdio entry), and keeps
|
||||
// them after the channel closes: the runtime answers from the child.
|
||||
if (recv.type.kind === "child" && CHILD_CHANNEL_MEMBERS.has(key))
|
||||
return { kind: "libCall", fn: "child.hasChannel", args: [recv], type: BOOL, loc };
|
||||
// A native Node handle (an http.Server, a socket, a child process...):
|
||||
// a member Node's declarations give the class is on its prototype, so
|
||||
// `"closeAllConnections" in server` answers true. Undeclared names can
|
||||
// still be real (internal `_`-prefixed fields), so they keep the fence.
|
||||
if (
|
||||
NATIVE_HANDLE_KINDS.has(recv.type.kind) &&
|
||||
nodeDeclaredMember(lowerer, lowerer.typeOf(expr.right), key)
|
||||
) {
|
||||
const answer: IrExpr = { kind: "boolLit", value: true, type: BOOL, loc };
|
||||
return isSafeToDiscard(recv)
|
||||
? answer
|
||||
: {
|
||||
kind: "seqExpr",
|
||||
stmts: [{ kind: "exprStmt", expr: recv, loc }],
|
||||
result: answer,
|
||||
type: BOOL,
|
||||
loc,
|
||||
};
|
||||
}
|
||||
if (recv.type.kind !== "record") {
|
||||
lowerer.unsupported(
|
||||
"SC1090",
|
||||
@@ -2215,3 +2239,35 @@ function dynInstanceOfRefusal(lowerer: Lowerer, expr: ts.BinaryExpression): stri
|
||||
const line = ts.getLineAndCharacterOfPosition(sf, diag.loc.start).line + 1;
|
||||
return `${diag.message} [${diag.code} at ${diag.loc.file}:${line}]`;
|
||||
}
|
||||
|
||||
/** IR kinds of native Node handles whose members come from Node's own
|
||||
* declarations. */
|
||||
const NATIVE_HANDLE_KINDS: ReadonlySet<IrType["kind"]> = new Set([
|
||||
"netServer",
|
||||
"netSocket",
|
||||
"httpReq",
|
||||
"httpRes",
|
||||
"httpClientReq",
|
||||
"child",
|
||||
"dgramSocket",
|
||||
"fsWatcher",
|
||||
"http2Session",
|
||||
"http2Stream",
|
||||
]);
|
||||
|
||||
/** Child process members that exist only on a child with an IPC channel. */
|
||||
const CHILD_CHANNEL_MEMBERS: ReadonlySet<string> = new Set(["send", "disconnect", "channel"]);
|
||||
|
||||
/** Whether every constituent of `t` declares a required member `key` in
|
||||
* a declaration file (Node's @types/node). */
|
||||
function nodeDeclaredMember(lowerer: Lowerer, t: ts.Type, key: string): boolean {
|
||||
const parts = t.isUnionType() ? ts.constituentTypes(t) : [t];
|
||||
return parts.every((part) => {
|
||||
const prop = lowerer.checker.getPropertyOfType(part, key);
|
||||
if (prop === undefined || (prop.flags & ts.SymbolFlags.Optional) !== 0) return false;
|
||||
return (
|
||||
prop.declarations.length > 0 &&
|
||||
prop.declarations.every((d) => ts.getSourceFileOfNode(d).isDeclarationFile)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2608,6 +2608,7 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
|
||||
result: CHILD_T,
|
||||
},
|
||||
"child.connected": { argTypes: [CHILD_T], result: BOOL },
|
||||
"child.hasChannel": { argTypes: [CHILD_T], result: BOOL },
|
||||
"child.send": { argTypes: [CHILD_T, STRING], result: BOOL },
|
||||
"child.sendCb": { argTypes: [CHILD_T, STRING, null], result: BOOL },
|
||||
"child.disconnect": { argTypes: [CHILD_T], result: VOID },
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"order": [
|
||||
"<repo>/tests/corpus/8290-child-channel-members/main.ts"
|
||||
],
|
||||
"diags": []
|
||||
}
|
||||
@@ -6669,6 +6669,9 @@ double scr_process_fork_target(double target_count) {
|
||||
return target;
|
||||
}
|
||||
|
||||
/* The child was created with an IPC channel; it keeps it until it is freed. */
|
||||
bool scr_child_ipc_present(ScrChild *c) { return c->ipc != NULL; }
|
||||
|
||||
bool scr_child_ipc_connected(ScrChild *c) {
|
||||
return c->ipc != NULL && c->ipc->connected;
|
||||
}
|
||||
|
||||
@@ -176,6 +176,7 @@ void scr_child_release_v(void *p);
|
||||
void scr_child_on_exit(ScrChild *c, ScrClosure *cb /*moves*/, ScrChildExitFn fn);
|
||||
void scr_child_on_close(ScrChild *c, ScrClosure *cb /*moves*/, ScrChildExitFn fn);
|
||||
void scr_child_on_error(ScrChild *c, ScrClosure *cb /*moves*/, ScrChildErrFn fn);
|
||||
bool scr_child_ipc_present(ScrChild *c);
|
||||
bool scr_child_ipc_connected(ScrChild *c);
|
||||
bool scr_child_ipc_send(ScrChild *c, ScrStr *json);
|
||||
bool scr_child_ipc_send_cb(ScrChild *c, ScrStr *json,
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { fork, spawn } from "node:child_process";
|
||||
|
||||
// `send`, `disconnect` and `channel` exist only on a child created with an
|
||||
// IPC channel, and stay after the channel closes; the other members are
|
||||
// on every child.
|
||||
const describe = (label: string, child: ReturnType<typeof spawn>): string =>
|
||||
`${label}: send ${"send" in child} disconnect ${"disconnect" in child} channel ${"channel" in child} kill ${"kill" in child} pid ${"pid" in child}`;
|
||||
|
||||
const lines: string[] = [];
|
||||
const plain = spawn("/bin/sh", ["-c", "exit 0"], { stdio: "ignore" });
|
||||
lines.push(describe("spawn", plain));
|
||||
const forked = fork(new URL("./worker.ts", import.meta.url), [], {
|
||||
stdio: ["ignore", "ignore", "inherit", "ipc"],
|
||||
});
|
||||
lines.push(describe("fork", forked));
|
||||
forked.once("message", () => {
|
||||
forked.disconnect();
|
||||
lines.push(describe("fork after disconnect", forked));
|
||||
});
|
||||
forked.once("exit", () => {
|
||||
lines.push(describe("fork after exit", forked));
|
||||
});
|
||||
plain.once("exit", () => {
|
||||
lines.push(describe("spawn after exit", plain));
|
||||
});
|
||||
process.once("exit", () => {
|
||||
console.log(lines.sort().join("\n"));
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
process.send?.({ value: 1 });
|
||||
@@ -0,0 +1,7 @@
|
||||
import http from "node:http";
|
||||
const port = Number(process.argv[2]);
|
||||
http.get({ host: "127.0.0.1", port, path: "/" }, (res) => {
|
||||
let body = "";
|
||||
res.on("data", (c) => (body += c));
|
||||
res.on("end", () => console.log(res.statusCode, body));
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
// `in` tests on native Node handles answer from Node's declared members.
|
||||
import { createServer } from "node:http";
|
||||
import type { AddressInfo } from "node:net";
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
console.log("request", "method" in req, "setHeader" in res, "writeHead" in res);
|
||||
res.end("ok");
|
||||
});
|
||||
console.log("closeAllConnections" in server, "closeIdleConnections" in server, "listen" in server);
|
||||
server.listen(0, "127.0.0.1", () => {
|
||||
process.stderr.write(`PORT ${(server.address() as AddressInfo).port}\n`);
|
||||
});
|
||||
server.on("request", () => {
|
||||
setTimeout(() => {
|
||||
if ("closeAllConnections" in server) server.closeAllConnections();
|
||||
server.close(() => console.log("closed"));
|
||||
}, 10);
|
||||
});
|
||||
Reference in New Issue
Block a user