Issue opening/closing character devices in Node.js #7101

Closed
bminer opened this Issue Feb 11, 2014 · 37 comments

7 participants

@bminer

I am having issues opening/closing character devices in Node. How does one properly open and close a character device (i.e. a serial port or TTY device) for reading and writing?

Here's my little program that doesn't quite work as expected:

var fs = require("fs");
fs.open("/dev/ttyUSB0", "r+", function(err, fd) {
    if(err) throw err;
    var input = fs.createReadStream("/dev/ttyUSB0", {"fd": fd});
    input.on("data", function(chunk) {
        console.log("in:", chunk);
    });
    var out = fs.createWriteStream("/dev/ttyUSB0", {"fd": fd});
    console.log("out:", "ATZ\r\n");
    out.write("ATZ\r\n");
    setTimeout(function() {
        console.log("Closing file...");
        fs.close(fd, function(err) {
            console.log("File has been closed", err);
            // At this point, Node will just hang
        });
    }, 5000).unref();
});

The output of the program is something like this:

out: ATZ

in: <Buffer 41 54 5a 0a>
in: <Buffer 0a>
in: <Buffer 0a>
in: <Buffer 4f 4b 0a>
in: <Buffer 0a>
Closing file...
File has been closed null

Everything looks okay, but Node does not exit, as expected; rather, Node simply hangs at this point. Why? Is this a bug?

This problem exists even when opening a TTY terminal (i.e. /dev/tty2 for reading/writing)

@bminer

Here is an even simpler program showing this bug:

var fs = require("fs");
var input = fs.createReadStream("/dev/tty2");
setTimeout(function() {
    console.log("Closing file...");
    input.pause();
    input.destroy();
    console.log("About to exit...");
    process.exit(0);
    console.log("This should not print.");
}, 5000);

In this case, the following output is printed:

Closing file...
About to exit...

Then Node hangs (it does not exit).

@rlidwka
Node.js Foundation member

Then Node hangs (it does not exit).

confirmed, bug seems to be present on both 0.10.21 and 0.11.11

@indutny indutny added a commit to indutny/node that referenced this issue Feb 13, 2014
@indutny indutny zlib: introduce pending close state
zlib should not crash in `close()` if the write is still in progress.

fix #7101
0f2efa2
@indutny
Node.js Foundation member

Should be fixed by #7111

@indutny
Node.js Foundation member

Though, I must admit that close() is internal undocumented method ;)

@indutny
Node.js Foundation member

Found the source of the issue: it is because of the blocking nature of ttys in libuv. Don't know about v0.10, but v0.11 for sure blocks in a read() call in a thread pool, while main thread blocks in uv_thread_join(): waiting for worker IO threads to terminate.

If you'll hit enter - it should exit. I'll see what I could do to fix it.

@bminer

@indutny - Thanks for the update. Why are worker I/O threads blocked on a read() call after the file or I/O stream has been closed? In other words, why doesn't the read() call fail once the file descriptor is closed / cleaned up?

I don't know if I quite understand how read() works in Linux...

@indutny indutny added a commit that referenced this issue Feb 17, 2014
@indutny indutny zlib: introduce pending close state
zlib should not crash in `close()` if the write is still in progress.

fix #7101
829a9b8
@indutny indutny added a commit that closed this issue Feb 18, 2014
@indutny indutny zlib: introduce pending close state
zlib should not crash in `close()` if the write is still in progress.

fix #7101
829a9b8
@indutny indutny closed this in 829a9b8 Feb 18, 2014
@indutny indutny reopened this Feb 18, 2014
@indutny
Node.js Foundation member

@bminer after some consideration, I came to conclusion that this is incorrect way to read from a character device. This kind of thing isn't actually a file and may block for a considerable long time if you would wish to read from it. The proper way would be to either instantiate http://nodejs.org/api/tty.html#tty_class_readstream or http://nodejs.org/api/net.html#net_new_net_socket_options with the result of fs.open as an fd option.

@indutny indutny closed this Feb 18, 2014
@bminer

@indutny - I figured as much; however, there is no clear Node API to read from character devices at this time...

What are the next steps?

@indutny
Node.js Foundation member

Have you tried creating new net.Socket({ fd: fs.open('/dev/...') })?

@tjfontaine

probably want openSync just to be clear

@indutny
Node.js Foundation member

oh, that's right, sorry.

@bminer

@indutny - Using new net.Socket({ fd: fs.open('/dev/...') }) causes an Error:

