// Copyright 2018-2026 the Deno authors. MIT license.

import CP from "node:child_process";
import net from "node:net";
import { Buffer } from "node:buffer";
import {
  assert,
  assertEquals,
  assertNotStrictEquals,
  assertStrictEquals,
  assertStringIncludes,
  assertThrows,
} from "@std/assert";
import * as path from "@std/path";
import { clearTimeout, setTimeout } from "node:timers";

const { spawn, spawnSync, execFile, execFileSync, ChildProcess } = CP;

function withTimeout<T>(
  timeoutInMS = 10_000,
): ReturnType<typeof Promise.withResolvers<T>> {
  const deferred = Promise.withResolvers<T>();
  const timer = setTimeout(() => {
    deferred.reject("Timeout");
  }, timeoutInMS);
  deferred.promise.then(() => {
    clearTimeout(timer);
  });
  return deferred;
}

// TODO(uki00a): Once Node.js's `parallel/test-child-process-spawn-error.js` works, this test case should be removed.
Deno.test("[node/child_process spawn] The 'error' event is emitted when no binary is found", async () => {
  const deferred = withTimeout<void>();
  const childProcess = spawn("no-such-cmd");
  childProcess.on("error", (_err: Error) => {
    // TODO(@bartlomieju) Assert an error message.
    deferred.resolve();
  });
  await deferred.promise;
});

Deno.test("[node/child_process spawn] The 'exit' event is emitted with an exit code after the child process ends", async () => {
  const deferred = withTimeout<void>();
  const childProcess = spawn(Deno.execPath(), ["--help"], {
    env: { NO_COLOR: "true" },
  });
  try {
    let exitCode = null;
    childProcess.on("exit", (code: number) => {
      deferred.resolve();
      exitCode = code;
    });
    await deferred.promise;
    assertStrictEquals(exitCode, 0);
    assertStrictEquals(childProcess.exitCode, exitCode);
  } finally {
    childProcess.kill();
    childProcess.stdout?.destroy();
    childProcess.stderr?.destroy();
  }
});

Deno.test("[node/child_process disconnect] the method exists", async () => {
  const deferred = withTimeout<void>();
  const childProcess = spawn(Deno.execPath(), ["--help"], {
    env: { NO_COLOR: "true" },
    stdio: ["pipe", "pipe", "pipe", "ipc"],
  });
  try {
    childProcess.disconnect();
    childProcess.on("exit", () => {
      deferred.resolve();
    });
    await deferred.promise;
  } finally {
    childProcess.kill();
    childProcess.stdout?.destroy();
    childProcess.stderr?.destroy();
  }
});

Deno.test({
  name: "[node/child_process spawn] Verify that stdin and stdout work",
  fn: async () => {
    const deferred = withTimeout<void>();
    const childProcess = spawn(Deno.execPath(), ["fmt", "-"], {
      env: { NO_COLOR: "true" },
      stdio: ["pipe", "pipe"],
    });
    try {
      assert(childProcess.stdin, "stdin should be defined");
      assert(childProcess.stdout, "stdout should be defined");
      let data = "";
      childProcess.stdout.on("data", (chunk) => {
        data += chunk;
      });
      childProcess.stdin.write("  console.log('hello')", "utf-8");
      childProcess.stdin.end();
      childProcess.on("close", () => {
        deferred.resolve();
      });
      await deferred.promise;
      assertStrictEquals(data, `console.log("hello");\n`);
    } finally {
      childProcess.kill();
    }
  },
});

Deno.test({
  name: "[node/child_process spawn] stdin and stdout with binary data",
  fn: async () => {
    const deferred = withTimeout<void>();
    const p = path.join(
      path.dirname(path.fromFileUrl(import.meta.url)),
      "./testdata/binary_stdio.js",
    );
    const childProcess = spawn(Deno.execPath(), ["run", p], {
      env: { NO_COLOR: "true" },
      stdio: ["pipe", "pipe"],
    });
    try {
      assert(childProcess.stdin, "stdin should be defined");
      assert(childProcess.stdout, "stdout should be defined");
      let data: Buffer;
      childProcess.stdout.on("data", (chunk) => {
        data = chunk;
      });
      const buffer = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
      childProcess.stdin.write(buffer);
      childProcess.stdin.end();
      childProcess.on("close", () => {
        deferred.resolve();
      });
      await deferred.promise;
      assertEquals(new Uint8Array(data!), buffer);
    } finally {
      childProcess.kill();
    }
  },
});

async function spawnAndGetEnvValue(
  inputValue: string | number | boolean,
): Promise<string> {
  const deferred = withTimeout<string>();
  const env = spawn(
    `"${Deno.execPath()}" eval -p "Deno.env.toObject().BAZ"`,
    {
      env: { BAZ: String(inputValue), NO_COLOR: "true" },
      shell: true,
    },
  );
  try {
    let envOutput = "";

    assert(env.stdout);
    env.on("error", (err: Error) => deferred.reject(err));
    env.stdout.on("data", (data) => {
      envOutput += data;
    });
    env.on("close", () => {
      deferred.resolve(envOutput.trim());
    });
    return await deferred.promise;
  } finally {
    env.kill();
  }
}

Deno.test({
  ignore: Deno.build.os === "windows",
  name:
    "[node/child_process spawn] Verify that environment values can be numbers",
  async fn() {
    const envOutputValue = await spawnAndGetEnvValue(42);
    assertStrictEquals(envOutputValue, "42");
  },
});

Deno.test({
  ignore: Deno.build.os === "windows",
  name:
    "[node/child_process spawn] Verify that environment values can be booleans",
  async fn() {
    const envOutputValue = await spawnAndGetEnvValue(false);
    assertStrictEquals(envOutputValue, "false");
  },
});

/* Start of ported part */
// Copyright Joyent and Node contributors. All rights reserved. MIT license.
// Ported from Node 15.5.1

