Finding the terminal your script is running in - Programming On Unix

Users browsing this thread: 1 Guest(s)
venam
Administrators
I recently dived a bit more into this.
This SO thread has a lot of good stuff, along with man(7) pty, and man(4) ptmx, pts, and this lwn article.

While on classic BSD systems, making the link between master and slave pair is much easier as they map to ptyXY and ttyXY with the same XY numbers, on Linux it's not as straight forward.

Initially, I couldn't find any tool to display clearly the link between the process linked to the pseudoterminal master end (/dev/ptmx) and the pseudoterminal slave (/dev/pts/<num>).

Yet, after digging more I could find the following methods:
You can type tty(1) in the shell to get the /dev/pts number. Then interrogating /dev/ptmx give us more info about the parent process. lsof(8) has the +E allows to display the pseudoterminal info.
For example, if the tty is /dev/pts/13, you can do:
Code:
lsof +E /dev/ptmx | awk '/pts\/13/ && /dev\/ptmx/'
Which will output:
Code:
urxvtd       783  vnm   11u   CHR    5,2      0t0   99 /dev/ptmx ->/dev/pts/13 794279,zsh,0u 794279,zsh,1u 794279,zsh,2u 794279,zsh,10u 795200,lsof,0u 795200,lsof,2u 795201,awk,2u 795202,xsel,1u 795202,xsel,2u
This indicates the shell is running on urxvtd as a terminal.

Another way to query the info is to perform ptsname(3) on /dev/ptmx, passing it the file descriptors of the linked terminals, to get the corresponding of pseudoterminal slave ends attached to it.

Manually doing this goes like this, first listing the current master ends and the file descriptors attached.
Code:
lsof -F pcf /dev/ptmx
Example output (the fields are prepended with single letter, weird lsof format):
Code:
p783
curxvtd
f9
f10
f11

Yet another listing method for the master end is to use fuser(1):
Code:
$ fuser -v /dev/ptmx

                     USER        PID ACCESS COMMAND
/dev/ptmx:           vnm         754 F.... urxvt
                     vnm         783 F.... urxvtd
                     vnm         986 F.... tmux: server
                     vnm       803385 F.... urxvtd
                     vnm       805201 F.... xsel

After this, there are multiple ways to find the slave pseudoterminal end.
The easiest way, is to rely on a recent patch (2017) in the Linux kernel has added an entry to /proc/[pid]/fdinfo/<FD> called tty-index. Though this field is not documented in man 5 proc.
The value of tty-index is the associated pts number.

Thus we can now do:
Code:
$ < /proc/783/fdinfo/9


pos:    0
flags:    0104002
mnt_id:    24
tty-index:    1
Then see who has a file descriptor opened on /dev/pts/1.
Code:
lsof /dev/pts/1
COMMAND    PID USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
zsh     796001  vnm    0u   CHR  136,1      0t0    4 /dev/pts/1
zsh     796001  vnm    1u   CHR  136,1      0t0    4 /dev/pts/1
zsh     796001  vnm    2u   CHR  136,1      0t0    4 /dev/pts/1
zsh     796001  vnm   10u   CHR  136,1      0t0    4 /dev/pts/1
vim     796445  vnm    0u   CHR  136,1      0t0    4 /dev/pts/1
vim     796445  vnm    1u   CHR  136,1      0t0    4 /dev/pts/1
vim     796445  vnm    2u   CHR  136,1      0t0    4 /dev/pts/1
So two processes, zsh (796001) and vim (796445) have three file descriptors (stdin, stdout, stderr) opened on /dev/pts/1.

Another method, if tty-index isn't present, is to rely on the ptsname(3) to get the name of the slave pseudoterminal. That's a bit clunky as you'd need to rely on gdb to access the file descriptor of the running process, which might need higher privileges.
There's two ways I've found, the simple one is to do:
Code:
#  gdb -batch -p 986 -ex 'p (char *)ptsname(8)' 2>/dev/null | grep 'dev\/pts'
$1 = 0x7ff95c3ab010 <buffer> "/dev/pts/3"
(beware that it might interfere with the running terminal, putting it in debug mode)

I've seen other answers online that directly do the ioctl that ptsname does in the background (source ioctl request TIOCGPTN = 0x80045430, which I can't find in man 2 ioctl_tty).
Something like:
Code:
# gdb -batch -p 986 -ex 'p (int)ioctl(8, 0x80045430, &errno)?-1: (int)errno' 2>/dev/null | grep '\$1'

$1 = 3
Note here that errno is used as the storage for the pts number and not for actual error.

Now that we got both ends, master and slave then we can print the processes on both ends.

For example:

Code:
# Master
$ ps -p 986 --forest
    PID TTY          TIME CMD
    986 ?        00:04:28 tmux: server

# Slave
$ ps -t pts/3 --forest
    PID TTY          TIME CMD
    987 pts/3    00:01:36 irssi

