node-serialport icon indicating copy to clipboard operation
node-serialport copied to clipboard

Flatpak Support

Open jwillikers opened this issue 2 years ago • 2 comments

💥 Proposal

What feature you'd like to see

Fallback to reading device information from /sys/class/tty when udevadm is not available, such as when run from inside a Flatpak.

Motivation

Flatpak's don't have access to udevadm, so applications using this library can't be Flatpaks and list available devices. See keyboardio/Chrysalis#708.

Pitch

Information like the vendor id and model id is still available by iterating over /sys/class/tty, which seems like a fine fallback when udevadm is not available.

For example, the following information is available for /dev/ttyACM0 from inside a Flatpak.

$ cat /sys/class/tty/ttyACM0/device/uevent
DEVTYPE=usb_interface
DRIVER=cdc_acm
PRODUCT=1209/2301/100 # <----- Keyboardio Model 01!
TYPE=239/2/1
INTERFACE=2/2/0

jwillikers avatar Aug 07 '21 18:08 jwillikers

I like this idea

reconbot avatar Aug 07 '21 19:08 reconbot

I was able to hash out a working solution downstream... but I pretty much have no clue what I'm doing when it comes to Javascript. And that is doubly true for figuring out how to mock out the filesystem calls in order to create some unit tests for this. Here's the code I was able to put together.

const fs = require("fs")
const readline = require("readline")
const path = require("path")

function createReadStreamSafe(filename, options) {
  return new Promise((resolve, reject) => {
    const fileStream = fs.createReadStream(filename, options);
    fileStream.on("error", reject).on("open", () => {
      resolve(fileStream);
    });
  });
}

const ttySysClassPath = "/sys/class/tty";
const productRegex = /^PRODUCT=(?<vendorId>\d+)\/(?<productId>\d+)\/.*/;

function listPortsSysClassTty() {
  return new Promise(async (resolve, reject) => {
    let ports = [];
    let openedDir;
    try {
      openedDir = await fs.promises.opendir(ttySysClassPath);
    } catch (err) {
      console.error(err);
      reject(err);
    }
    for await (const fileDirent of openedDir) {
      const dir = fileDirent.name;
      const dirPath = path.join(ttySysClassPath, dir);

      let stat;
      try {
        stat = await fs.promises.stat(dirPath);
      } catch (err) {
        continue;
      }
      if (!stat.isDirectory()) {
        continue;
      }

      let port = { path: path.join("/dev", dir) };

      let fileStream;
      try {
        fileStream = await createReadStreamSafe(
          path.join(dirPath, "device", "uevent")
        );
      } catch (err) {
        continue;
      }

      const rl = readline.createInterface({
        input: fileStream,
        crlfDelay: Infinity
      });

      for await (const line of rl) {
        const found = line.match(productRegex);
        if (!found) {
          continue;
        }
        port.vendorId = found.groups["vendorId"];
        port.productId = found.groups["productId"];
        ports.push(port);
        break;
      }
    }
    resolve(ports);
  });
}

Basically, Linux lists a bunch of TTY devices under /sys/class/tty, most of which will be disconnected. Here's what that directory structure looks like under /sys/class/tty, shortened for brevity.

$ ls -l /sys/class/tty
total 0
lrwxrwxrwx. 1 root root 0 Aug 11 06:30 console -> ../../devices/virtual/tty/console
lrwxrwxrwx. 1 root root 0 Aug 11 06:30 ptmx -> ../../devices/virtual/tty/ptmx
lrwxrwxrwx. 1 root root 0 Aug 11 06:30 tty -> ../../devices/virtual/tty/tty
lrwxrwxrwx. 1 root root 0 Aug 11 06:30 tty0 -> ../../devices/virtual/tty/tty0
...
lrwxrwxrwx. 1 root root 0 Aug 11 06:30 tty63 -> ../../devices/virtual/tty/tty63
lrwxrwxrwx. 1 root root 0 Aug 11 07:34 ttyACM0 -> ../../devices/pci0000:00/0000:00:14.0/usb1/1-5/1-5.3/1-5.3.3/1-5.3.3:1.0/tty/ttyACM0
lrwxrwxrwx. 1 root root 0 Aug 11 06:30 ttyS0 -> ../../devices/platform/serial8250/tty/ttyS0
...
lrwxrwxrwx. 1 root root 0 Aug 11 06:30 ttyS31 -> ../../devices/platform/serial8250/tty/ttyS31
lrwxrwxrwx. 1 root root 0 Aug 11 06:30 ttyS4 -> ../../devices/pci0000:00/0000:00:1e.0/dw-apb-uart.2/tty/ttyS4

