Open jwillikers opened 3 years ago
I like this idea
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
💥 Proposal
What feature you'd like to see
Fallback to reading device information from
/sys/class/tty
whenudevadm
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 whenudevadm
is not available.For example, the following information is available for
/dev/ttyACM0
from inside a Flatpak.