Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

No way to read standard input stream #521

Open
guest271314 opened this issue Aug 3, 2024 · 2 comments
Open

No way to read standard input stream #521

guest271314 opened this issue Aug 3, 2024 · 2 comments

Comments

@guest271314
Copy link

I'm experimenting with using llrt as a Native Messaging host.

This is the Native Messaging protocol https://developer.chrome.com/docs/extensions/mv3/nativeMessaging/#native-messaging-host-protocol

Chrome starts each native messaging host in a separate process and communicates with it using standard input (stdin) and standard output (stdout). The same format is used to send messages in both directions; each message is serialized using JSON, UTF-8 encoded and is preceded with 32-bit message length in native byte order. The maximum size of a single message from the native messaging host is 1 MB, mainly to protect Chrome from misbehaving native applications. The maximum size of the message sent to the native messaging host is 4 GB.

So far I've tried using fs/promise and spawn() from child_process without success

function encodeMessage(str) {
  return new Uint8Array([...JSON.stringify(str)].map((s) => s.codePointAt()));
}

import { open } from "node:fs/promises";
// process.env.UV_THREADPOOL_SIZE = 1;

// https://github.com/denoland/deno/discussions/17236#discussioncomment-4566134
// https://github.com/saghul/txiki.js/blob/master/src/js/core/tjs/eval-stdin.js
async function readFullAsync(length, buffer = new Uint8Array(65536)) {
  const data = [];
  while (data.length < length) {
    const input = await open("/dev/stdin");
    let { bytesRead } = await input.read({
      buffer
    });
    await input.close();
    if (bytesRead === 0) {
      break;
    }
    data.push(...buffer.subarray(0, bytesRead));  
  }
  return new Uint8Array(data);
}

async function getMessage() {
  const header = new Uint32Array(1);
  await readFullAsync(1, header);
  const content = await readFullAsync(header[0]);
  return content;
}

async function sendMessage(message) {
  const header = new Uint32Array([message.length]);
  const stdout = await open("/dev/stdout", "w");
  await stdout.write(header);
  await stdout.write(message);
  await stdout.close();
  //global.gc();
}

async function main() {
  while (true) {
    try {
      const message = await getMessage();
      await sendMessage(message);
    } catch (e) {
      process.exit();
    }
  }
}

main();
import { readFileSync, writeFileSync } from "fs";
import { spawn } from "child_process";

const [, filename] = process.argv;

function encodeMessage(str) {
  return new Uint8Array([...JSON.stringify(str)].map((s) => s.codePointAt()));
}

async function getMessage() {

  let pid = await new Promise((resolve, reject) => {
    const p = spawn("pgrep", ["-n", "-f", filename]); //.replace(/\D+/g, "")
    p.on("data", (data) => {
      resolve({data});
    });
    p.on("exit", (code) => {
      reject({code});
    });
  }).catch((e) => e);

  const bash = ["bash", [
    "--posix",
    "-c",
    `
length=$(head -q -z --bytes=4 /proc/${pid}/fd/0 | od -An -td4 -)
message=$(head -q -z --bytes=$((length)) /proc/${pid}/fd/0)
# length=$(dd iflag=fullblock oflag=nocache conv=notrunc,fdatasync bs=4 count=1 if=/proc/${pid}/fd/0 | od -An -td4 -)
# message=$(dd iflag=fullblock oflag=nocache conv=notrunc,fdatasync bs=$((length)) count=1 if=/proc/${pid}/fd/0)
printf "$message"
`,
  ]];
  const qjs = ["/home/user/bin/qjs", [
    "--std",
    "-m",
    "-e",
    `const path = "/proc/${pid}/fd/0";
  try {
    const size = new Uint32Array(1);
    const err = { errno: 0 };
    const pipe = std.open(
      path,
      "rb",
      err,
    );
    if (err.errno !== 0) {
      throw std.strerror(err.errno);
    }
    pipe.read(size.buffer, 0, 4);
    // writeFile("len.txt", size);
    // {error: 'writeFile is not defined'
    const output = new Uint8Array(size[0]);
    pipe.read(output.buffer, 0, size[0]);
    const res = new Uint8Array([...new Uint8Array(size.buffer),...output]);
    std.out.write(res.buffer, 0, res.length);
    std.out.flush();
    std.exit(0);
  } catch (e) {
    const json = JSON.stringify({error:e.message});
    std.out.write(Uint32Array.of(json.length).buffer, 0, 4);
    std.out.puts(json);
    std.out.flush();
    std.exit(0);
  }
`,
  ]];
  let message = await new Promise((resolve) => {
    const p = spawn(bash[0], bash[1], {stdio: "pipe"}); //.replace(/\D+/g, "")
    p.on("data", (data) => {
      resolve(data);
    });
    p.on("exit", (code) => {
      resolve(code);
    });
  });
  return encodeMessage({pid});
}