// TODO(uki00a): Remove this case once Node's `parallel/test-child-process-spawn-event.js` works.
Deno.test("[child_process spawn] 'spawn' event", async () => {
  const timeout = withTimeout<void>();
  const subprocess = spawn(Deno.execPath(), ["eval", "console.log('ok')"]);

  let didSpawn = false;
  subprocess.on("spawn", function () {
    didSpawn = true;
  });

  function mustNotBeCalled() {
    timeout.reject(new Error("function should not have been called"));
  }

  const promises = [] as Promise<void>[];
  function mustBeCalledAfterSpawn() {
    const deferred = Promise.withResolvers<void>();
    promises.push(deferred.promise);
    return () => {
      if (didSpawn) {
        deferred.resolve();
      } else {
        deferred.reject(
          new Error("function should be called after the 'spawn' event"),
        );
      }
    };
  }

  subprocess.on("error", mustNotBeCalled);
  subprocess.stdout!.on("data", mustBeCalledAfterSpawn());
  subprocess.stdout!.on("end", mustBeCalledAfterSpawn());
  subprocess.stdout!.on("close", mustBeCalledAfterSpawn());
  subprocess.stderr!.on("data", mustNotBeCalled);
  subprocess.stderr!.on("end", mustBeCalledAfterSpawn());
  subprocess.stderr!.on("close", mustBeCalledAfterSpawn());
  subprocess.on("exit", mustBeCalledAfterSpawn());
  subprocess.on("close", mustBeCalledAfterSpawn());

  try {
    await Promise.race([Promise.all(promises), timeout.promise]);
    timeout.resolve();
  } finally {
    subprocess.kill();
  }
});

// TODO(uki00a): Remove this case once Node's `parallel/test-child-process-spawn-shell.js` works.
Deno.test("[child_process spawn] Verify that a shell is executed", async () => {
  const deferred = withTimeout<void>();
  const doesNotExist = spawn("does-not-exist", { shell: true });
  try {
    assertNotStrictEquals(doesNotExist.spawnfile, "does-not-exist");
    doesNotExist.on("error", () => {
      deferred.reject("The 'error' event must not be emitted.");
    });
    doesNotExist.on("exit", (code: number, signal: null) => {
      assertStrictEquals(signal, null);

      if (Deno.build.os === "windows") {
        assertStrictEquals(code, 1); // Exit code of cmd.exe
      } else {
        assertStrictEquals(code, 127); // Exit code of /bin/sh });
      }

      deferred.resolve();
    });
    await deferred.promise;
  } finally {
    doesNotExist.kill();
    doesNotExist.stdout?.destroy();
    doesNotExist.stderr?.destroy();
  }
});

// TODO(uki00a): Remove this case once Node's `parallel/test-child-process-spawn-shell.js` works.
Deno.test({
  ignore: Deno.build.os === "windows",
  name: "[node/child_process spawn] Verify that passing arguments works",
  async fn() {
    const deferred = withTimeout<void>();
    const echo = spawn("echo", ["foo"], {
      shell: true,
    });
    let echoOutput = "";

    try {
      assertStrictEquals(
        echo.spawnargs[echo.spawnargs.length - 1].replace(/"/g, ""),
        "echo foo",
      );
      assert(echo.stdout);
      echo.stdout.on("data", (data) => {
        echoOutput += data;
      });
      echo.on("close", () => {
        assertStrictEquals(echoOutput.trim(), "foo");
        deferred.resolve();
      });
      await deferred.promise;
    } finally {
      echo.kill();
    }
  },
});

// TODO(uki00a): Remove this case once Node's `parallel/test-child-process-spawn-shell.js` works.
Deno.test({
  ignore: Deno.build.os === "windows",
  name: "[node/child_process spawn] Verity that shell features can be used",
  async fn() {
    const deferred = withTimeout<void>();
    const cmd = "echo bar | cat";
    const command = spawn(cmd, {
      shell: true,
    });
    try {
      let commandOutput = "";

      assert(command.stdout);
      command.stdout.on("data", (data) => {
        commandOutput += data;
      });

      command.on("close", () => {
        assertStrictEquals(commandOutput.trim(), "bar");
        deferred.resolve();
      });

      await deferred.promise;
    } finally {
      command.kill();
    }
  },
});

// TODO(uki00a): Remove this case once Node's `parallel/test-child-process-spawn-shell.js` works.
Deno.test({
  ignore: Deno.build.os === "windows",
  name:
    "[node/child_process spawn] Verity that environment is properly inherited",
  async fn() {
    const deferred = withTimeout<void>();
    const env = spawn(
      `"${Deno.execPath()}" eval -p "Deno.env.toObject().BAZ"`,
      {
        env: { BAZ: "buzz", NO_COLOR: "true" },
        shell: true,
      },
    );
    try {
      let envOutput = "";

      assert(env.stdout);
      env.on("error", (err: Error) => deferred.reject(err));
      env.stdout.on("data", (data) => {
        envOutput += data;
      });
      env.on("close", () => {
        assertStrictEquals(envOutput.trim(), "buzz");
        deferred.resolve();
      });
      await deferred.promise;
    } finally {
      env.kill();
    }
  },
});
/* End of ported part */

Deno.test({
  name: "[node/child_process execFile] Get stdout as a string",
  async fn() {
    let child: unknown;
    const script = path.join(
      path.dirname(path.fromFileUrl(import.meta.url)),
      "./testdata/exec_file_text_output.js",
    );
    const promise = new Promise<string | null>((resolve, reject) => {
      child = execFile(Deno.execPath(), ["run", script], (err, stdout) => {
        if (err) reject(err);
        else if (stdout) resolve(stdout as string);
        else resolve(null);
      });
    });
    try {
      const stdout = await promise;
      assertEquals(stdout, "Hello World!\n");
    } finally {
      if (child instanceof ChildProcess) {
        child.kill();
      }
    }
  },
});

Deno.test({
  name: "[node/child_process execFile] Get stdout as a buffer",
  async fn() {
    let child: unknown;
    const script = path.join(
      path.dirname(path.fromFileUrl(import.meta.url)),
      "./testdata/exec_file_text_output.js",
    );
    const promise = new Promise<Buffer | null>((resolve, reject) => {
      child = execFile(
        Deno.execPath(),
        ["run", script],
        { encoding: "buffer" },
        (err, stdout) => {
          if (err) reject(err);
          else if (stdout) resolve(stdout as Buffer);
          else resolve(null);
        },
      );
    });
    try {
      const stdout = await promise;
      assert(Buffer.isBuffer(stdout));
      assertEquals(stdout.toString("utf8"), "Hello World!\n");
    } finally {
      if (child instanceof ChildProcess) {
        child.kill();
      }
    }
  },
});

Deno.test({
  name: "[node/child_process execFile] Get stderr",
  async fn() {
    let child: unknown;
    const script = path.join(
      path.dirname(path.fromFileUrl(import.meta.url)),
      "./testdata/exec_file_text_error.js",
    );
    const promise = new Promise<
      { err: Error | null; stderr?: string | Buffer }
    >((resolve) => {
      child = execFile(Deno.execPath(), ["run", script], (err, _, stderr) => {
        resolve({ err, stderr });
      });
    });
    try {
      const { err, stderr } = await promise;
      if (child instanceof ChildProcess) {
        assertEquals(child.exitCode, 1);
        assertEquals(stderr, "yikes!\n");
      } else {
        throw err;
      }
    } finally {
      if (child instanceof ChildProcess) {
        child.kill();
      }
    }
  },
});

Deno.test({
  name: "[node/child_process execFile] Exceed given maxBuffer limit",
  async fn() {
    let child: unknown;
    const script = path.join(
      path.dirname(path.fromFileUrl(import.meta.url)),
      "./testdata/exec_file_text_error.js",
    );
    const promise = new Promise<
      { err: Error | null; stderr?: string | Buffer }
    >((resolve) => {
      child = execFile(Deno.execPath(), ["run", script], {
        encoding: "buffer",
        maxBuffer: 3,
      }, (err, _, stderr) => {
        resolve({ err, stderr });
      });
    });
    try {
      const { err, stderr } = await promise;
      if (child instanceof ChildProcess) {
        assert(err);
        assertEquals(
          // deno-lint-ignore no-explicit-any
          (err as any).code,
          "ERR_CHILD_PROCESS_STDIO_MAXBUFFER",
        );
        assertEquals(err.message, "stderr maxBuffer length exceeded");
        assertEquals((stderr as Buffer).toString("utf8"), "yik");
      } else {
        throw err;
      }
    } finally {
      if (child instanceof ChildProcess) {
        child.kill();
      }
    }
  },
});

Deno.test({
  ignore: true,
  name: "[node/child_process] ChildProcess.unref()",
  async fn() {
    const script = path.join(
      path.dirname(path.fromFileUrl(import.meta.url)),
      "testdata",
      "child_process_unref.js",
    );
    const childProcess = spawn(Deno.execPath(), [
      "run",
      "-A",
      script,
    ]);
    const deferred = Promise.withResolvers<void>();
    childProcess.on("exit", () => deferred.resolve());
    await deferred.promise;
  },
});

Deno.test({
  ignore: true,
  name: "[node/child_process] child_process.fork",
  async fn() {
    const testdataDir = path.join(
      path.dirname(path.fromFileUrl(import.meta.url)),
      "testdata",
    );
    const script = path.join(
      testdataDir,
      "node_modules",
      "foo",
      "index.js",
    );
    const p = Promise.withResolvers<void>();
    const cp = CP.fork(script, [], { cwd: testdataDir, stdio: "pipe" });
    let output = "";
    cp.on("close", () => p.resolve());
    cp.stdout?.on("data", (data) => {
      output += data;
    });
    await p.promise;
    assertEquals(output, "foo\ntrue\ntrue\ntrue\n");
  },
});

Deno.test({
  name: "[node/child_process] child_process.fork with URL",
  async fn() {
    const testdataDir = path.join(
      path.dirname(path.fromFileUrl(import.meta.url)),
      "testdata",
    );
    const script = path.join(
      testdataDir,
      "node_modules",
      "foo",
      "index.js",
    );
    const scriptUrl = path.toFileUrl(script);
    const p = Promise.withResolvers<void>();
    const cp = CP.fork(scriptUrl, [], { cwd: testdataDir, stdio: "pipe" });
    let output = "";
    cp.on("close", () => p.resolve());
    cp.stdout?.on("data", (data) => {
      output += data;
    });
    await p.promise;
    assertEquals(output, "foo\ntrue\ntrue\ntrue\n");
  },
});

Deno.test("[node/child_process execFileSync] 'inherit' stdout and stderr", () => {
  execFileSync(Deno.execPath(), ["--help"], { stdio: "inherit" });
});

Deno.test(
  "[node/child_process spawn] supports windowsVerbatimArguments option",
  { ignore: Deno.build.os !== "windows" },
  async () => {
    const cmdFinished = Promise.withResolvers<void>();
    let output = "";
    const cp = spawn("cmd", ["/d", "/s", "/c", '"deno ^"--version^""'], {
      stdio: "pipe",
      windowsVerbatimArguments: true,
    });
    cp.on("close", () => cmdFinished.resolve());
    cp.stdout?.on("data", (data) => {
      output += data;
    });
    await cmdFinished.promise;
    assertStringIncludes(output, "deno");
    assertStringIncludes(output, "v8");
    assertStringIncludes(output, "typescript");
  },
);

Deno.test(
  "[node/child_process spawn] supports stdio array option",
  async () => {
    const cmdFinished = Promise.withResolvers<void>();
    let output = "";
    const script = path.join(
      path.dirname(path.fromFileUrl(import.meta.url)),
      "testdata",
      "child_process_stdio.js",
    );
    const cp = spawn(Deno.execPath(), ["run", "-A", script]);
    cp.stdout?.on("data", (data) => {
      output += data;
    });
    cp.on("close", () => cmdFinished.resolve());
    await cmdFinished.promise;

    assertStringIncludes(output, "foo");
    assertStringIncludes(output, "close");
  },
);

Deno.test(
  "[node/child_process spawn] supports stdio [0, 1, 2] option",
  async () => {
    const cmdFinished = Promise.withResolvers<void>();
    let output = "";
    const script = path.join(
      path.dirname(path.fromFileUrl(import.meta.url)),
      "testdata",
      "child_process_stdio_012.js",
    );
    const cp = spawn(Deno.execPath(), ["run", "-A", script]);
    cp.stdout?.on("data", (data) => {
      output += data;
    });
    cp.on("close", () => cmdFinished.resolve());
    await cmdFinished.promise;

    assertStringIncludes(output, "foo");
    assertStringIncludes(output, "close");
  },
);

Deno.test({
  name: "[node/child_process spawn] supports Deno child fd 3 pipe",
  ignore: Deno.build.os === "windows",
  async fn() {
    const cmdFinished = Promise.withResolvers<void>();
    let output = "";
    let stderr = "";
    const script = path.join(
      path.dirname(path.fromFileUrl(import.meta.url)),
      "testdata",
      "child_process_extra_stdio_fd3.js",
    );
    const cp = spawn(Deno.execPath(), ["run", "-A", script], {
      stdio: ["ignore", "ignore", "pipe", "pipe"],
    });
    const extra = cp.stdio[3];
    if (!extra) {
      throw new Error("missing fd 3 pipe");
    }
    extra.on("data", (data) => {
      output += data;
    });
    cp.stderr?.on("data", (data) => {
      stderr += data;
    });
    cp.on("close", (code) => {
      assertEquals(code, 0, stderr);
      assertEquals(stderr, "");
      assertEquals(output, "hello from fd 3");
      cmdFinished.resolve();
    });
    await cmdFinished.promise;
  },
});

Deno.test({
  name:
    "[node/child_process spawn] concurrent spawns with extra pipe fds keep handles valid (#35994)",
  async fn() {
    // Playwright launches Firefox/WebKit with two extra pipe fds (3 and 4)
    // for its "pipe" browser-protocol transport. On Windows the child end of
    // each extra pipe used to be closed twice, which - under concurrent
    // spawns - raced with handle-value reuse and intermittently failed a
    // sibling spawn with "The handle is invalid. (os error 6)". Spawn a batch
    // concurrently to stress handle reuse and assert none of them fail.
    const isWindows = Deno.build.os === "windows";
    const command = isWindows ? "cmd.exe" : "/bin/sh";
    const args = isWindows ? ["/c", "exit 0"] : ["-c", "exit 0"];

    const spawnOne = () =>
      new Promise<void>((resolve, reject) => {
        const cp = spawn(command, args, {
          stdio: ["ignore", "pipe", "pipe", "pipe", "pipe"],
        });
        cp.on("error", reject);
        // Drain every piped stream so the child's exit is observed promptly
        // and no pipe keeps the process from closing.
        for (const stream of cp.stdio) {
          // deno-lint-ignore no-explicit-any
          const s = stream as any;
          if (s && typeof s.resume === "function") {
            s.resume();
          }
        }
        cp.on("close", (code) => {
          if (code !== 0) {
            reject(new Error(`child exited with code ${code}`));
          } else {
            resolve();
          }
        });
      });

    const BATCH = 24;
    await Promise.all(Array.from({ length: BATCH }, () => spawnOne()));
  },
});

Deno.test({
  name:
    "[node/child_process spawn] Deno child fd 3 stays claimable by net.Socket",
  ignore: Deno.build.os === "windows",
  async fn() {
    const cmdFinished = Promise.withResolvers<void>();
    let output = "";
    let stderr = "";
    const script = path.join(
      path.dirname(path.fromFileUrl(import.meta.url)),
      "testdata",
      "child_process_extra_stdio_fd3_socket.js",
    );
    const cp = spawn(Deno.execPath(), ["run", "-A", script], {
      stdio: ["ignore", "ignore", "pipe", "pipe"],
    });
    const extra = cp.stdio[3];
    if (!extra) {
      throw new Error("missing fd 3 pipe");
    }
    extra.on("data", (data) => {
      output += data;
    });
    cp.stderr?.on("data", (data) => {
      stderr += data;
    });
    cp.on("close", (code) => {
      assertEquals(code, 0, stderr);
      assertEquals(stderr, "");
      assertEquals(output, "hello from fd 3 and from fd 3 socket");
      cmdFinished.resolve();
    });
    await cmdFinished.promise;
  },
});

Deno.test({
  name:
    "[node/child_process spawn] Deno child adopts a TCP fd passed as extra stdio",
  ignore: Deno.build.os === "windows",
  async fn() {
    const cmdFinished = Promise.withResolvers<void>();
    const received = Promise.withResolvers<string>();
    const serverClosed = Promise.withResolvers<void>();
    let stderr = "";
    const script = path.join(
      path.dirname(path.fromFileUrl(import.meta.url)),
      "testdata",
      "child_process_extra_stdio_fd3_tcp.js",
    );
    const server = net.createServer((conn) => {
      let data = "";
      conn.on("data", (chunk) => {
        data += chunk;
      });
      conn.on("end", () => received.resolve(data));
    });
    server.listen(0, "127.0.0.1", () => {
      const address = server.address() as net.AddressInfo;
      const client = net.connect(address.port, "127.0.0.1", () => {
        client.pause();
        // Grab a raw duplicate of the connected TCP fd to pass through the
        // child's stdio slot 3. The dup is intentionally left open here; only
        // the child writes to the connection, and its shutdown() delivers the
        // FIN to the server regardless of this process's remaining handles.
        // deno-lint-ignore no-explicit-any
        const rawFd = (client as any)._handle.fdForIpc();
        assert(rawFd >= 0);
        const cp = spawn(Deno.execPath(), ["run", "-A", script], {
          stdio: ["ignore", "ignore", "pipe", rawFd],
        });
        cp.stderr?.on("data", (data) => {
          stderr += data;
        });
        cp.on("close", (code) => {
          client.destroy();
          server.close(() => serverClosed.resolve());
          assertEquals(code, 0, stderr);
          assertEquals(stderr, "");
          cmdFinished.resolve();
        });
      });
    });
    await cmdFinished.promise;
    assertEquals(await received.promise, "hello from fd 3 tcp");
    await serverClosed.promise;
  },
});

Deno.test({
  name:
    "[node/child_process spawn] Deno child adopts a listening TCP fd passed as extra stdio",
  ignore: Deno.build.os === "windows",
  async fn() {
    const cmdFinished = Promise.withResolvers<void>();
    const received = Promise.withResolvers<string>();
    let stderr = "";
    const script = path.join(
      path.dirname(path.fromFileUrl(import.meta.url)),
      "testdata",
      "child_process_extra_stdio_fd3_tcp_listen.js",
    );
    // Create a listening socket, then hand a raw dup of it to the child and
    // close our copy; the kernel keeps the socket listening through the dup
    // (which is intentionally left open in this process).
    const bootstrap = net.createServer();
    bootstrap.listen(0, "127.0.0.1", () => {
      const address = bootstrap.address() as net.AddressInfo;
      // deno-lint-ignore no-explicit-any
      const rawFd = (bootstrap as any)._handle.fdForIpc();
      assert(rawFd >= 0);
      bootstrap.close(() => {
        const cp = spawn(Deno.execPath(), ["run", "-A", script], {
          stdio: ["ignore", "ignore", "pipe", rawFd],
        });
        cp.stderr?.on("data", (data) => {
          stderr += data;
        });
        cp.on("close", (code) => {
          assertEquals(code, 0, stderr);
          assertEquals(stderr, "");
          cmdFinished.resolve();
        });
        // The kernel queues this connection on the still-listening socket
        // until the child adopts fd 3 and accepts it.
        const client = net.connect(address.port, "127.0.0.1");
        let data = "";
        client.on("data", (chunk) => {
          data += chunk;
        });
        client.on("end", () => received.resolve(data));
        client.on("error", (err) => received.reject(err));
      });
    });
    await cmdFinished.promise;
    assertEquals(await received.promise, "hello from fd 3 listener");
  },
});

Deno.test({
  name: "[node/child_process spawn] supports SIGIOT signal",
  ignore: Deno.build.os === "windows",
  async fn() {
    // Note: attempting to kill Deno with SIGABRT causes the process to zombify on certain OSX builds
    // eg: 22.5.0 Darwin Kernel Version 22.5.0: Mon Apr 24 20:53:19 PDT 2023; root:xnu-8796.121.2~5/RELEASE_ARM64_T6020 arm64
    // M2 Pro running Ventura 13.4

    // Spawn an infinite cat
    const cp = spawn("cat", ["-"]);
    const p = withTimeout<void>();
    const pStdout = withTimeout<void>();
    const pStderr = withTimeout<void>();
    cp.on("exit", () => p.resolve());
    cp.stdout.on("close", () => pStdout.resolve());
    cp.stderr.on("close", () => pStderr.resolve());
    cp.kill("SIGIOT");
    await p.promise;
    await pStdout.promise;
    await pStderr.promise;
    assert(cp.killed);
    assertEquals(cp.signalCode, "SIGIOT");
  },
});

Deno.test({
  name:
    "[node/child_process spawn] AbortSignal aborts the child and emits AbortError",
  async fn() {
    const ac = new AbortController();
    const cp = spawn(
      Deno.execPath(),
      ["eval", "setInterval(() => {}, 1000)"],
      { signal: ac.signal },
    );
    const error = withTimeout<Error>();
    const exit = withTimeout<void>();
    cp.on("error", (err) => error.resolve(err));
    cp.on("exit", () => exit.resolve());
    cp.on("spawn", () => ac.abort());
    const err = await error.promise;
    assertEquals(err.name, "AbortError");
    await exit.promise;
  },
});

Deno.test({
  name:
    "[node/child_process] kill() on an exited process returns false and does not throw",
  async fn() {
    // Regression for https://github.com/denoland/deno/issues/28882 — killing
    // a process that has already exited must not surface a spurious error
    // (PermissionError on Windows / ESRCH on Unix), it should just return
    // false like Node.js does.
    const cp = spawn(Deno.execPath(), ["eval", "Deno.exit(0)"]);
    const exit = withTimeout<void>();
    cp.on("exit", () => exit.resolve());
    await exit.promise;
    assertEquals(cp.kill("SIGTERM"), false);
  },
});

Deno.test({
  name:
    "[node/child_process spawn] child inherits Deno.env when options.env is not provided",
  async fn() {
    const deferred = withTimeout<string>();
    Deno.env.set("BAR", "BAR");
    const env = spawn(
      `"${Deno.execPath()}" eval -p "Deno.env.toObject().BAR"`,
      {
        shell: true,
      },
    );
    try {
      let envOutput = "";

      assert(env.stdout);
      env.on("error", (err: Error) => deferred.reject(err));
      env.stdout.on("data", (data) => {
        envOutput += data;
      });
      env.on("close", () => {
        deferred.resolve(envOutput.trim());
      });
      await deferred.promise;
    } finally {
      env.kill();
      Deno.env.delete("BAR");
    }
    const value = await deferred.promise;
    assertEquals(value, "BAR");
  },
});

Deno.test({
  name:
    "[node/child_process spawn] child doesn't inherit Deno.env when options.env is provided",
  async fn() {
    const deferred = withTimeout<string>();
    Deno.env.set("BAZ", "BAZ");
    const env = spawn(
      `"${Deno.execPath()}" eval -p "Deno.env.toObject().BAZ"`,
      {
        env: {},
        shell: true,
      },
    );
    try {
      let envOutput = "";

      assert(env.stdout);
      env.on("error", (err: Error) => deferred.reject(err));
      env.stdout.on("data", (data) => {
        envOutput += data;
      });
      env.on("close", () => {
        deferred.resolve(envOutput.trim());
      });
      await deferred.promise;
    } finally {
      env.kill();
      Deno.env.delete("BAZ");
    }
    const value = await deferred.promise;
    assertEquals(value, "undefined");
  },
});

// Regression test for https://github.com/denoland/deno/issues/20373
Deno.test(async function undefinedValueInEnvVar() {
  const deferred = withTimeout<string>();
  const env = spawn(
    `"${Deno.execPath()}" eval -p "Deno.env.toObject().BAZ"`,
    {
      env: {
        BAZ: "BAZ",
        NO_COLOR: "true",
        UNDEFINED_ENV: undefined,
        // deno-lint-ignore no-explicit-any
        NULL_ENV: null as any,
      },
      shell: true,
    },
  );
  try {
    let envOutput = "";

    assert(env.stdout);
    env.on("error", (err: Error) => deferred.reject(err));
    env.stdout.on("data", (data) => {
      envOutput += data;
    });
    env.on("close", () => {
      deferred.resolve(envOutput.trim());
    });
    await deferred.promise;
  } finally {
    env.kill();
  }
  const value = await deferred.promise;
  assertEquals(value, "BAZ");
});

// Regression test for https://github.com/denoland/deno/issues/20373
Deno.test(function spawnSyncUndefinedValueInEnvVar() {
  const ret = spawnSync(
    `"${Deno.execPath()}" eval -p "Deno.env.toObject().BAZ"`,
    {
      env: {
        BAZ: "BAZ",
        NO_COLOR: "true",
        UNDEFINED_ENV: undefined,
        // deno-lint-ignore no-explicit-any
        NULL_ENV: null as any,
      },
      shell: true,
    },
  );

  assertEquals(ret.status, 0);
  assertEquals(ret.stdout.toString("utf-8").trim(), "BAZ");
});

Deno.test(function spawnSyncStdioUndefined() {
  const ret = spawnSync(
    `"${Deno.execPath()}" eval "console.log('hello');console.error('world')"`,
    {
      stdio: [undefined, undefined, undefined],
      shell: true,
    },
  );

  assertEquals(ret.status, 0);
  assertEquals(ret.stdout.toString("utf-8").trim(), "hello");
  assertEquals(ret.stderr.toString("utf-8").trim(), "world");
});

Deno.test(function spawnSyncExitNonZero() {
  const ret = spawnSync(
    `"${Deno.execPath()}" eval "Deno.exit(22)"`,
    { shell: true },
  );

  assertEquals(ret.status, 22);
});

// https://github.com/denoland/deno/issues/21630
Deno.test(async function forkIpcKillDoesNotHang() {
  const testdataDir = path.join(
    path.dirname(path.fromFileUrl(import.meta.url)),
    "testdata",
  );
  const script = path.join(
    testdataDir,
    "node_modules",
    "foo",
    "index.js",
  );
  const p = Promise.withResolvers<void>();
  const cp = CP.fork(script, [], {
    cwd: testdataDir,
    stdio: ["inherit", "inherit", "inherit", "ipc"],
  });
  cp.on("close", () => p.resolve());
  cp.kill();

  await p.promise;
});

Deno.test(async function stripForkEnableSourceMaps() {
  const testdataDir = path.join(
    path.dirname(path.fromFileUrl(import.meta.url)),
    "testdata",
  );
  const script = path.join(
    testdataDir,
    "node_modules",
    "foo",
    "check_argv.js",
  );
  const p = Promise.withResolvers<void>();
  const cp = CP.fork(script, [], {
    cwd: testdataDir,
    stdio: "pipe",
    execArgv: ["--enable-source-maps"],
  });
  let output = "";
  cp.on("close", () => p.resolve());
  cp.stdout?.on("data", (data) => {
    output += data;
    cp.kill();
  });
  await p.promise;
  assertEquals(output, "2\n");
});

Deno.test(async function execFileWithUndefinedTimeout() {
  const { promise, resolve, reject } = Promise.withResolvers<void>();
  CP.execFile(
    "git",
    ["--version"],
    { timeout: undefined, encoding: "utf8" },
    (err) => {
      if (err) {
        reject(err);
        return;
      }
      resolve();
    },
  );
  await promise;
});

Deno.test(async function spawnCommandNotFoundErrno() {
  const { promise, resolve } = Promise.withResolvers<void>();
  const cp = CP.spawn("no-such-command");
  cp.on("error", (err) => {
    const errno = Deno.build.os === "windows" ? -4058 : -2;
    // @ts-ignore: errno missing from typings
    assertEquals(err.errno, errno);
    resolve();
  });
  await promise;
});

// https://github.com/denoland/deno/issues/23045
Deno.test(function spawnCommandNullStdioArray() {
  const ret = spawnSync(
    `"${Deno.execPath()}" eval "console.log('hello');console.error('world')"`,
    {
      stdio: [null, null, null],
      shell: true,
    },
  );

  assertEquals(ret.status, 0);
});

Deno.test(
  function stdinInherit() {
    const script = `
      function timeoutPromise(promise, timeout) {
        return new Promise((resolve, reject) => {
          const timeoutId = setTimeout(() => {
            Deno.exit(69);
          }, timeout);
          promise.then((value) => {
            clearTimeout(timeoutId);
            resolve(value);
          }, (reason) => {
            clearTimeout(timeoutId);
            reject(reason);
          });
        });
      }

      await timeoutPromise(Deno.stdin.read(new Uint8Array(1)), 100)
    `;

    const output = spawnSync(Deno.execPath(), ["eval", script], {
      stdio: "inherit",
    });

    // We want to timeout to occur because the stdin isn't 'null'
    assertEquals(output.status, 69);
    assertEquals(output.stdout, null);
    assertEquals(output.stderr, null);
  },
);

Deno.test(
  async function ipcSerialization() {
    const timeout = withTimeout<void>();
    const script = `
      if (typeof process.send !== "function") {
        console.error("process.send is not a function");
        process.exit(1);
      }

      class BigIntWrapper {
        constructor(value) {
          this.value = value;
        }
        toJSON() {
          return this.value.toString();
        }
      }

      const makeSab = (arr) => {
        const sab = new SharedArrayBuffer(arr.length);
        const buf = new Uint8Array(sab);
        for (let i = 0; i < arr.length; i++) {
          buf[i] = arr[i];
        }
        return buf;
      };


      const inputs = [
        "foo",
        {
          foo: "bar",
        },
        42,
        true,
        null,
        new Uint8Array([1, 2, 3]),
        {
          foo: new Uint8Array([1, 2, 3]),
          bar: makeSab([4, 5, 6]),
        },
        [1, { foo: 2 }, [3, 4]],
        new BigIntWrapper(42n),
      ];
      for (const input of inputs) {
        process.send(input);
      }
    `;
    const file = await Deno.makeTempFile();
    await Deno.writeTextFile(file, script);
    const child = CP.fork(file, [], {
      stdio: ["inherit", "inherit", "inherit", "ipc"],
    });
    const expect = [
      "foo",
      {
        foo: "bar",
      },
      42,
      true,
      null,
      [1, 2, 3],
      {
        foo: [1, 2, 3],
        bar: [4, 5, 6],
      },
      [1, { foo: 2 }, [3, 4]],
      "42",
    ];
    let i = 0;

    child.on("message", (message) => {
      assertEquals(message, expect[i]);
      i++;
    });
    child.on("close", () => timeout.resolve());
    await timeout.promise;
    assertEquals(i, expect.length);
  },
);

Deno.test(async function ipcSerializationAdvanced() {
  const timeout = withTimeout<void>();
  const script = `
      if (typeof process.send !== "function") {
        console.error("process.send is not a function");
        process.exit(1);
      }

      class BigIntWrapper {
        constructor(value) {
          this.value = value;
        }
        toJSON() {
          return this.value.toString();
        }
      }

      const makeSab = (arr) => {
        const sab = new SharedArrayBuffer(arr.length);
        const buf = new Uint8Array(sab);
        for (let i = 0; i < arr.length; i++) {
          buf[i] = arr[i];
        }
        return buf;
      };


      const inputs = [
        "foo",
        {
          foo: "bar",
        },
        42,
        true,
        null,
        new Uint8Array([1, 2, 3]),
        // Empty typed arrays / buffers used to panic the IPC deserializer
        // because a zero-length ArrayBuffer has a null backing store.
        new Uint8Array([]),
        Buffer.alloc(0),
        {
          foo: new Uint8Array([1, 2, 3]),
          bar: makeSab([4, 5, 6]),
          baz: new Uint8Array([]),
        },
        [1, { foo: 2 }, [3, 4]],
        42n,
      ];
      for (const input of inputs) {
        process.send(input);
      }
    `;
  const makeSab = (arr: number[]) => {
    const sab = new SharedArrayBuffer(arr.length);
    const buf = new Uint8Array(sab);
    for (let i = 0; i < arr.length; i++) {
      buf[i] = arr[i];
    }
    return buf;
  };

  const file = await Deno.makeTempFile();
  await Deno.writeTextFile(file, script);
  const child = CP.fork(file, [], {
    stdio: ["inherit", "inherit", "inherit", "ipc"],
    serialization: "advanced",
  });
  const expect = [
    "foo",
    {
      foo: "bar",
    },
    42,
    true,
    null,
    new Uint8Array([1, 2, 3]),
    new Uint8Array([]),
    Buffer.alloc(0),
    {
      foo: new Uint8Array([1, 2, 3]),
      bar: makeSab([4, 5, 6]),
      baz: new Uint8Array([]),
    },
    [1, { foo: 2 }, [3, 4]],
    42n,
  ];
  let i = 0;

  child.on("message", (message) => {
    assertEquals(message, expect[i]);
    i++;
  });
  child.on("close", () => timeout.resolve());
  await timeout.promise;
  assertEquals(i, expect.length);
});

Deno.test(async function childProcessExitsGracefully() {
  const testdataDir = path.join(
    path.dirname(path.fromFileUrl(import.meta.url)),
    "testdata",
  );
  const script = path.join(
    testdataDir,
    "node_modules",
    "foo",
    "index.js",
  );
  const p = Promise.withResolvers<void>();
  const cp = CP.fork(script, [], {
    cwd: testdataDir,
    stdio: ["inherit", "inherit", "inherit", "ipc"],
  });
  cp.on("close", () => p.resolve());

  await p.promise;
});

Deno.test(async function killMultipleTimesNoError() {
  const loop = `
    while (true) {
      await new Promise((resolve) => setTimeout(resolve, 10000));
    }
  `;

  const timeout = withTimeout<void>();
  const file = await Deno.makeTempFile();
  await Deno.writeTextFile(file, loop);
  const child = CP.fork(file, [], {
    stdio: ["inherit", "inherit", "inherit", "ipc"],
  });
  child.on("close", () => {
    timeout.resolve();
  });
  child.kill();
  child.kill();

  // explicitly calling disconnect after kill should throw
  assertThrows(() => child.disconnect());

  await timeout.promise;
});

// Make sure that you receive messages sent before a "message" event listener is set up
Deno.test(async function bufferMessagesIfNoListener() {
  const code = `
    process.on("message", (_) => {
      process.channel.unref();
    });
    process.send("hello");
    process.send("world");
    console.error("sent messages");
  `;
  const file = await Deno.makeTempFile();
  await Deno.writeTextFile(file, code);
  const timeout = withTimeout<void>();
  const child = CP.fork(file, [], {
    stdio: ["inherit", "inherit", "pipe", "ipc"],
  });

  let got = 0;
  child.on("message", (message) => {
    if (got++ === 0) {
      assertEquals(message, "hello");
    } else {
      assertEquals(message, "world");
    }
  });
  child.on("close", () => {
    timeout.resolve();
  });
  let stderr = "";
  child.stderr?.on("data", (data) => {
    stderr += data;
    if (stderr.includes("sent messages")) {
      // now that we've set up the listeners, and the child
      // has sent the messages, we can let it exit
      child.send("ready");
    }
  });
  await timeout.promise;
  assertEquals(got, 2);
});

Deno.test(async function sendAfterClosedThrows() {
  const code = ``;
  const file = await Deno.makeTempFile();
  await Deno.writeTextFile(file, code);
  const timeout = withTimeout<void>();
  const child = CP.fork(file, [], {
    stdio: ["inherit", "inherit", "inherit", "ipc"],
  });
  child.on("error", (err) => {
    assert("code" in err);
    assertEquals(err.code, "ERR_IPC_CHANNEL_CLOSED");
    timeout.resolve();
  });
  child.on("close", () => {
    child.send("ready");
  });

  await timeout.promise;
});

Deno.test(async function noWarningsFlag() {
  const code = ``;
  const file = await Deno.makeTempFile();
  await Deno.writeTextFile(file, code);
  const timeout = withTimeout<void>();
  const child = CP.fork(file, [], {
    execArgv: ["--no-warnings"],
    stdio: ["inherit", "inherit", "inherit", "ipc"],
  });
  child.on("close", () => {
    timeout.resolve();
  });

  await timeout.promise;
});

Deno.test({
  name: "[node/child_process] spawnSync supports input option",
  fn() {
    const text = "  console.log('hello')";
    const expected = `console.log("hello");\n`;
    {
      const { stdout } = spawnSync(Deno.execPath(), ["fmt", "-"], {
        input: text,
      });
      assertEquals(stdout.toString(), expected);
    }
    {
      const { stdout } = spawnSync(Deno.execPath(), ["fmt", "-"], {
        input: Buffer.from(text),
      });
      assertEquals(stdout.toString(), expected);
    }
    {
      const { stdout } = spawnSync(Deno.execPath(), ["fmt", "-"], {
        input: new TextEncoder().encode(text),
      });
      assertEquals(stdout.toString(), expected);
    }
    {
      const b = Buffer.from(text);
      const { stdout } = spawnSync(Deno.execPath(), ["fmt", "-"], {
        input: new DataView(b.buffer, b.byteOffset, b.byteLength),
      });
      assertEquals(stdout.toString(), expected);
    }

    assertThrows(
      () => {
        spawnSync(Deno.execPath(), ["fmt", "-"], {
          // deno-lint-ignore no-explicit-any
          input: {} as any,
        });
      },
      Error,
      'The "input" argument must be of type string or an instance of Buffer, TypedArray, or DataView. Received an instance of Object',
    );
  },
});

// Regression test for https://github.com/denoland/deno/issues/25776
Deno.test(async function killSignalZero() {
  const child = CP.spawn(Deno.execPath(), [
    "eval",
    "setTimeout(() => {}, 60_000)",
  ]);
  try {
    // Signal 0 checks if the process exists without sending a signal
    const alive = child.kill(0);
    assertEquals(alive, true);
    // The child should not be marked as killed
    assertEquals(child.killed, false);
  } finally {
    child.kill();
    await new Promise((resolve) => child.on("close", resolve));
  }
});

Deno.test(async function experimentalFlag() {
  const code = ``;
  const file = await Deno.makeTempFile();
  await Deno.writeTextFile(file, code);
  const timeout = withTimeout<void>();
  const child = CP.fork(file, [], {
    execArgv: ["--experimental-vm-modules"],
    stdio: ["inherit", "inherit", "inherit", "ipc"],
  });
  child.on("close", () => {
    timeout.resolve();
  });

  await timeout.promise;
});

// Regression test for https://github.com/denoland/deno/issues/26784
// process.stdout partial writes were silently dropped, causing data
// truncation when piping >64KB through a subprocess.
Deno.test(async function stdoutPipePartialWriteNotTruncated() {
  // Child script: pipe a file to stdout via createReadStream
  const pipeScript = await Deno.makeTempFile({ suffix: ".mjs" });
  await Deno.writeTextFile(
    pipeScript,
    `import fs from "node:fs";fs.createReadStream(process.argv[2]).pipe(process.stdout);`,
  );
  // Create a file >65536 bytes with multiple lines (triggers chunked reads)
  const dataFile = await Deno.makeTempFile();
  const content = "x".repeat(40000) + "\n" + "y".repeat(40000) + "\n";
  await Deno.writeTextFile(dataFile, content);
  try {
    const output = execFileSync(Deno.execPath(), [
      "run",
      "--allow-read",
      pipeScript,
      dataFile,
    ], { encoding: "utf-8", maxBuffer: 50 * 1024 * 1024 });
    assertEquals(output.length, content.length);
    assertEquals(output, content);
  } finally {
    await Deno.remove(pipeScript);
    await Deno.remove(dataFile);
  }
});

Deno.test(async function stdoutWriteMultipleChunksNotTruncated() {
  // Child script: write two large chunks to stdout directly
  const script = await Deno.makeTempFile({ suffix: ".mjs" });
  await Deno.writeTextFile(
    script,
    `const chunk1 = "A".repeat(50000);const chunk2 = "B".repeat(50000);process.stdout.write(chunk1);process.stdout.write(chunk2);`,
  );
  try {
    const output = execFileSync(Deno.execPath(), ["run", script], {
      encoding: "utf-8",
      maxBuffer: 50 * 1024 * 1024,
    });
    assertEquals(output.length, 100000);
    assertEquals(output, "A".repeat(50000) + "B".repeat(50000));
  } finally {
    await Deno.remove(script);
  }
});

Deno.test(function spawnSyncShellMetacharactersEscaped() {
  // Shell metacharacters in args should be escaped, not interpreted.
  // On Unix, & would launch a background process; on Windows, it
  // would chain a second command. Either way echo should print
  // the literal string.
  const ret = spawnSync(
    Deno.execPath(),
    ["eval", "console.log(Deno.args[0])", "--", "a&b|c<d>e"],
    { shell: true, encoding: "utf-8" },
  );
  assertEquals(ret.status, 0);
  assertEquals(ret.stdout.trim(), "a&b|c<d>e");
});

Deno.test({
  name: "spawn shell preserves pre-quoted arguments",
  ignore: Deno.build.os === "windows",
  async fn() {
    for (const quote of ["'", '"']) {
      const deferred = withTimeout<number | null>();
      const child = spawn(
        "printf",
        [`${quote}<%s>\\n${quote}`, `${quote}hello world${quote}`],
        { shell: true },
      );
      let stdout = "";
      child.stdout!.setEncoding("utf-8");
      child.stdout!.on("data", (chunk) => stdout += chunk);
      child.on("error", deferred.reject);
      child.on("close", deferred.resolve);
      assertEquals(await deferred.promise, 0);
      assertEquals(stdout, "<hello world>\n");
    }
  },
});

Deno.test({
  name: "spawnSync shell preserves pre-quoted arguments",
  ignore: Deno.build.os === "windows",
  fn() {
    for (const quote of ["'", '"']) {
      const ret = spawnSync(
        "printf",
        [`${quote}<%s>\\n${quote}`, `${quote}hello world${quote}`],
        { shell: true, encoding: "utf-8" },
      );
      assertEquals(ret.status, 0);
      assertEquals(ret.stdout, "<hello world>\n");
    }

    const breakout = spawnSync(
      "printf",
      ["'%s\\n'", "'safe'; printf injected; 'still-safe'"],
      { shell: true, encoding: "utf-8" },
    );
    assertEquals(breakout.status, 0);
    assertEquals(
      breakout.stdout,
      "safe'; printf injected; 'still-safe\n",
    );
  },
});

Deno.test(function spawnSyncReturnsPid() {
  const ret = spawnSync(Deno.execPath(), ["eval", "console.log('hi')"]);
  assertEquals(ret.status, 0);
  assertEquals(typeof ret.pid, "number");
  assert(ret.pid > 0);
});

Deno.test({
  name: "spawnWithNumericFdInStdioArray",
  ignore: Deno.build.os === "windows",
  async fn() {
    const fs = await import("node:fs");
    const tmpFile = Deno.makeTempFileSync();
    try {
      const fd = fs.openSync(tmpFile, "w");
      const child = spawn("/bin/sh", [
        "-c",
        "echo hello from child >&3",
      ], {
        stdio: ["ignore", "pipe", "pipe", fd],
      });
      const { promise, resolve } = Promise.withResolvers<number>();
      child.on("close", (code: number) => {
        resolve(code);
      });
      const code = await promise;
      fs.closeSync(fd);
      assertEquals(code, 0);
      const content = Deno.readTextFileSync(tmpFile);
      assertEquals(content, "hello from child\n");
    } finally {
      Deno.removeSync(tmpFile);
    }
  },
});
