Shell – How to Use and Understand PTY in Shell

io-redirectionptyshellterminaltty

It seems that a simple shell redirect from /dev/ptmx gets me a new pseudo-terminal.

$ ls /dev/pts; ls /dev/pts </dev/ptmx
0  1  2  ptmx
0  1  2  3  ptmx

It disappears as soon as the process that owns the fd claim on /dev/ptmx quits, but it is simple enough to retain it:

$ PS1='<$(ls -1 "$@" /dev/pts/*|uniq -u)> $ ' \
  exec 3<>/dev/ptmx "$0" -si -- /dev/pts/*
</dev/pts/3> $ ls /dev/pts; exec 3>&-
0  1  2  3  ptmx
<> $ ls /dev/pts; exec 3<>/dev/ptmx
0  1  2  ptmx
</dev/pts/3> $ exit

So it looks like the simple open() on /dev/ptmx is enough to get a pseudo-terminal. I guess that would make the shell – (or the process for which it does the redirect) – the owner of the master side of the pty.

But how do I assign the slave side – or can I assign the slave side? And if I can – what can I do with it? Can I affect its read/write/flush timings w/ stty? Will a slave side process link through to my new pty when referencing /dev/tty?

Best Answer

So, as @muru points out in the comments, there doesn't seem to be a simple way to interface the pty created for you with just the shell. I managed all of it but the unlockpt() part. According to something I read here it may be there are some compile-time options in the kernel for disabling newly created pty locking, but I didn't want to do that. So, I did something else.

I didn't need grantpt() actually. According to the description found here all it does is change the UID/GID for /dev/pts/[num] device file. But according to man mount there are easier ways to handle that. Here are some devpts mount options:

  • uid=value and gid=value
    • This sets the owner or the group of newly created PTYs to the specified values. When nothing is specified, they will be set to the UID and GID of the creating process. For example, if there is a tty group with GID 5, then gid=5 will cause newly created PTYs to belong to the tty group.

That was already the case on my system by default. But after reading that I realized I might want to make a change after all. The very next section reads:

  • ptmxmode=value
    • Set the mode for the new ptmx device node in the devpts filesystem.
    • With the support for multiple instances of devpts (see newinstance option above), each instance has a private ptmx node in the root of the devpts filesystem (typically /dev/pts/ptmx).
    • For compatibility with older versions of the kernel, the default mode of the new ptmx node is 0000. ptmxmode=value specifies a more useful mode for the ptmx node and is highly recommended when the newinstance option is specified.

While it would have worked without doing so, I liked the idea and set that to 0640 as recommended in the kernel documentation. The kernel doc link, by the way, elaborates on the newinstance mount option - which is pretty cool and basically enables you get a separately name-spaced group of ptys per /dev/ptmx mount.

Anyway, so the something else amounted mostly to:

mount -o remount,newinstance,gid=5,ptmxmode=0640 /dev/pts
mount --bind /dev/pts/ptmx /dev/ptmx

...as the kernel docs recommend - see the link about why. I also made the effect of the above commands permanent by adding a couple of lines to my /etc/fstab.

And...

<<\C cc -xc - -o pts
#include <stdio.h>
int main(int argc, char *argv[]) {
        if(unlockpt(0)) return 2;
        char *ptsname(int fd);
        printf("%s\n",ptsname(0));
        return argc - 1;
}
C

Which just compiles a tiny little C program that attempts to call unlockpt() on its stdin and if successful prints the name of the newly created and unlocked pty to stdout or else silently returns 2.

Once that was done I could create my own screened processes like:

exec 3<>/dev/ptmx

...to get the master-side fd in the current shell then...

(setsid -c "$0" -i 2>&1|tee log) <>"$(./pts <&3)" 3>&- >&0 &

That gets an interactive shell running on the other end of the pseudo-terminal in the background. It will interpret anything printed to >&3 as user input.

mikeserv@localhost$ echo echo hey >&3
mikeserv@localhost$ cat log
$ hey
$
mikeserv@localhost$ echo echo hey >&3
mikeserv@localhost$ cat log
$ hey
$ hey
$ 

Which basically nets me a backgrounded, logged, interactive interpreter (or anything else I might care to run on these) ala screen without so much overhead and on any file descriptor I choose.

The master-side fd owned by my current shell is the only means of serving the slave-side input and is only writable by my current shell process (and its children). I can communicate with the other end by writing to >&3 and I can either read from same or from a log file as I wish.

And stty does work on the terminal after all:

mikeserv@localhost$ echo stty -a ">$(tty)" >&3
speed 38400 baud; rows 0; columns 0; line = 0;                                      
intr = ^C; quit = ^\; erase = ^?; kill = ^U; eof = ^D; eol = <undef>; eol2 = <undef>;
swtch = <undef>; start = ^Q; stop = ^S; susp = ^Z; rprnt = ^R; werase = ^W;
lnext = ^V; flush = ^O; min = 1; time = 0;
-parenb -parodd -cmspar cs8 -hupcl -cstopb cread -clocal -crtscts
-ignbrk -brkint -ignpar -parmrk -inpck -istrip -inlcr -igncr icrnl ixon -ixoff -iuclc
-ixany -imaxbel -iutf8
opost -olcuc -ocrnl onlcr -onocr -onlret -ofill -ofdel nl0 cr0 tab0 bs0 vt0 ff0
isig icanon iexten echo echoe echok -echonl -noflsh -xcase -tostop -echoprt echoctl
echoke