This can be automated in a script, someone has posted one on SO:
Code:
my (%pty, %ctty);
for(</proc/*[0-9]*/{fd/*,stat}>){
    if(my ($pid, $fd) = m{/proc/(\d+)/fd/(\d+)}){
        next unless -c $_;
        my $rdev = (stat)[6]; my $maj = $rdev >> 8 & 0xfff;
        if($rdev == 0x502){ # /dev/ptmx or /dev/pts/ptmx
            $pty{ptsname($pid, $fd, readlink $_)}{m}{$pid}{$fd} = 1;
        }elsif($maj >= 136 && $maj <= 143){ # /dev/pts/N
            $pty{readlink $_}{s}{$pid}{$fd} = 1;
        }
    }else{
        my @s = readfile($_) =~ /(?<=\().*(?=\))|[^\s()]+/gs;
        $ctty{$s[6]}{$s[0]} =       # ctty{tty}{pid} =
        ($s[4] == $s[7] ? '+' : '-').   # pgrp == tpgid
        ($s[0] == $s[5] ? '*' : '');    # pid == sid
    }
}
for(sort {length($a)<=>length($b) or $a cmp $b} keys %pty){
    print "$_\n";
    pproc(4, $pty{$_}{m}); pproc(8, $pty{$_}{s}, $ctty{(stat)[6]});
}

sub readfile { local $/; my $h; open $h, '<', shift and <$h> }
sub cmdline {
    join ' ', map { s/'/'\\''/g, $_ = "'$_'" if m{^$|[^\w./+=-]}; $_ }
    readfile("/proc/$_[0]/cmdline") =~ /([^\0]*)\0/g;
}
sub pproc {
    my ($px, $h, $sinfo) = @_;
    exists $$h{$_} or $$h{$_} = {''} for keys %$sinfo;
    return printf "%*s???\n", $px, "" unless $h;
    for my $pid (sort {$a<=>$b} keys %$h){
        printf "%*s%-5d %s%-3s   %s\n", $px, "", $pid, $$sinfo{$pid},
        join(',', sort {$a<=>$b} keys %{$$h{$pid}}),
        cmdline $pid;
    }
}
sub ptsname {
    my ($pid, $fd, $ptmx) = @_;
    return '???' unless defined(my $ptn = getptn($pid, $fd));
    $ptmx =~ m{(.*)(?:/pts)?/ptmx$} ? "$1/pts/$ptn" : "$ptmx ..?? pts/$ptn"
}
sub getptn {
    my ($pid, $fd) = @_;
    return $1 if
    readfile("/proc/$pid/fdinfo/$fd") =~ /^tty-index:\s*(\d+)$/m;
    return gdb_ioctl($pid, $fd, 0x80045430);    # TIOCGPTN
}
sub gdb_ioctl {
    my ($pid, $fd, $ioctl) = @_;
    my $cmd = qq{p (int)ioctl($fd, $ioctl, &errno) ? -1 : errno};
    qx{exec 3>&1; gdb -batch -p $pid -ex '$cmd' 2>&1 >&3 |
    grep -v '/sysdeps/.*No such file or directory' >&2}
    =~ /^\$1 *= *(\d+)$/m ? $1 : undef;
}
However, it's not very readable, so I wrote a much simpler script.

Code:
use strict;
use warnings;

print "MASTER\tSLAVES\n";
my $pid = -1;
for my $master_end (qx#lsof -F pcf /dev/ptmx#) {
    chomp $master_end;
    if ($master_end =~ /p.*/) {
        print "\n" if $pid>0;
        $pid = substr($master_end, 1);
        print "$pid,";
    } elsif ($master_end =~ /c.*/) {
        print substr($master_end, 1)."\t";
    }
    elsif ($master_end =~ /f.*/) {
        # a file descriptor for current pid
        my $fd = substr($master_end, 1);
        readfile("/proc/$pid/fdinfo/$fd") =~ /^tty-index:\s*(\d+)$/m;
        my $slave_processes = qx#ps -o "%p,%c" -t pts/$1#;
        $slave_processes =~ s/^(?:.*\n)//;
        $slave_processes =~ s/^\s*//;
        $slave_processes =~ s/\n/\t/g;
        print "$slave_processes";
    }
}
sub readfile { local $/; my $h; open $h, '<', shift and <$h> }
print "\n";

Or you can simply rely on lsof:
Code:
lsof +E /dev/ptmx | awk '/dev\/ptmx/'

Wow, it turned out to be a much deeper dive than I thought! I hope that clarifies things for at least someone. As for the initial question, you can then simply execute one of these and grep for the PID of the process to find the terminal it is running on.
Yet, it still doesn't answer "what is actually considered a terminal".


Messages In This Thread
RE: Finding the terminal your script is running in - by venam - 05-06-2021, 05:26 AM