24

If I do a:

echo foo > /dev/pts/12

Some process will read that foo\n from its file descriptor to the master side.

Is there a way to find out what that(those) process(es) is(are)?

Or in other words, how could I find out which xterm/sshd/script/screen/tmux/expect/socat... is at the other end of /dev/pts/12?

lsof /dev/ptmx will tell me the processes that have file descriptors on the master side of any pty. A process itself can use ptsname() (TIOCGPTN ioctl) to find out the slave device based on its own fd to the master side, so I could use:

gdb --batch --pid "$the_pid" -ex "print ptsname($the_fd)"

for each of the pid/fd returned by lsof to build up that mapping, but is there a more direct, reliable and less intrusive way to get that information?

  • Is this what you want? sudo find /proc/*/fd/0 -ls | grep '/dev/pts/4', would provide the list of PIDs (/proc/PID) as output. – slm Jun 12 '14 at 0:51
  • @slm, no, in other words, I want to find out which xterm/sshd/script/screen/tmux/expect/socat... is at the other end of /dev/pts/4. Usually, that will be a common ancestor of those processes that have /dev/pts/4 open, but not necessarily. – Stéphane Chazelas Jun 12 '14 at 7:32
  • 1
    It's even worse with sockets — you need a kernel debugger! – Gilles Jun 12 '14 at 17:04
  • /proc/locks has a few leads. – mikeserv Jun 15 '14 at 13:10
  • 1
    @Falsenames - I understood the question to mean - perhaps incorrectly - not what process is passed the data read - such as the first shell invoked in terminal - but what process actually reads it from the master side. For instance, if I launch a shell in screen, it is screen that allocates and actively manages the pty slave for the life of the device, but - as, I think - the shell is made the process-leader for that tty and so, as your output shows, you get bash or whatever from ps not screen. I traced a few xterms back to the xterm pid based on /proc/locks but it was loose. – mikeserv Jun 29 '14 at 4:18
3

At first I tried tracing a few xterms 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 mounts, 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.

2

If you are just looking for who owns the connection and where they are connected from, the who command will work well.

$ who
falsenames   tty8         Jun 13 16:54 (:0)
falsenames   pts/0        Jun 16 11:18 (:0)
falsenames   pts/1        Jun 16 12:59 (:0)
falsenames   pts/2        Jun 16 13:46 (:0)
falsenames   pts/3        Jun 16 14:10 (:0)
falsenames   pts/4        Jun 16 16:41 (:0)

If you also want to know what is listening on that connection, w will show that at the end.

$ w
 16:44:09 up 2 days, 23:51,  6 users,  load average: 0.26, 0.98, 1.25
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
falsenames   tty8     :0               Fri16    2days 53:36   0.59s x-session-manager
falsenames   pts/0    :0               11:18    5:25m  1:10   1:10  synergys -a 10.23.8.245 -c .synergy.conf -f -d DEBUG
falsenames   pts/1    :0               12:59    3:44m  0.05s  0.05s bash
falsenames   pts/2    :0               13:46    2:52m  0.11s  0.11s bash
falsenames   pts/3    :0               14:10    2:17   0.07s  0.07s bash
falsenames   pts/4    :0               16:41    1.00s  0.04s  0.00s w

And to get the pids, limit a ps to the tty session you are looking at. Completely unobtrusive to boot.

$ ps -t pts/0 --forest 
  PID TTY          TIME CMD
23808 pts/0    00:00:00 bash
23902 pts/0    00:03:27  \_ synergys

Note, this can lead to red herrings, depending on timing. But it's a good place to start.

$ tty
/dev/pts/4
$ ps -t pts/4 --forest
  PID TTY          TIME CMD
27479 pts/4    00:00:00 bash
 3232 pts/4    00:00:00  \_ ps
27634 pts/4    00:00:00 dbus-launch
  • Thanks, but that's not what I'm looking for. Above for instance, I'd like to find the pid of the terminal application (Xterm/gnome-terminal...) that corresponds to /dev/pts/4, where you ran that w command. – Stéphane Chazelas Jun 17 '14 at 6:48
  • Sorry, completely missed the pid part when I scanned through the first time. I thought you just wanted to know the end process name. – Falsenames Jun 17 '14 at 19:27
2

I had the same problem with qemu, and I finally found a very bad solution (but still a solution): parsing the process memory.

This is working here because I know that qemu is storing the remote pts in a string with a specific format and allocated on the heap. May be it can work in other situations too with a few changes and by reusing the pid from the fuser output (check other answer).

The code is adapted from here.

#! /usr/bin/env python

import sys
pid = sys.argv[1]

import re
maps_file = open("/proc/" + pid + "/maps", 'r')
mem_file = open("/proc/" + pid + "/mem", 'r', 0)
for line in maps_file.readlines():
    # You may want to remove the 'heap' part to search all RAM
    m = re.match(r'([0-9A-Fa-f]+)-([0-9A-Fa-f]+) ([-r]).*\[heap\]', line)
    if m and m.group(3) == 'r':
        start = int(m.group(1), 16)
        end = int(m.group(2), 16)
        mem_file.seek(start)
        chunk = mem_file.read(end - start)
        # You may want to adapt this one to reduce false matches
        idx = chunk.find("/dev/pts/")
        if idx != -1:
            end = chunk.find("\0", idx)
            print chunk[idx:end]
maps_file.close()
mem_file.close()

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.