20110202

How to run something other than login on a tty

Sometimes you don't want to run login on the console or other terminal. The reasons for this vary. Some appliances want to boot to a shell prompt on the console for debugging purposes (this console typically isn't exposed, so no security problems there). Sometimes you want to use init's restart feature to keep critical daemons alive. I've developed several control and measurement applications that used this feature. Sometimes your want to offer a menu driven interface to your users instead of a cli-based one.

Recently, I had to add support for a menu console to FreeNAS. I couldn't find any immediately available documentation on how-to do this, I had to dive into some little-configured recesses of FreeBSD to make this happen. Thankfully, no code changes to FreeBSD were required to make this work. I thought I'd do a quick how-to here to cover the basics.

We'll start with /etc/ttys. This is the file that has entries like the following:

ttyv0 "/usr/libexec/getty Pc" cons25 on secure

which tells init to run getty with the parameter Pc on /dev/ttyv0 with TERM set to cons25. Getty is a program, for those that don't know, that sets up the tty device for interactive use so that normal interaction works as you'd expect. While one can run programs without getty, especially on a 'device' that doesn't exist, I'll ignore that path for this post. This is about creating an interactive program that runs on a tty device.

The 'Pc' here is the key to understanding what getty is doing. Pc refers to the gettytab entry 'Pc' which looks something like:
P|Pc|Pc console:\
:ht:np:sp#115200:

which, according to the gettytab(5) man page means "Use 115200 baud, no parity, hard tabs". This is great if you want to run login to get an account and password, but what if you want to run a program other than login instead? Maybe one that doesn't know about tty sessions, stdin/stdout redirection etc?

The answer turns out to be fairly straight forward. You just tell tty to use a different entry. This scales well for a small number of programs, but not so well if you have dozens since you can't pass parameters to the final program. For my case, I just needed a menu for FreeNAS. I added the following entry:
#
# FreeNAS menu system entry
#
FreeNAS|freenas|FreeNAS Menu:\
:ht:np:sp#15200:lo=/etc/netcli.sh:al=root:

to /etc/gettytab. This tells getty to run /etc/netcli.sh as root. Since netcli.sh was mucking with the network it had to run as root, but there's no reason it couldn't run as a different user for safety. Once I had this entry in gettytab, I changed the above /etc/ttys line to look like:
ttyv0 "/usr/libexec/getty freenas" cons25 on secure

and sent init a hup with a "kill -1 1" command (the -1 is very important, otherwise you reboot your system). Once init reparsed /etc/ttys, netcli.sh started running.

Normally, that would be the end of it. However, in this case netcli really is a pyhton script. Why did I have to wrap it in a shell script that looks like:
#!/bin/sh
# Helper script to set the path for netcli menu
export PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin
exec /etc/netcli

When you run out of getty, you have a very restrictive shell. The python script called a bunch of other programs, so it needed to have a good path. Also, since the shell didn't need to stick around, I tossed the typical 'exec' at the end of it to save a little bit of memory on this embedded system.

And there you have it. All the steps to create a program that runs automatically at boot. If you have any other cool tricks, please feel free to comment here...