2
votes

I am opening a PTY (in Python/Linux) and writing to it. I can read from it via minicom perfectly. But, I can't read from it in another Python (or C++) program. Here is a minimized example:

producer.py (opens pty / writes to it):

import os, sys
from time import sleep
master_fd, slave_fd = os.openpty()
print "minicom -D %s" % os.ttyname( slave_fd )
for i in range(0,30): 
    d = str(i % 10)
    os.write( master_fd, d )
    sys.stdout.write( d )
    sys.stdout.flush()
    sleep( 2 )
os.close( slave_fd )
os.close( master_fd )
print "\nDone"    

consumer.py (tries to open/read):

import os, sys
from time import sleep
pts=raw_input("Enter pts number:")
while True:
    fd=0
    try:
        fd=os.open('/dev/pts/%d' % (pts,), 
            os.O_RDONLY | os.O_NONBLOCK )
        sys.stdout.write( os.read(fd, 1 ) )  
        sys.stdout.flush()       
    except Exception as e: print e
    if fd: os.close(fd)    
    sleep(1)        

The result of the read is always:

[Errno 11] Resource temporarily unavailable

If I read in blocking mode, it just blocks until the producer terminates. Then, it says the file doesn't exist.

I have spent days fiddling with trying to set various modes, permissions, locks, etc. and nothing seems to get me anywhere. This sort of thing works fine with regular files easily. Also, note again that minicom can read the pty without a hitch. Further, using lsof I can see that both minicom and my consumer.py script indeed open the file - it is just the read that doesn't work in the python example. So what's the minicom secret? I tried finding such in the minicom source code, but I did not succeed in finding anything I could use.

Ideally, my producer would make it easy to open and read (like in my consumer example), but if I can see this work, I'm open to modifying either end...

2

2 Answers

2
votes

What makes you think you cannot open the PTY? Nothing in your code provides information about which system call failed.

The most likely thing is that the os.read() call fails with error code EAGAIN (aka EWOULDBLOCK) because you have opened the PTY in non-blocking mode. There's no data to read because a PTY is a tty, and ttys are initially in "cooked" mode, which means that no input is passed to the consumer until either an end of line character or some interrupt character is sent. Minicom probably puts the pty into "raw" mode with a termios call, and you should do that, too.

I would guess that you don't really want to put the PTY into non-blocking mode. Unless you set up event polling or a select loop, you're going to repeatedly get EAGAIN "errors" (which are not really errors) and you really don't want to wait a full second before you try again. (Nor do you really want to close and reopen the PTY.) You would be better advised to leave the PTY in blocking mode but configure it to return immediately on each keystroke (again, with termios).

2
votes

My primary hangup was in the pty settings. See my comment under @rici's answer.

Revised producer.py:

import os, sys
from time import sleep
import fcntl
import termios 

# create a new Psdeuo Terminal (pty), with a dynamically 
# assigned path to it, and display the minicom command to 
# open it as a test consumer
master_fd, slave_fd = os.openpty()
print "minicom -D %s" % os.ttyname( slave_fd )

# termios attribute index constants
iflag  = 0
oflag  = 1
cflag  = 2
lflag  = 3
ispeed = 4
ospeed = 5
cc     = 6
# get current pty attributes
termAttr = termios.tcgetattr( master_fd )
# disable canonical and echo modes       
termAttr[lflag] &= ~termios.ICANON & ~termios.ECHO
# disable interrupt, quit, and suspend character processing 
termAttr[cc][termios.VINTR] = '\x00' 
termAttr[cc][termios.VQUIT] = '\x00'
termAttr[cc][termios.VSUSP] = '\x00'
# set revised pty attributes immeaditely
termios.tcsetattr( master_fd, termios.TCSANOW, termAttr )

# enable non-blocking mode on the file descriptor
flags = fcntl.fcntl( master_fd, fcntl.F_GETFL ) 
flags |= os.O_NONBLOCK               
fcntl.fcntl( master_fd, fcntl.F_SETFL, flags )

# write some example data for a couple of minutes
for i in range(0,60): 
    d = str(i % 10)
    os.write( master_fd, d )
    sys.stdout.write( d )
    sys.stdout.flush()
    sleep( 2 )

# release the resources     
os.close( slave_fd )
os.close( master_fd )
print "\nDone"

Revised consumer.py:

import os, sys
from time import sleep
from errno import EAGAIN, EBUSY

ERRORS_TO_IGNORE = [EAGAIN, EBUSY]

# the PTS will be dynamically assigned to the producer,
# so the consumer needs to have that supplied
pts=raw_input("Enter pts number:")

fd=None
print "Press Ctrl+Z to exit"
while True:
    sleep(1)      
    # if the pty is not open, attempt to open it
    # in readonly, non-blocking mode
    try:
        if not fd:
            fd=os.open('/dev/pts/%s' % (pts,), 
                       os.O_RDONLY | os.O_NONBLOCK )
    except Exception as e:
        print e
        if fd: fd = os.close(fd)     
        continue         
    # attempt to read/pop a character from the stream
    # and then display it in this terminal                 
    try:        
        c = os.read( fd, 1 )
        sys.stdout.write( str(c) )  
        sys.stdout.flush()
    except Exception as e:
        # ignore some "normal" / "race condition" errors
        if( isinstance(e, OSError) and
            e.errno in ERRORS_TO_IGNORE ):pass
        else : print e