Another use for strace (isatty)
(This is a followup to an earlier article describing an interesting use of strace .)
A while back I was writing a talk about Unix internals and I wanted to
discuss how the ls command does a different display when talking to
a terminal than otherwise:
ls to a terminal
ls not to a terminal
How does ls know when it is talking to a terminal? I expect that is
uses the standard POSIX function isatty . But how does isatty find
out?
I had written down my guess. Had I been programming in C, without
isatty , I would have written something like this:
@statinfo = stat STDOUT;
if ( $statinfo[2] & 0060000 == 0020000
&& ($statinfo[6] & 0xff) == 5) { say "Terminal" }
else { say "Not a terminal" }
(This is Perl, written as if it were C.) It uses fstat (exposed in
Perl as stat ) to get the mode bits ($statinfo[2] ) of the inode
attached to STDOUT , and then it masks out the bits the determine if
the inode is a character device file. If so, $statinfo[6] is the
major and minor device numbers; if the major number (low byte) is
equal to the magic number 5, the device is a terminal device. On my
current computers the magic number is actually 136. Obviously this
magic number is nonportable. You may hear people claim that those bit
operations are also nonportable. I believe that claim is mistaken.
The analogous code using isatty is:
use POSIX 'isatty';
if (isatty(STDOUT)) { say "Terminal" }
else { say "Not a terminal" }
Is isatty doing what I wrote above? Or something else?
Let's use strace to find out. Here's our test script:
% perl -MPOSIX=isatty -le 'print STDERR isatty(STDOUT) ? "terminal" : "nonterminal"'
terminal
% perl -MPOSIX=isatty -le 'print STDERR isatty(STDOUT) ? "terminal" : "nonterminal"' > /dev/null
nonterminal
Now we use strace :
% strace -o /tmp/isatty perl -MPOSIX=isatty -le 'print STDERR isatty(STDOUT) ? "terminal" : "nonterminal"' > /dev/null
nonterminal
% less /tmp/isatty
We expect to see a long startup as Perl gets loaded and initialized,
then whatever isatty is doing, the write of nonterminal , and then
a short teardown, so we start searching at the end and quickly
discover, a couple of screens up:
ioctl(1, SNDCTL_TMR_TIMEBASE or TCGETS, 0x7ffea6840a58) = -1 ENOTTY (Inappropriate ioctl for device)
write(2, "nonterminal", 11) = 11
write(2, "\n", 1) = 1
My guess about fstat was totally wrong! The actual method is that
isatty makes an ioctl call; this is a device-driver-specific
command. The TCGETS parameter says what command is, in this case
“get the terminal configuration”. If you do this on a non-device, or
a non-terminal device, the call fails with the error ENOTTY . When
the ioctl call fails, you know you don't have a terminal. If you do
have a terminal, the TCGETS command has no effects, because it is a
passive read of the terminal state. Here's the successful call:
ioctl(1, SNDCTL_TMR_TIMEBASE or TCGETS, {B38400 opost isig icanon echo ...}) = 0
write(2, "terminal", 8) = 8
write(2, "\n", 1) = 1
The B38400 opost… stuff is the terminal configuration; 38400 is the baud rate.
(In the past the explanatory text for ENOTTY was the mystifying “Not
a typewriter”, even more mystifying because it tended to pop up when
you didn't expect it. Apparently Linux has revised the message to the
possibly less mystifying “Inappropriate ioctl for device”.)
(SNDCTL_TMR_TIMEBASE is mentioned because apparently someone decided
to give their SNDCTL_TMR_TIMEBASE operation, whatever that is, the
same numeric code as TCGETS , and strace isn't sure which one is
being requested. It's possible that if we figured out which device was
expecting SNDCTL_TMR_TIMEBASE , and redirected standard output to
that device, that isatty would erroneously claim that it was a
terminal.)
[ Addendum 20150415: Paul Bolle has found that the
SNDCTL_TMR_TIMEBASE pertains to the old and possibly deprecated OSS
(Open Sound System)
It is conceivable that isatty would yield the wrong answer when
pointed at the OSS /dev/dsp or /dev/audio device or similar. If
anyone is running OSS and willing to give it a try, please contact me at mjd@plover.com . ]
[ Addendum 20191201: Thanks to Hacker News user
jwilk for pointing
out that strace is
now able to distinguish TCGETS from SNDCTL_TMR_TIMEBASE . ]
[Other articles in category /Unix]
permanent link
|