// https://github.com/denoland/deno/discussions/17236#discussioncomment-4566134
// https://github.com/saghul/txiki.js/blob/master/src/js/core/tjs/eval-stdin.js
function readFullSync(fd, buf) {
  let offset = 0;
  while (offset < buf.byteLength) {
    const data = readFileSync(fd);
    buf.set(offet, data);
    offset += data.length;
  }
  return buf;
}

function sendMessage(json) {
  let header = Uint32Array.from({
    length: 4,
  }, (_, index) => (json.length >> (index * 8)) & 0xff);
  let output = new Uint8Array(header.length + json.length);
  output.set(header, 0);
  output.set(json, 4);
  writeFileSync("/proc/self/fd/1", output);
}

async function main() {
  sendMessage(encodeMessage(process.argv[1]));
  while (true) {
    try {
      const message = await getMessage();
      sendMessage(message);
      break;
    } catch (e) {
      sendMessage(encodeMessage(e.message));
      break;
    }
  }
}

main();
@guest271314
Copy link
Author

I figured it out, using child_process.stdout.on("data", fn)

nm_llrt.js

#!/usr/bin/env -S /home/user/bin/llrt
// llrt Native Messaging host
// guest271314, 8-3-2024
import { writeFileSync } from "node:fs";
import { spawn } from "node:child_process";

const [, filename] = process.argv;

function encodeMessage(str) {
  return new Uint8Array([...JSON.stringify(str)].map((s) => s.codePointAt()));
}

async function getMessage([command, argv]) {
  const message = await new Promise((resolve, reject) => {
    const res = [];
    const subprocess = spawn(command, argv, { stdio: "pipe" }); 
    subprocess.stdout.on("data", (data) => {
      res.push(...data);
    });
    subprocess.stdout.on("close", (code) => {
      resolve(new Uint8Array(res));
    });
    subprocess.stdout.on("exit", (code) => {
      reject(encodeMessage({ code }));
    });
  }).catch((e) => e);
  const cmd = command.split(/[/-]/).pop();
  if (cmd === "bash") {
    return message;
  }
  if (cmd === "qjs") {
    const view = new DataView(message.subarray(0, 4).buffer);
    const length = view.getUint32(0, true);
    // sendMessage(encodeMessage({ length }));
    return message.subarray(4, 4 + length);
  }
}

function sendMessage(json) {
  const header = Uint32Array.from({
    length: 4,
  }, (_, index) => (json.length >> (index * 8)) & 0xff);
  const output = new Uint8Array(header.length + json.length);
  output.set(header, 0);
  output.set(json, 4);
  writeFileSync("/proc/self/fd/1", output);
}

async function main() {
  const pid = String.fromCodePoint.apply(
    null,
    await new Promise((resolve, reject) => {
      let res = [];
      const subprocess = spawn("pgrep", ["-n", "-f", filename], {
        stdio: "pipe",
      });
      subprocess.stdout.on("data", (data) => {
        res.push(...data);
      });
      subprocess.stdout.on("close", (code) => {
        resolve(new Uint8Array(res));
      });
      subprocess.stdout.on("exit", (code) => {
        reject(encodeMessage({ code }));
      });
    }).catch((e) => e),
  ).replace(/\D+/g, "");

  const bash = ["bash", [
    "--posix",
    "-c",
    `
length=$(head -q -z --bytes=4 /proc/${pid}/fd/0 | od -An -td4 -)
message=$(head -q -z --bytes=$((length)) /proc/${pid}/fd/0)
# length=$(dd iflag=fullblock oflag=nocache conv=notrunc,fdatasync bs=4 count=1 if=/proc/${pid}/fd/0 | od -An -td4 -)
# message=$(dd iflag=fullblock oflag=nocache conv=notrunc,fdatasync bs=$((length)) count=1 if=/proc/${pid}/fd/0)
printf "$message"
`,
  ]];
  const qjs = ["/home/user/bin/qjs", [
    "--std",
    "-m",
    "-e",
    `const path = "/proc/${pid}/fd/0";
  try {
    const size = new Uint32Array(1);
    const err = { errno: 0 };
    const pipe = std.open(
      path,
      "rb",
      err,
    );
    if (err.errno !== 0) {
      throw std.strerror(err.errno);
    }
    pipe.read(size.buffer, 0, 4);
    // writeFile("len.txt", size);
    // {error: 'writeFile is not defined'
    const output = new Uint8Array(size[0]);
    pipe.read(output.buffer, 0, size[0]);
    const res = new Uint8Array([...new Uint8Array(size.buffer),...output]);
    std.out.write(res.buffer, 0, res.length);
    std.out.flush();
    std.exit(0);
  } catch (e) {
    const json = JSON.stringify({error:e.message});
    std.out.write(Uint32Array.of(json.length).buffer, 0, 4);
    std.out.puts(json);
    std.out.flush();
    std.exit(0);
  }
`,
  ]];
  while (true) {
    try {
      const message = await getMessage(bash);
      sendMessage(message);
    } catch (e) {
      sendMessage(encodeMessage(e.message));
      break;
    }
  }
}

main();

@guest271314
Copy link
Author

Is there any way to read standard input stream without creating a subprocess?

@guest271314 guest271314 reopened this Aug 4, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant