0

I am using the tty subsystem of Linux to emulate serial ports. The emulated serial ports are used by an application that expects a physical serial port. In my case, the application is running under Docker, but I don't think Docker is relevant to this question. However, I am having an issue in which the application that manages the emulated serial ports is locking up; I believe it is blocking on a call to write. I'd like to figure out if there's a way to make the call not block, or to detect when it would block.

I have a Python application that manages the emulated serial ports and performs broadcasting between them (i.e. if one application writes to its serial port, then all other serial ports in the network receive the written message). The application looks something like this:

#!/usr/bin/env python3

import os
import pty
import selectors
import signal
import tty

# read the configuration
all_apps = ...


selector = selectors.DefaultSelector()


# create the emulated serial ports
master_fds = { }

for app in all_apps:
    app_master, app_slave = pty.openpty()

    tty.setraw(app_master)
    master_fds[app] = app_master
    selector.register(app_master, selectors.EVENT_READ, app_master)

    # make the symlink that gets picked up by the application
    os.symlink(os.ttyname(app_slave), "/run/my-app/{}/ttyS0".format(app))


# exit on SIGINT or SIGTERM
run_flag = True

def signal_handler(sig, stack):
    global run_flag
    run_flag = False

signal.signal(signal.SIGINT, signal_handler)
signal.signal(signal.SIGTERM, signal_handler)


# main loop
while run_flag:
    events = selector.select(timeout = 1)

    for key,mask in events:
        fd = key.data

        # read up to 1024 bytes of data from the app that sent the message
        data = os.read(fd, 1024)

        for app_fd in master_fds.values():
            # don't broadcast to the application that sent the message
            if fd == app_fd:
                continue

            # send the message to each application
            os.write(app_fd, data)


# cleanup
for key in master_fds.keys():
    os.remove("/run/my-app/{}/ttyS0".format(key))
    os.close(master_fds[key])

Generally this script works well - when the applications start, they pick up the pty slave as a serial port, and messages are broadcasted between the applications. However, at some point, the script may hang - I believe it is on a call to os.write, which is only ever called when sending an application to one of the pty masters. In this state, the script doesn't respond to signals (since run_flag is only checked when the loop runs, but os.write is blocking the loop).

The only explanation that makes sense to me is that at least one of the applications isn't properly reading from its slave end of the pty. If this is true, then I can imagine that some buffer in the kernel which is backing the pty is becoming full, and since the call to write would overflow the buffer, the call blocks until the buffer is drained enough (which never happens).

I found that I am sometimes able to drain the emulated serial port by running cat on the slave end of the port. However, I need the application to be reliable enough to not block when the call to os.write would normally block, without human intervention.

Is there a way to measure (for example, in my Python script) the remaining capacity of the buffer backing a pty, to avoid a call to write from blocking on a pty master?

1

I think I found one solution, although I don't know if it's necessarily ideal.

We can edit the script to set the pty masters into non-blocking mode:

import fcntl

...

for app in all_apps:
    app_master, app_slave = pty.openpty()

    flags = fcntl.fcntl(app_master, fcntl.F_GETFL)
    flags |= os.O_NONBLOCK
    fnctl.fcntl(app_master, fcntl.F_SETFL, flags)

Then, whenever we write to a master, if the write would normally block, we can catch BlockingIOError to continue:

            # (try to) send the message to each application
            try:
                os.write(app_fd, data)
            except BlockingIOError:
                print("Caught BlockingIOError")

This doesn't solve the fundamental problem which is that the applications might misbehave and not read their serial ports, but with this solution the script will at least continue to do its job. The cost is that some data may be lost (broadcasted to applications which aren't draining their port).

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.