Most of these devices aren't attached to anything, so they contain contents without a device subdirectory like the following.

$ ls -l /sys/class/tty/tty0/
total 0
-r--r--r--. 1 root root 4096 Aug 11 06:30 active
-r--r--r--. 1 root root 4096 Aug 11 08:08 dev
drwxr-xr-x. 2 root root    0 Aug 11 08:08 power
lrwxrwxrwx. 1 root root    0 Aug 11 06:30 subsystem -> ../../../../class/tty
-rw-r--r--. 1 root root 4096 Aug 11 06:30 uevent

Contrast this to the contents of an attached device and you'll notice that a device symlink exists such as shown here.

ls -l /sys/class/tty/ttyACM0/
total 0
-r--r--r--. 1 root root 4096 Aug 11 08:09 dev
lrwxrwxrwx. 1 root root    0 Aug 11 07:30 device -> ../../../1-5.3.3:1.0
drwxr-xr-x. 2 root root    0 Aug 11 08:09 power
lrwxrwxrwx. 1 root root    0 Aug 11 07:30 subsystem -> ../../../../../../../../../../class/tty
-rw-r--r--. 1 root root 4096 Aug 11 07:30 uevent

The device directory contains a bunch of stuff, but the important bit is the uevent file which has the product ID and the vendor ID. Here I show the directory structure below the device directory.

$ ls -l /sys/class/tty/ttyACM0/device/
total 0
-rw-r--r--. 1 root root 4096 Aug 11 08:11 authorized
-r--r--r--. 1 root root 4096 Aug 11 08:11 bAlternateSetting
-r--r--r--. 1 root root 4096 Aug 11 07:30 bInterfaceClass
-r--r--r--. 1 root root 4096 Aug 11 07:30 bInterfaceNumber
-r--r--r--. 1 root root 4096 Aug 11 08:11 bInterfaceProtocol
-r--r--r--. 1 root root 4096 Aug 11 08:11 bInterfaceSubClass
-r--r--r--. 1 root root 4096 Aug 11 08:11 bmCapabilities
-r--r--r--. 1 root root 4096 Aug 11 07:30 bNumEndpoints
lrwxrwxrwx. 1 root root    0 Aug 11 07:30 driver -> ../../../../../../../../bus/usb/drivers/cdc_acm
drwxr-xr-x. 3 root root    0 Aug 11 08:11 ep_81
-r--r--r--. 1 root root 4096 Aug 11 08:11 iad_bFirstInterface
-r--r--r--. 1 root root 4096 Aug 11 08:11 iad_bFunctionClass
-r--r--r--. 1 root root 4096 Aug 11 08:11 iad_bFunctionProtocol
-r--r--r--. 1 root root 4096 Aug 11 08:11 iad_bFunctionSubClass
-r--r--r--. 1 root root 4096 Aug 11 08:11 iad_bInterfaceCount
-r--r--r--. 1 root root 4096 Aug 11 08:11 modalias
drwxr-xr-x. 2 root root    0 Aug 11 08:11 power
lrwxrwxrwx. 1 root root    0 Aug 11 07:30 subsystem -> ../../../../../../../../bus/usb
-r--r--r--. 1 root root 4096 Aug 11 08:11 supports_autosuspend
drwxr-xr-x. 3 root root    0 Aug 11 07:30 tty
-rw-r--r--. 1 root root 4096 Aug 11 07:30 uevent

For completeness, the device/uevent file looks like this:

$ cat /sys/class/tty/ttyACM0/device/uevent
DEVTYPE=usb_interface
DRIVER=cdc_acm
PRODUCT=1209/2301/100 # <----- Vendor ID / Product ID /
TYPE=239/2/1
INTERFACE=2/2/0

jwillikers avatar Aug 11 '21 13:08 jwillikers