/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";

// This test is a simple check that terminated processes are cleaned
// up.  It's currently Linux only, and only launches one content
// process (no other types, no use of multiple processes to try to
// provoke race conditions, etc.), then causes it to exit and waits
// until it no longer exists.
//
// If the process continues to exist, including as a zombie, the test
// will time out.  The test repeatedly polls the process status and
// logs it, so it will be obvious if it was a regular hang or a
// process management failure.

const { ctypes } = ChromeUtils.importESModule(
  "resource://gre/modules/ctypes.sys.mjs"
);

const libc = ctypes.open("libc.so.6");
const { O_PATH, O_CLOEXEC } = ChromeUtils.getLibcConstants();
const c_open = libc.declare(
  "open",
  ctypes.default_abi,
  ctypes.int,
  ctypes.char.ptr,
  ctypes.int
);
const c_close = libc.declare(
  "close",
  ctypes.default_abi,
  ctypes.int,
  ctypes.int
);

async function millisleep(ms) {
  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
  await new Promise(resolve => setTimeout(resolve, ms));
}

// pidfd is a fd for the /proc/{pid} subdir, to avoid the small
// possibility of spurious test failures if the pid is reused.
async function getProcStatus(pidfd) {
  let bytes;
  try {
    bytes = await IOUtils.read(`/proc/self/fd/${pidfd}/stat`, {
      maxBytes: 4096,
    });
  } catch (ex) {
    if (!DOMException.isInstance(ex)) {
      throw ex;
    }
    // Hopefully this is ENOENT
    return "dead";
  }
  let rparen = bytes.lastIndexOf(41 /* ')' */);
  if (rparen < 0 || rparen >= bytes.length - 2) {
    throw new Error(`bad proc_pid_stat line`);
  }
  let state = String.fromCodePoint(bytes[rparen + 2]);
  if (state == "Z") {
    return "zombie";
  }
  return "running";
}

add_task(async function () {
  let pidfd = -1;
  let pid = -1;
  try {
    // 1. Launch content process
    await BrowserTestUtils.withNewTab(
      {
        gBrowser,
        url: "https://example.com/",
        forceNewProcess: true,
      },
      async function (browser) {
        pid = browser.frameLoader.remoteTab.osPid;
        if (pid <= 0) {
          throw new Error(`bad pid: ${pid}`);
        }
        ok(true, `got content process pid ${pid}`);
        // 2. Open procfs dir
        pidfd = c_open(`/proc/${pid}`, O_PATH | O_CLOEXEC);
        if (pidfd < 0) {
          throw new Error(`failed to open pidfd: errno ${ctypes.errno}`);
        }
        ok(true, `opened pidfd ${pidfd}`);
        Assert.equal(await getProcStatus(pidfd), "running");
        // 3. Shut down content process
      }
    );
    // 4. Check the process status until it's gone
    while (true) {
      let state = await getProcStatus(pidfd);
      dump(`process status: ${state}\n`);
      if (state == "dead") {
        break;
      }
      await millisleep(333); // 3 Hz
    }
    ok(true, "process eventually exited");
  } finally {
    if (pidfd >= 0) {
      let rv = c_close(pidfd);
      Assert.greaterOrEqual(rv, 0, "closed pidfd");
    }
  }
});
