ctapobep / blog

My personal blog on IT topics in the form of GitHub issues.
6 stars 0 forks source link

TTY, file descriptors, line endings #2

Open ctapobep opened 3 years ago

ctapobep commented 3 years ago

When writing scripts for Linux from time to time I'm running into problems with TTY like this one:

sudo: sorry, you must have a tty to run sudo

I've had couple of attempts already to figure out what TTY exactly is, but I never grasped the concept fully. Hopefully this time was the last one..

History of TTY

In old days TTY (tele-typewriter) was a typewriter connected via a wire to another typewriter. So when typing something on your end you could've send the same text to someone far away. And on the receiving end the typewriter was typing the same thing on the paper too. Here's a great video explaining how it worked.

With the evolution of computers remote access became a necessity. But those folks didn't have a laptop to SSH to the "server". And so they hooked up tele-typewriters, which made it possible to type something on one end - and that would send an electrical signal over the wire to the computer. It's hard to imagine today, but the first display was.. a roll of paper! You typed something on a TTY and you saw the text you just typed. And then a response came from the computer - and it also was getting typed on your "screen". See this demo (although it has Linux machine on the other end). Additionally a punched tape was used for data storage - you could feed it into a TTY and it would type whatever was encoded on the tape.

At some point TTYs were replaced with computer terminals (e.g. VT05). These were fat clients - you could've edited the text before transferring it all to the computer. And while those were electronic - they still emulated TTYs when communicating with the mainframe.

Interestingly, because typewriters were used for remote control in the early days, people had to use Line Feed and Carriage Return buttons (typewriter had 2 separate buttons to bring the carriage to the next line and move it to the 1st column). To this day (2021) Windows uses them 2 symbols for line breaks: \r\n. While Linux & MacOS moved on to just \n.

TTY in Linux

While actual TTYs are not used anymore - *nix systems keep using it as an abstraction for serial communication. When you work in a terminal - it doesn't just listen to keyboard events and sends them directly to bash or other programs. Instead it uses an intermediary - a pseudo-tty (aka pty or pts for pseudo-teletypes). The program (shell or anything else that you started in the shell) listens to a pty and receives keyboard strokes from it:

image

And vice versa: when our program writes something into console, it actually writes into pty instead of directly sending data to the terminal:

image

The reason to have a pty as an intermediate instead of transferring keyboard events directly to the program is that it makes it easier to implement the same mechanism over network:

image

So there's no need to come up with a new mechanism to transfer data as an input to the command line program. Or to transfer data back from the program (output).

File Descriptors and Standard Input/Output

When opening files we use file names, but once opened OS assigns an ID to the opened file - a file descriptor (fd). In C you'd pass this fd to function read() and write() so that Kernel would read from/write to that file. Besides explicitly opened files, any program must have 3 file descriptors opened when it starts:

In Java when we work with System.in, System.out, System.err JVM actually works with these 0, 1, 2 file descriptors.

In Linux a File Descriptor doesn't necessarily point to a file, there could be other options. E.g. the output of the program can go to:

Ordinary file

echo 'write this to regular file' > /home/user/out.txt

fd:1 will reference /home/user/out.txt

Pipe

echo 'pass this to the next command' | grep pass`

fd:1 of echo will reference pipe:[some number here]. fd:0 of grep will also reference the same pipe:[some number here].

TTY:

echo 'shown this in the terminal'

All the fd's will reference /dev/pts/[some number]. And our terminal can read from there (and draw the text on the screen), and write there too (user input) - which will be read by the program (well, echo is a bad example as it doesn't read anything from fd:0).

TTY and scripts

So if you're writing a script or a program that reads user input (fd:0) and for some reason you want to ensure it's a real user input in the terminal (as opposed to a file on the disk), you can check it with a command tty -s which returns 0 if fd:0 is a tty:

if $(tty -s); then
  echo Enter password:
  read password
  echo "You entered: $password"
else
  echo "You have to enter password, so tty must be enabled"
  exit 1
fi

As a result:

$ sh script.sh
Enter password:

While using an ordinary file as an input produces:

$ sh script.sh < password.txt
You have to enter password, so tty must be enabled

Finding logged in users

You can also run a command w (probably stands for who, what, when):

$ w
 14:26:50 up 10 days, 21:40,  2 users,  load average: 0.00, 0.00, 0.00
USER     TTY      FROM             LOGIN@   IDLE   JCPU   PCPU WHAT
stas     pts/0    46.32.87.80      14:15    2.00s  0.07s  0.00s w
stas     pts/1    46.32.87.80      14:26    2.00s  0.02s  0.02s -bash

As you can see I have 2 sessions open, one uses /dev/pts/0 and the other /dev/pts/1 for TTY. I can even write something to /dev/pts/1 and this will show up in my 2nd session:

echo 'Hello my second session' > /dev/pts/1

You can even chat with someone else logged in on the server, though you'd need sudo for this :)

PS: the real picture with pty's is a little more complicated. When terminal reads/writes - it uses a pty-master, but when a running program reads/writes it uses a pty-slave. These are 2 different "files", but when writing into one - the input will appear in the other.