net.js:50
  throw new TypeError('Unsupported fd type: ' + type);
        ^
TypeError: Unsupported fd type: TTY
    at createHandle (net.js:50:9)
    at new Socket (net.js:156:20)
    at Object.<anonymous> (/node_issue_7101.js:4:14)
    at Module._compile (module.js:456:26)
    at Object.Module._extensions..js (module.js:474:10)
    at Module.load (module.js:356:32)
    at Function.Module._load (module.js:312:12)
    at Function.Module.runMain (module.js:497:10)
    at startup (node.js:119:16)
    at node.js:901:3

Node version 0.10.22

@bminer

Also, I should mention that I've tried the http://nodejs.org/api/tty.html#tty_class_readstream route to no avail. I'm not quite sure how this would work either since the APIs aren't really exposed.

@indutny
Node.js Foundation member

Oh, right what about var t = new tty.ReadStream(fs.openSync('/dev/tty', 'r')) ? Seems to be working fine for me.

@indutny
Node.js Foundation member

But I see what you mean, documentation is incomplete on this topic. Would you mind submitting issue or pull request about that?

@bminer

@indutny - Yeah! Actually, that works great! Problem is the API docs, I suppose...

A net.Socket subclass that represents the readable portion of a tty. In normal circumstances, process.stdin will be the only tty.ReadStream instance in any node program (only when isatty(0) is true).

@bminer

Also... what is the difference between tty.ReadStream and tty.WriteStream? Why are there two classes when they both inherit from net.Socket? Sockets are readable and writable. In fact, writing to the tty.ReadStream socket actually writes to the character device.

@indutny
Node.js Foundation member

Readable are non-blocking and writable are blocking. I think ReadStream may be fine for now. Would you mind submitting doc fix?

@bminer

@indutny - Sure, I can fix the docs, but what about writing to the character device? If I use tty.WriteStream, I get something like:

node: ../deps/uv/src/unix/core.c:701: uv__io_stop: Assertion `loop->watchers[w->fd] == w' failed.
Aborted

Alternatively, I can call .write(...) on the tty.ReadStream and it will write to the character device just fine. Any idea why?

@bminer

Just thinking... could this be because I am using tty.ReadStream and tty.WriteStream on the same file descriptor? I just opened the device once using "r+" flags. Thoughts?

@indutny
Node.js Foundation member

Its because you are using the same fd for both of them.

@indutny
Node.js Foundation member

I guess we should probably dup fd, @saghul thoughts?

@bminer

Also, it would appear that Node will hang if only the file descriptor is closed. For example:

var fd, socket = new tty.ReadStream(fd = fs.openSync("/dev/tty2", "r") );
setTimeout(fs.closeSync.bind(fs, fd), 1000);
// Node will hang

As opposed to:

var fd, socket = new tty.ReadStream(fd = fs.openSync("/dev/tty2", "r") );
setTimeout(socket.destroy.bind(socket), 1000);
// Node will not hang and will exit properly

I don't think that the same happens for normal files. What are your thoughts on this? Perhaps, closing the fd of a character device should cause the Readable stream to get EOF.

@saghul
Node.js Foundation member

I guess we should probably dup fd, @saghul thoughts?

Not sure it that's our silver bullet. The same limitation applies to poll handles, FWIW. About the blocking-ness issue, we need to think about how to solve this The Right Way (TM) once 0.12 is out, IMHO.

@indutny
Node.js Foundation member

@saghul at least we need a concept of both readable/writable tty.

@bminer

@indutny - What's this status of this issue? Any chance that this could be re-opened?

@indutny indutny reopened this Mar 7, 2014
@indutny
Node.js Foundation member

Ok, if you insist :)

@bminer

Hello again. I wanted to follow up on this issue to see what I can do to help. I'd like to document the proper way to interact with character devices. Is this the best way?

const CHAR_DEVICE = "/dev/tty2";
var fs = require("fs")
    , tty = require("tty");
var input = new tty.ReadStream(fs.openSync(CHAR_DEVICE, "r") );
input.setRawMode(true);
var output = new tty.WriteStream(fs.openSync(CHAR_DEVICE, "w") );
input.on("data", function(chunk) {
    console.log("Read chunk from CHAR_DEVICE:", chunk);
});
output.write("This is a test!\n");
setTimeout(function() {
    console.log("Closing file...");
    input.end();
    output.end();
    console.log("Exiting...");
}, 5000);

The above seems to work OK to me. If this is the preferred method of talking to a TTY, I think that the API docs may need a bit of work. I'd be willing to re-work them and open a PR. Please let me know your thoughts. Thanks!

EDIT: Also, while I'm thinking about this, what is the preferred way to set TTY options on a serial port (baud rate or partity, for example)? In Linux, Node could spawn stty or something... is there a decent cross-platform solution?

@bminer bminer referenced this issue in bminer/trivial-port Apr 1, 2014
Closed

Weird blocking and issues closing the serial port #2

@saghul
Node.js Foundation member

That looks ok to me. It still looks a bit weird to me that we need to use fs.openSync for a "non fs operation", we just need the fd. The idea of doing stat on uv_fs_open and returning an error if the path isn't a regular file crossed my mind, but hell would break loose, it seems :-(

EDIT: Also, while I'm thinking about this, what is the preferred way to set TTY options on a serial port (baud rate or partity, for example)? In Linux, Node could spawn stty or something... is there a decent cross-platform solution?

We don't have a way to do that currently.

@bminer

Hello again. I found out that tty.WriteStream blocks the main event loop in Node while writing. If you're writing a lot of data in one sitting, that can be a few seconds of blocking.

Does that sound right? And if so, is there any way to write data to a TTY in a separate thread? Can I use the tty.ReadStream instance to write to the TTY instead?

For me, I am writing a lot of data to a serial port at 9600 baud... this causes my whole web server to freeze for a bit. Connection timeouts occur, etc. No good.

@indutny
Node.js Foundation member

I think it should be no longer a problem with v0.11 release. Could you please give it a try?

@misterdjules

@bminer Is that still an issue with the latest 0.11 release?

@bminer

@misterdjules - I don't know what I don't know.
@indutny - What do you believe has been fixed in latest 0.11? Do filesystem read/write calls and network operations (i.e. DNS lookups) use separate threads now? Or (better question), is it possible to safely read/write to a serial port/TTY in Node 0.11 in a non-blocking way?

@misterdjules

@bminer I had read the whole (long) thread too quickly while triaging issues, and after another read, I realize that my question doesn't help. I'm sorry for the confusion.

I don't have an answer to "what would be the best way" to achieve what you're trying to do, but I'm going to try to give you some pointers that may help you answer some of your other questions.

In node's v0.12 branch, it seems that writing to a tty.writeStream is not a blocking operation, unless the current terminal cannot be reopened. This logic seems a bit bogus to me, since the file descriptor in uv_tty_init does not necessarily point to /dev/tty (like in your case when you're creating a tty.WriteStream to write to a serial TTY). You might want to check if your tty.WriteStream's underlying fd is incorrectly set to non-blocking mode there.

Another thing that could lead to a big write is that when writing a string to a buffer through a tty.WriteStream, node will first try to write immediately if the data to write isn't too big. In your case, if you're writing less than 16KB of data, but still a large number of bytes, on a slow serial connection, it could lead to some delay.

I'm not too familiar though with this part of the code base yet, so I would take my suggestions with a grain of salt. It could be an interesting avenue to explore.

It should be possible to a write to a TTY in a separate thread. One way to do that would be to write a binary node add-on that uses libuv's thread pool to write to it. However, using a fs.WriteStream instance to write to your TTY (like you showed us in one of your previous comments) would basically achieve that, since fs.WriteStream operations run on libuv's thread pool.

Regarding your question about file system operations, they happen on libuv's thread pool, unless one uses the "sync" variants (e.g fs.readFileSync).

DNS lookups (if we're talking about dns.lookup and not dns.resolve*) also happen on libuv's thread pool. I've recently created a PR that tries to improve the dns' module documentation by adding some information about this specific topic.

I hope it helps!

@saghul
Node.js Foundation member

This might be relevant here: joyent/libuv#1580

@bminer

Interesting... I don't know enough about this topic, so I'm going to hang back and wait for things to transpire. I'm hoping that would be cool with everyone.

I'm just a measly app developer who wants a Node.js serial port library that works well.

In addition, I'm griping here because file system I/O is shared with network I/O on the same thread pool. To me, that seems rather foolish since poor network connectivity can translate to slow/unresponsive file system I/O (see issue #2868).... and vice versa.

@jasnell
Node.js Foundation member

Closing due to lack of activity and it's not clear if there's anything actually to do. Can reopen again if necessary

@jasnell jasnell closed this Jun 24, 2015
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment