At first I tried tracing a few xterm
s back to the xterm
pid based on info I found in /proc/locks
but it was loose. I mean, it worked, I think, but it was at best circumstancial - I don't fully understand all of the information that file provides and was only matching what seemed to correspond between its content and known terminal processes.
Then I tried watching lsof/strace
on an active write/talk
process between ptys. I had never actually used either program before, but they seem to rely on utmp
. If my targeted pty did not have a utmp
entry for whatever reason they both refused to admit that it existed. Maybe there's a way around that, but i was confused enough to abandon it.
I tried some udevadm
discovery with 136 and 128 major number device nodes as advertised for pts
and ptm
respectively in /proc/tty/drivers
, but I also lack any very useful experience with that tool and once again turned up nothing substantial. Interestingly, though, I noticed the :min
range for both device types was listed at a staggering 0-1048575
.
It wasn't until I revisited this this kernel doc that I started thinking about the problem in terms of mount
s, though. I had read that several times before but when continued research in that line led me to this this 2012 /dev/pts
patchset I had an idea:
sudo fuser -v /dev/ptmx
I thought what do I usually use to associate processes with a mount
? And sure enough:
USER PID ACCESS COMMAND
/dev/ptmx: root 410 F.... kmscon
mikeserv 710 F.... terminology
So with that information I can do, for instance from terminology
:
sudo sh -c '${cmd:=grep rchar /proc/410/io} && printf 1 >/dev/pts/0 && $cmd'
###OUTPUT###
rchar: 667991010
rchar: 667991011
As you can see, with a little explicit testing such a process could be made to pretty reliably output the master process of an arbitrary pty. Regarding the sockets, I'm fairly certain one could approach it from that direction as well using socat
as opposed to a debugger, but I've yet to straighten out how. Still, I suspect ss
might help if you're more familiar with it than I:
sudo sh -c 'ss -oep | grep "$(printf "pid=%s\n" $(fuser /dev/ptmx))"'
So I set it up with a little more explicit testing, actually:
sudo sh <<\CMD
chkio() {
read io io <$1
dd bs=1 count=$$ </dev/zero >$2 2>/dev/null
return $((($(read io io <$1; echo $io)-io)!=$$))
}
for pts in /dev/pts/[0-9]* ; do
for ptm in $(fuser /dev/ptmx 2>/dev/null)
do chkio /proc/$ptm/io $pts && break
done && set -- "$@" "$ptm owns $pts"
done
printf %s\\n "$@"
CMD
It prints $$
num \0
null bytes to each pty and checks each master process's io against a previous check. If the difference is $$
then it associates the pid with the pty. This mostly works. I mean, for me, it returns:
410 owns /dev/pts/0
410 owns /dev/pts/1
710 owns /dev/pts/2
Which is correct , but, obviously, it's a little racy. I mean, if one of those others was reading in a bunch of data at the time it would probably miss. I'm trying to figure out how to change the stty
modes on another pty in order to send the stop bit first or something like that so I can fix that.
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
Best Answer
On Linux, a
read()
on the master side of a pseudo-tty will return-1
and setERRNO
toEIO
when all the handles to its slave side have been closed, but will either block or returnEAGAIN
before the slave has been first opened.The same thing will happen when trying to read from a slave with no master. For the master side, the condition is transient; re-opening the slave will cause a
read()
on the master side to work again.On *BSD and Solaris the behavior is similar, with the difference that the
read()
will return0
instead of-1
+EIO
. Also, on OpenBSD aread()
will also return0
before the slave is first opened.I don't know if there's any standard spec or rationale for this, but it allows to (crudely) detect when the other side was closed, and simplifies the logic of programs like
script
which are just creating a pty and running another program inside it.The solution in a program which manages the master part of a pty to which other unrelated programs can connect is to also open and keep open a handle to its slave side.
See related answer: read(2) blocking behaviour changes when pts is closed resulting in read() returning error: -1 (EIO)
When a process exits, all its file descriptors are automatically closed.