Why does os.system(command, [args]) return value get converted from 0 or 1 to 512 or 513?

I’m using this in js shell

(os.system("pgrep", ["-f", scriptArgs[0]])

The return value when pgrep finds a match is the number 512, not 0. When there is no match the return value is 513, not 1.

I can get 0 or 1 respectively using bitwise operators

if ((os.system("pgrep", ["-f", scriptArgs[0]]) & 0xff) === 1) {
  // Do stuff

I’m curious where and why the conversion happens. Thanks.

1 Like

It looks like we return the raw return value from the system function here but to get the child process exit code it should be passed through WIFEXITED / WEXITSTATUS.

1 Like

I’m on Linux if that means anything in this case.

It looks like we return the raw return value from the system function here but to get the child process exit code it should be passed through WIFEXITED / WEXITSTATUS .

Does that mean this is working as intended to return 512 and 513 and not 0 and 1?

Here’s the relevant part of the code https://github.com/guest271314/native-messaging-spidermonkey-shell/blob/main/nm_spidermonkey.js#L53C3-L61.

  while (true) {
    // Terminate current process when chrome-extension://<ID> is not a running process
    // \x00 or \x01, 512 or 513
    if (os.system("pgrep", ["-f", scriptArgs[0]]) == 513) {
    const message = getMessage();

Interestingly os.file.writeTypedArrayToFile() doesn’t work when called directly before break in the if statement.

Yes, it is working as intended, though there aren’t a whole lot of explicit intentions for os.system(). The “advantage” of returning the raw code is that you can distinguish between the process exiting with a status code vs the process being killed by a signal. Compare:

  js -e 'print(os.system("sleep 1000"))' & sleep 1 && killall sleep
  js -e 'print(os.system("sleep 1000"))' & sleep 1 && killall sleep
  js -e 'print(os.system("/bin/false"))'
  js -e 'print(os.system("/bin/true"))'

512 surprises me though. Given

#define	__WEXITSTATUS(status)	(((status) & 0xff00) >> 8)

That corresponds to an exit status of 2, which for pgrep means an invalid command line rather than not finding any matching processes:

       0      One or more processes matched the criteria. For pkill and pidwait, one or more processes must also have been successfully signalled or waited for.
       1      No processes matched or none of them could be signalled.
       2      Syntax error in the command line.
       3      Fatal error: out of memory etc.

If I’m reading /usr/include/bits/waitstatus.h correctly, 513 would mean WIFEXITED(513) is false, WIFSIGNALED(513) is true, and WTERMSIG(513) is 1 (SIGHUP). I don’t really know what having both a 2 in the status “field” and a 1 in the signal “field” means.

On the other hand, it seems like maybe bash may mutilate exit status codes (and system() runs its argument under /bin/sh). So I don’t really know how all this works in the end. os.system cowardly just passes the raw information through.

Ah! os.system("pgrep", ["-f", "boo"]) is incorrect. os.system calls system which just takes a string that it passes to /bin/sh. So that 2nd argument is being ignored. You want os.system("pgrep -f " + scriptArgs[0]).

Though you said you were getting different result codes depending on whether it does or does not find the process, which doesn’t make sense if the argument is being ignored.

1 Like

I also don’t have an answer for why os.file.writeTypedArrayToFile() doesn’t work in that position. There’s an RAII guard that should close the output file, so it’s not a buffering problem. (Unless all my answers here are wrong because you’re using an older version with different behavior, but I would not expect any of this code to have changed in a long time.) I would probably run it under strace to see what syscalls are happening.

The steps to reproduce everything I describe above are here https://github.com/guest271314/native-messaging-spidermonkey-shell. Specially here https://github.com/guest271314/native-messaging-spidermonkey-shell/blob/main/nm_spidermonkey.js.

os.system(`pgrep -f "${scriptArgs[0]}"`);

results in js exiting with an error

Error when communicating with the native messaging host.

There’s no terminal or TTY. I’m experimenting and testing SpiderMonkey as a Native Messaging host.

This is the protocol and working examples in C, C++, Python, Node.js, Deno, Bun, QuickJS, txiki.js, WebAssembly/WASI, Bash, nm_host is JavaScript runtime agnostic tested the same code using node, deno, bun, https://github.com/guest271314/NativeMessagingHosts?tab=readme-ov-file#native-messaging-documentation.

This is my local testing code, heavily commented. I started testing V8 and SpiderMonkey as Native Messaging hosts around a year ago. Precisely because I knew it would be challenging due to lack of standard input/outout/error streams in JavaScript. There’s zero compatibility or interoperability as to reading STDIN or writing to STDOUT for JavaScript(/WebAssembly) engines and runtimes.

#!/usr/bin/env -S JS_STDERR=err.txt /home/user/.jsvu/engines/spidermonkey/spidermonkey --enable-import-assertions --enable-uint8array-base64 --enable-arraybuffer-resizable --enable-top-level-await
// /home/user/.jsvu/engines/spidermonkey/spidermonkey
// SpiderMonkey Shell Native Messaging host (W.I.P.)
// guest271314 7-7-2023, 6-16-2024

function encodeMessage(str) {
  return new Uint8Array([...str].map((s) => s.codePointAt()));
// Call readline() N times to catch `\r\n\r\n"` from 2d port.postMessage()

function getMessage() {
  let stdin;
  while (true) {
    stdin = readline();
    //const previous = redirect(`${reads++}.txt`);
    if (stdin !== null) {
  // \\x([0-9A-Fa-f]{2,4})|\d+\\x([0-9A-Fa-f]{2,4})|^\r\n\r\n
  let data = `${stdin}`.replace(/[\r\n]+|\\x([0-9A-Fa-f]{2,4})/gu, "")
    .replace(/[^A-Za-z0-9\s\[,\]\{\}:_"]+/igu, "")
    .replace(/^"rnrn/gu, "")
    .replace(/^[#\r\n\}_]+(?=\[)/gu, "")
    .replace(/^"(?=["\{]+)|^"(?!"$)/gu, "") // |^"(?!"$) // sendMessage("{}"):{}"
    .replace(/^\[(?=\[(?!.*\]{2}$))/gu, "")
    .replace(/^\{(?!\}|.+\}$)/gu, "")
    .replace(/^[0-9A-Z]+(?=[\[\{"])/igu, "") // [^A-Za-z0-9\s\[,\]\{\}:_"]
    .replace(/^[\]\}](?=\[)/i, "")
  //if (/^\[\[/u.test(d) && !/\]\]$/.test(d)
  //  || (/^\{/u.test(d) && !/\}$/u.test(d))
  //  || (/^\[\[/u.test(d) && !/\]\]/u.test(d))
  //) {
  //d = d.slice(1)
  //if (d[0] === `###"\r\n\r\n"####`) d = d.slice(1)

  //let previous = redirect("length.txt");
  //redirect(previous); // restore the redirection to stdout
  os.file.writeTypedArrayToFile("input.txt", encodeMessage(data));
  // putstr(d);
  return encodeMessage(data);
  //const re = redirect(`${reads++}.txt`);
  //redirect(re); // restore the redirection to stdout
  const data = encodeMessage(stdin.replace(/\r\n+/g, ``)); // new Uint8Array([...stdin].map((s) => s.codePointAt()));
  const view = new DataView(data.buffer);
  const length = view.getUint32(0, true);
  const message = data.subarray(4);

  //if (done) {
  //  return void 0;
 // if (!done) {
    // https://stackoverflow.com/a/52434176
    const previous = redirect("length.txt");
     redirect(previous); // restore the redirection to stdout
     os.file.writeTypedArrayToFile("input.txt", message);
     // done = true;
    return message;

function sendMessage(message) {
    new Uint32Array([message.length]),
  os.file.writeTypedArrayToFile("/proc/self/fd/1", message);
  // os.file.close("/proc/self/fd/1");

function main() {
  // Send help() to client
  // const previous = redirect("help.txt");
  // putstr(help());
  // redirect(previous); // restore the redirection to stdout
  // const h = read("help.txt", "binary");
  // sendMessage(encodeMessage(JSON.stringify([...h])));
  // TODO: Make this persistent, comparable to JavaScript runtimes
  // sendMessage(encodeMessage(JSON.stringify("TEST")));
  //const file = os.file.readFile("help.txt");
  const pid = os.getpid();
  //const ppid = os.system("pgrep", ["-f", "chrome"]); // 3035687
  //os.file.writeTypedArrayToFile("ppid.txt", encodeMessage(JSON.stringify(ppid)));
  //if (ppid == 512) sendMessage(encodeMessage(JSON.stringify("yup")));
  sendMessage(encodeMessage(JSON.stringify({scriptPath, scriptArgs})));
  while (true) {   
    // \x00 or \x01, 512 or 513
    const client = os.system("pgrep", ["-f", scriptArgs[0]]);

    if (client & 0xff === 1) {
    os.file.writeTypedArrayToFile("exit.txt", encodeMessage(JSON.stringify(client & 0xff)));
    const message = getMessage();

try {
} catch (e) {

I will point out that I’m calling os.system() and pgrep because js remains active when all chrome instances are terminated. I have to do the same thing for V8’s d8.

I encountered similar behaviour using wasmtime to execute C and C++ compiled to WASM, see Note 10

  1. Make sure *.sh files are executable executable; for WAT embedded in nm_c_wat.sh we use kill_wasmtime.sh to terminate wasmtime using polling of nm_c_wat.sh because process substitution keeps wasmtime running after the Native Messaging host is disconnected and exits.
while pgrep -f nm_c_wat.sh > /dev/null
  sleep 1
killall -9 wasmtime
exit 0

and https://github.com/guest271314/native-messaging-webassembly/blob/main/nm_c_wat.sh#L6290-L6291 where above that code in Bash script is WAT embedded

./kill_wasmtime.sh &
./wasmtime <(printf '%s' "$script")

I put the projects of implementing SpiderMonkey and V8 as Native Messaging hosts on hold for year because other projects took precedence. And I kind of knew the challenge would be a real one, so this has been something to look forward to to make the requirement so.

It would be so much simpler if JavaScript as a whole, engines, runtimes, interpreters, et al. just read STDIN into a resizable ArrayBuffer, e.g., https://github.com/guest271314/NativeMessagingHosts/blob/main/nm_host.js#L45C1-L69, instead of for every engine and runtime processing standard input, output, and error streams is completely different. It’s 2024… We have exotic objects, and no end in sight to proposals ranging from dealing with UI to Float16Array, yet no specified and agreed upon means to process STDIN and write to STDOUT

async function* getMessage() {
  let messageLength = 0;
  let readOffset = 0;
  for await (let message of readable) {
    if (buffer.byteLength === 0) {
      for (let i = 0; i < 4; i++) {
        view.setUint8(i, message[i]);
      messageLength = view.getUint32(0, true);
      message = message.subarray(4);
    buffer.resize(buffer.byteLength + message.length);
    for (let i = 0; i < message.length; i++, readOffset++) {
      view.setUint8(readOffset, message[i]);
    if (buffer.byteLength === messageLength) {
      yield new Uint8Array(buffer);
      messageLength = 0;
      readOffset = 0;


Whether I use os.system() or os.spawn() I’m getting results when I shouldn’t be.

When I close all instances of the Chrome extension and use os.spawn() I’m still getting a PID or something

$ /home/user/.jsvu/engines/spidermonkey/spidermonkey
js> os.spawn("pgrep -f chrome-extension://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/")
js> 3087055
os.system("pgrep -f chrome-extension://xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/")

I don’t understand why js is exiting when I use os.system("pgrep -f chrome") to check if any chrome instances are still running.

Alright I figured it out. pgrep by default prints to stdout. The Native Messaging protocol expects message length in native byte order preceding message, which pgrep doesn’t do, making the host exit.

In this case os.system() returns 256 (the buffer size from C++ source code?) when piping to /dev/null, else 0, which we will use true or false in the if condition.

os.file.writeTypedArrayToFile() still doesn’t work in this case.


    if (!!os.system(`pgrep -fln ${scriptArgs[0]} > /dev/null`)) {
      os.file.writeTypedArrayToFile("exit.txt", encodeMessage(JSON.stringify(client)));