Contents


Introduction

Depinit is an alternative init program that can handle parallel execution, dependencies, true roll-back, pipelines, improved signaling and unmounting filesystems on shutdown. It incorporates ideas from sysvinit, simpleinit, daemontools and make. At present, it is a bit experimental, and requires good knowledge of the initialisation process to set up.

I am keen to hear from people who try it. Here is why you should.

Reasons for depinit

Implications of using depinit

Future

Changes since 0.1.3

License

Depinit is copyright 2002 by Richard Lightman, and released under Version 2 of the GNU General Public Licence. There is NO WARRANTY.

Bugs

The big bad bugs have been fixed, but as yet there has not been sufficient testing to call this anything more than an beta release.

A bit more work needs to go into logging when syslog is not available or wanted. Anything with a file descriptor open to a terminal can be killed by anyone at the terminal. All they have to do is press the SAK key. The (untested) solutions are to have a syslog report on the console, or to use a separate daemon to log to the console (it does not matter if anyone kills that daemon because depinit can restart it without loosing data).

Downloads

Take a look in the download section of the man-pages for depinit or depctl for the download location of the source code.

Building

This program has only been tested on linux-2.4.*. This could cause problems with the more unusual system calls such as for shutting down, detaching loop devices and capturing the keyboard signal.

The build process requires make-3.80 (with some bugs fixed, a patch by Mr P. Smith is included in the sources), gcc version 3.* (3.2 works, 2.* probably will not), help2man, gettext, and I suspect a reasonably modern awk (gawk-3.1.1 is fine).

There are only two settings that are not part of the generic install process described in the sources. DEFAULT_CONFIG defaults to $(SYSCONFDIR)/depinit and is the default directory for the configuration directories. DEFAULT_ERROR_FILE defaults to /dev/console, and is the default file to use if depinit is process 1 and is told to output to stderr.

Man-pages

Depinit uses two programs: depinit often runs as process 1, and does all the real work. depctl is used to communicate with depinit, much like telinit communicates with init. The depinit package also contains some helper programs: utmp for maintainting the user accounting databases and spawnshell for starting new shells with the KeyboardSignal key.

Setting it up

To start with, I recommend using a kernel with the magic SysReq key enabled, so you can sync and unmount the filesystems if something goes wrong (see /usr/src/linux/Documentation/sysrq.txt). If you are going to recompile the kernel, now is a good time to fix the built in keymap. You will need one with KeyboardSignal defined. Also, add an entry to your bootloader menu that has the kernel option 'init=/sbin/depinit'. That way, you can still use the old init if you have problems with depinit. When you are confident that you have depinit working, you can make /sbin/init a symlink to depinit. If you did not compile bash with a sane default path, you will have to set PATH correctly in all the depinit scripts. The environment depinit provides to the scripts is exactly the one depinit got from the kernel.

/etc/depinit

Next you will need some init scripts. By default, these go in '/etc/depinit/'.



mkdir -p /etc/depinit/ 
cd       /etc/depinit/

/etc/depinit/exec

If process 1 crashes, your system becomes largely unusable. If depinit crashes, it will attempt to exec /etc/depinit/exec. This example script will let you log in and shut things down cleanly.



echo -e >exec '#! /bin/bash -e\n'
echo   >>exec 'exec </dev/vc/2 >/dev/vc/2 2>&1'
echo   >>exec 'chvt 2'
echo   >>exec 'exec spawnshell -d -l'
chmod 754 exec

/etc/depinit/default

When depinit starts, it tries to start 'default', so you will need something there. As there is no start, stop, source, filter, dest or daemon file in 'default', depinit knows that to start 'default', it only has to start its dependencies (to be added later). To stop 'default', no action is required.



mkdir default

/etc/depinit/fixing/

As this is your first go with depinit, you will get the scripts wrong, and probably will not be able to log in. To fix things you could do with a shell. Remember to remove it when things are working. Any processes left running from this shell will cause trouble when it is time to shut down.



mkdir fixing
echo >>default/depend 'fixing'
echo >fixing/start -e '#! /bin/bash -e\nexec spawnshell -l'

/etc/depinit/sig_quit/start

It would be handy to be able to log in, so let's fix that next. This is a slightly modified service - normally services are started because you asked for the service, or something depends on it. In this case, the script is started by a signal caused by pressing the KeyboardSignal key.

Normally, once the start script has run, depinit will not run it again because the service has already started. In this case we want depinit to run the script each time the KeyboardSignal key is pressed. The solution is to tell depinit to stop the service each time the start script is run. As there is no stop script, no action is required to stop the service.



mkdir sig_quit
cat >sig_quit/start <<'EOF'
#! /bin/bash -e

# This script is started when you press the KeyboardSignal key.
# It starts a shell on a new virtual terminal belonging to the same
# user as the owner of /var/run/console_user, if that file exists
# and is not empty. Otherwise the script prompts for a user name,
# and if that is a valid login user, encrypts a token with the user's
# public key. If the user can get gpg to decrypt the token correctly
# (types the password for the corresponding secret key) then this script
# puts the username into /var/run/console_user, changes the owner of
# that file to match the user name, and starts a shell as above.

# This type of login works better with depinit than the standard
# *getty/login because it makes depinit recognise all user processes
# as processes to kill before stopping services for shutdown.

# Users can prevent unauthenticated logins by typing:
# >/var/run/console_user

# @@@ version using /etc/shadow instead of gpg.

# Delete temporary files and directories used in gpg_login:
tidy () {
	if [ "$authdir" ]; then
		rm -r "$authdir"
	fi
	if [ "$userdir" ]; then
		rm -r "$userdir"
	fi
	trap '' EXIT
}

gpg_login () {
	trap tidy EXIT
	umask 077
	authdir="$(mktemp -d -t auth.XXXXXXXXXX)"
	userdir="$(mktemp -d -t auth.XXXXXXXXXX)"

	check="$(dd if=/dev/random of="$authdir/tok" bs=1024 count=1 2>&1)"
	[ "$check" = $'0+1 records in\n0+1 records out' ]

	gpgopts='--no-verbose -q --keyserver-options no-auto-key-retrieve'
	su "$USER" -c <"$authdir/tok" >"$userdir/enc" \
	   "gpg $gpgopts --batch --no-tty -e --default-recipient-self"
	chown -R "$USER" "$userdir"
	su "$USER" -c "gpg $gpgopts -d -o $userdir/dec $userdir/enc"

	cmp "$authdir/tok" "$userdir/dec"
	tidy
	exec </dev/null >/dev/null 2>&1
}

vc=3
devname=/dev/vc/$vc
if ! [ -e $devname ]; then
	devname=/dev/tty$vc
fi

if [ -s /var/run/console_user ]; then
	USER="$(find /var/run/ -name console_user -printf "%u\n")"
else
	exec <$devname >$devname 2>&1
	TERM=linux
	stty sane
	cd /
	chvt $vc

	echo -en "\n$(hostname) user name: "
	read USER
	id "$USER" &>/dev/null
	shell="$(pwcat | awk -v FS=: '$1=="'"$USER"'" {print $7}')"
	home="$( pwcat | awk -v FS=: '$1=="'"$USER"'" {print $6}')"
	[ "$shell" ]
	[ -x "$shell" ]
	[ /bin/bash == "$shell" ]
	[ "$home" ]
	[ -d "$home" ]
	[ -z "${home##/home/*}" ]

	gpg_login

	echo  "$USER" >/var/run/console_user
	chown "$USER"  /var/run/console_user
fi

umask 022
spawnshell -u "$USER" || true
exec depctl -k sig_quit
EOF
chmod 754 sig_quit/start

/etc/depinit/utmp

If the 'w' and 'who' commands are needed, /var/run/utmp must exist before any logins. As I have /var/run on tmpfs, this needs to be fixed.



mkdir utmp
echo >>sig_quit/depend 'utmp'
echo -e   >utmp/start '#! /bin/bash\n'
echo     >>utmp/start 'exec >>/var/run/utmp chmod 644 /var/run/utmp'
chmod 754 utmp/start

/etc/depinit/varrun

The above utmp script thinks that /var/run exists. This needs fixing promptly because I keep /var/run on a tmpfs. /var/run is used to hold pid's of daemons (superfluous with depinit). Some things get confused if there are stale files in /var/run, so it is an obvious place to use a tmpfs (which is always empty when mounted) and prevent such confusion.



mkdir varrun
echo >>utmp/depend 'varrun'
echo -e   >varrun/start '#! /bin/bash -e\n'
echo     >>varrun/start 'mounts="$(</proc/mounts)"'
echo     >>varrun/start '[ -z "${mounts##* /var/run *}" ] ||'
echo     >>varrun/start '       exec mount /var/run'
echo -e   >varrun/stop '#! /bin/bash -e\n'
echo     >>varrun/stop 'mounts="$(</proc/mounts)"'
echo     >>varrun/stop '[ -n  "${mounts##* /var/run *}" ] ||'
echo     >>varrun/stop '  exec umount -rnd /var/run'
echo -e   >varrun/depend 'proc\nswap\nvar'
chmod 754 varrun/{start,stop}

Mounting all the filesystems

Let's now use the above example as a template for (almost) all the mounts. Although depinit will unmount anything that the following scripts miss, it is best to have explicit stop scripts to avoid confusion, and so that roll-back works as people would expect.



mkdir devshm home tmp usb var varlock vartmp
sed <varrun/start >devshm/start  's:/var/run:/dev/shm:g' 
sed <varrun/start >home/start    's:/var/run:/home:g' 
sed <varrun/start >tmp/start     's:/var/run:/tmp:g' 
sed <varrun/start >usb/start     's:/var/run:/proc/bus/usb:g' 
sed <varrun/start >var/start     's:/var/run:/var:g' 
sed <varrun/start >varlock/start 's:/var/run:/var/lock:g' 
sed <varrun/start >vartmp/start  's:/var/run:/var/tmp:g' 
sed <varrun/stop >devshm/stop  's:/var/run:/dev/shm:g' 
sed <varrun/stop >home/stop    's:/var/run:/home:g' 
sed <varrun/stop >tmp/stop     's:/var/run:/tmp:g' 
sed <varrun/stop >usb/stop     's:/var/run:/proc/bus/usb:g' 
sed <varrun/stop >var/stop     's:/var/run:/var:g' 
sed <varrun/stop >varlock/stop 's:/var/run:/var/lock:g' 
sed <varrun/stop >vartmp/stop  's:/var/run:/var/tmp:g' 
echo -e >> devshm/depend 'devfsd\nproc\nswap'
echo -e >> home/depend 'proc'
echo -e >> tmp/depend 'swap\nproc\n'
echo -e >> usb/depend 'proc'
echo -e >> var/depend 'proc'
echo -e >> varlock/depend 'swap\nvar'
echo -e >> vartmp/depend 'swap\nvar'
echo -e >> sig_quit/depend 'home\nvartmp'

/etc/depinit/proc

If you were awake, you will have noticed that proc was not mounted, and that the example script would not work with proc. Here is the right script. There is no 'stop' script for proc because depinit would only have to mount it again to check that nothing is mounted before shutdown.



mkdir proc
echo -e   >proc/start '#! /bin/bash\n'
echo     >>proc/start '[ -f /proc/mounts ] || exec mount /proc'

There is no stop script - lots of things do not work if you unmount proc.

/etc/depinit/swap

It is about time we had some swapspace so the tmpfs's do not waste memory:



mkdir swap
echo -e   >swap/start '#! /bin/bash\nexec swapon -a'

Avoiding mount related problems

Mount needs /etc/mtab to contain accurate data. This is normally done by remounting / rw, and creating /etc/mtab. The other way to do it is to link /etc/mtab to /proc/mounts, and mount /proc. I prefer the second method, so that is what is in these example script. The advantage of doing it this way is that /etc/mtab always gives an accurate idea of what file systems are mounted. The disadvantage is that some information is lost. There is work in progress on getting the kernel to include this information.

Now for some real confusion. After boot up, imagine that you tell depinit to stop 'home' (depctl -k home), then you mount it by hand (mount /home), then you tell depinit to start home (depctl -s home). Without a check, to see if /home is already mounted, mount would and exit with an error. This would cause depinit to think that home had not started, so it would not start anything that depends on home, or run the stop script on shutdown.

Depinit would still unmount /home on shutdown because it contains code to unmount everything, but doing proper checks reduce the chance of inaccurate error messages. Missing out the stop script causes similar problems. Depinit's unmount on exit code is there to recover from accidents, not to make up for laziness.

I keep /var/run, /var/lock, /var/tmp, /tmp and /dev/shm on tmpfs's. I have over 2GB of swap space, but only 128MB of ram. If I put lots of data in a tmpfs (for example by compiling the kernel in /var/tmp), and turn off swap, the contents of the tmpfs must be loaded from swap into ram. It won't fit. The swapoff command does not stop running, and is hard to kill. The kernel knows it is short of vm, and starts killing things to free up space. This does not do any good because the vm is being used by tmpfs, not processes.

To avoid this happening (again ;-), I have proper bomb proof stop scripts to unmount anything that uses tmpfs, and no stop script for swap.

Finally, /var must be mounted before /var/run can be mounted. This shows the advantage of using individual mount scripts in depinit over using 'mount -a'. Depinit has proper dependency checking to allow mounts to work in parallel. 'mount -a -F' does handle parallel mounts, but only if there are no dependencies. Also, if any filesystem fails to mount, 'mount -a' exits with error status. Depinit would then not be able to start anything that depended on mounts - such as login.

Daemons and unbreakable pipes

Being able to log in is important, but is is not enough, so my next example shows how to set up unbreakable pipes:



mkdir daemons devfsd gpm klogd{,l} syslog{c,d,l}
echo >>default/depend -e 'daemons'
echo  >daemons/depend -e 'devfsd\ngpm\nklogd\nsyslogd'

echo  >devfsd/daemon -e '#! /bin/bash\n\nexec devfsd /dev -fg'

echo  >gpm/depend -e 'devfsd\nrw'
echo  >gpm/daemon -e '#! /bin/bash\n'
echo >>gpm/daemon    'exec gpm -D -m /dev/misc/psaux -t imps2 -a 3 -d 4 -3'

echo  >klogd/source -e  '#! /bin/bash\n'
echo >>klogd/source -en 'exec klogd -n -2 -f - 2>&1 -k '
echo >>klogd/source '"/lib/modules/$(</proc/sys/kernel/osrelease)/System.map"'
echo  >klogd/depend klogdl
echo  >klogdl/depend var
echo  >klogdl/dest -e '#! /bin/bash\n'
echo >>klogdl/dest -e '[ -d /var/log/multi/klog ] || {'
echo >>klogdl/dest -e '  mkdir -p /var/log/multi/klog'
echo >>klogdl/dest -e '  chown klogl:klogl /var/log/multi/klog'
echo >>klogdl/dest -e '  ln -s multi/klog/current /var/log/klog'
echo >>klogdl/dest -e '}'
echo >>klogdl/dest -e 'exec setuidgid klogl multilog t /var/log/multi/klog'

echo  >syslogd/depend -e 'devfsd\nsyslogc\nvar'
echo  >syslogd/daemon -e '#! /bin/bash\n'
echo >>syslogd/daemon -e "exec syslogd -n -m 0 -f $PWD/syslogd/config"
echo  >syslogd/config -e '*.*\t\t||/var/log/multi/syslog/fifo'
echo  >syslogc/depend syslogl
echo  >syslogc/source -e  '#! /bin/cat /var/log/multi/syslog/fifo'
echo  >syslogl/depend var
echo  >syslogl/dest -e '#! /bin/bash\n'
echo >>syslogl/dest -e '[ -d /var/log/multi/syslog ] || {'
echo >>syslogl/dest -e '  mkdir -p /var/log/multi/syslog'
echo >>syslogl/dest -e '  chown syslogl:syslogl /var/log/multi/syslog'
echo >>syslogl/dest -e '  ln -s multi/syslog/current /var/log/syslog'
echo >>syslogl/dest -e '}'
echo >>syslogl/dest -e '[ -p /var/log/multi/syslog/fifo ] ||'
echo >>syslogl/dest -e '    mkfifo --m 600 /var/log/multi/syslog/fifo'
echo >>syslogl/dest -e 'exec setuidgid syslogl multilog /var/log/multi/syslog'

chmod 754 */{start,stop,daemon,dest,source}

groupadd klogdl
useradd -g klogdl -s /bin/false -d / -s /bin/false klogdl
ln -s multi/klogd/current /var/log/klog
groupadd syslogl
useradd -g syslogl -s /bin/false -d / -s /bin/false syslogl
ln -s multi/syslogd/current /var/log/syslog

devfsd needs to be told not to fork, so that depinit can keep restarting it if required. The same is required for klogd, but I have told it to output to stdout, and told depinit to pipe the output to klogdl. klogdl runs multilog - a log rotator - as an ordinary user. If either klogd or klogdl dies, depinit can restart it with the old pipe, so no data is lost.

syslogd cannot output to stdout, but it will output to a fifo, and that can be copied to stdout by cat. depinit can do more complicated stuff with pipes such as one daemon with pipes to several others, and daemons acting as filters for other daemons. These features are there because they were simple to do. I have not yet found a use for them.

gpm is badly screwed up. The only way to stop it from forking is to use the debug option, and then you have to send all its trace output somewhere. Depinit starts daemons with stdout and stderr connected to /dev/null, so that is dealt with. If gpm was allowed to fork, execution of the daemon script would continue and exit. Depinit would spot that the process id assigned to gpm had terminated, so depinit would try to restart gpm. On the second attempt gpm would spot that it is already running, and terminate. Depinit would start it again, and again...

Loose ends

I think it is time to tidy up some loose ends, and sort out shutdown. The only new depinit concept is a stop script without a start script. This is useful for doing things just before shutdown.



mkdir apm c_a_d dmesg extras hwclock log rw sig_int
echo >>default/depend -e 'extras'
echo  >extras/depend -e 'apm\nc_a_d\ndmesg\nhwclock'
echo  >apm/stop -e '#! /bin/bash\n\nexec modprobe apm'
echo  >c_a_d/depend proc
echo  >c_a_d/start -e '#! /bin/bash\n\necho 0 >/proc/sys/kernel/ctrl-alt-del'
echo  >dmesg/start -e '#! /bin/bash\n\nexec dmesg -n 4'
echo  >hwclock/depend rw
echo  >hwclock/stop -e '#! /bin/bash\n\nhwclock --systohc --utc'
mkdir -p /var/lib/misc/hwclock
mv /etc/adjtime /var/lib/misc/hwclock
ln -s /var/lib/misc/hwclock/adjtime /etc/adjtime
echo  >log/depend 'syslogd'
echo  >log/start -e '#! /bin/bash -e\n'
echo >>log/start 'depctl --syslog notice'
echo >>log/start 'exec depctl --no-stderr'
echo  >log/stop -e '#! /bin/bash -e\n'
echo >>log/stop -e 'depctl -F /dev/console'
echo >>log/stop -e 'depctl --stderr debug'
echo >>log/stop -e 'exec depctl --no-syslog'
echo >>sig_quit/depend 'log'
echo  >rw/depend -e 'devshm\ntmp\nvartmp\nutmp\nvarlock'
echo  >sig_int/start -e '#! /bin/bash\n\nexec depctl -Q'
chmod 754 */{start,stop}

depinit needs the apm module loaded to reset, halt or shut down the computer. By default, the <ctrl><alt><delete> button does not do anything. c_a_d tells the kernel that it should send SIGINT to init when <ctrl><alt><delete> is pressed. dmesg prevents trivial kernel messages from being output to the console. I update the system clock with ntp. hwclock can be used to correct systematic errors in the hardware clock. The commands above make this work even when / is mounted read only.

hwclock probably does not need everything in rw, but I wanted to introduce the concept because I think it is a good idea. rw depends on all the standard read/write filesystems. The idea is to have a few standard service names for portability. I should probably use ro to mean that /usr and /opt are mounted, but I have been extending / with LVM rather than allocating new partitions.

The log scripts switch logging to syslog after syslog has started, but before sig_quit starts, so messages to the console do not hinder typing the user name and password. Also, on shutdown, logging is diverted back to the console when syslog dies. The debug setting is probably a bit verbose for every day use, and can be trimmed back to something reasonable like 'notice' or 'info'.

The final piece of magic is to do something with SIGINT. When depinit receives SIGINT, it starts 'sig_int'. This uses depctl to tell depinit to kill any children it did not create, stop everything, unmount everything and turn the power off.

Network scripts

I have been trying to decide what to do about the network. A service called network that starts all the interfaces looks useful as a standard thing for network daemons to depend on. Traditionally, the data to set up network interfaces goes in files in /etc/sysconfig/, and these files are detected, sourced and interpreted by complex init scripts.

IMarrogantO init scripts have become excessively complex so they can handle many interface setups from configuration files set up by GUI system administration program. X is one of the things I like to delete from secure servers, so I want the network configuration files to be easy to use without an administration program. The simplest answer is to just put the data in the depinit scripts, and call those scripts configuration data. If you want to separate data from code, you could always put the appropriate scripts in /etc/sysconfig/, and access them via symlinks.



mkdir eth0 lo name network wall
echo >>default/depend network
echo  >eth0/depend wall
echo  >eth0/start -e  '#! /bin/bash -e\n'
echo >>eth0/start -n 'ifconfig eth0 192.168.189.1'
echo >>eth0/start -n     ' broadcast 192.168.189.255'
echo >>eth0/start -e     ' netmask 255.255.255.0'
echo >>eth0/start 'echo 0 >/proc/sys/net/ipv4/conf/eth0/accept_redirects'
echo >>eth0/start 'echo 0 >/proc/sys/net/ipv4/conf/eth0/accept_source_route'
echo >>eth0/start 'echo 2 >/proc/sys/net/ipv4/conf/eth0/rp_filter'
echo  >eth0/stop -e '#! /bin/bash -e\n\nexec ifconfig eth0 down'
echo  >lo/depend proc
echo  >lo/start -e  '#! /bin/bash -e\n\nexec ifconfig lo 127.0.0.1'
echo  >lo/stop  -e  '#! /bin/bash -e\n\nexec ifconfig lo down'
echo  >name/depend proc
echo  >name/start '#! /bin/hostname urusai.no.nezumi.plus.com'
echo  >network/depend -e 'eth0\nlo\nname\nwall'
echo  >wall/depend proc
cat   >wall/start <<'EOF'
#! /bin/bash -e

echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_all
echo 1 > /proc/sys/net/ipv4/icmp_echo_ignore_broadcasts
echo 1 > /proc/sys/net/ipv4/icmp_ignore_bogus_error_responses
echo 0 > /proc/sys/net/ipv4/ip_forward
#    0 > /proc/sys/net/ipv4/tcp_ecn
echo 1 > /proc/sys/net/ipv4/tcp_syncookies
echo 0 > /proc/sys/net/ipv4/tcp_timestamps
echo 0 > /proc/sys/net/ipv4/conf/all/accept_redirects
echo 0 > /proc/sys/net/ipv4/conf/all/accept_source_route
echo 1 > /proc/sys/net/ipv4/conf/all/log_martians
echo 1 > /proc/sys/net/ipv4/conf/all/rp_filter

modprobe ip_tables
modprobe ip_nat_ftp
modprobe ip_conntrack_ftp
iptables-restore </etc/firewall
EOF
chmod 754 */{start,stop}

Now that we have a network, I think it is time to put it to some good use.



mkdir sshd{,l}
echo  >sshd/depend -e sshdl
echo  >sshd/source -e '#! /bin/bash\n'
echo >>sshd/source 'exec tcpserver -v 192.168.189.1 ssh /usr/sbin/sshd -i -e'
echo  >sshdl/depend -e 'rw\nnetwork'
echo  >sshdl/dest -e '#! /bin/bash\n'
echo >>sshdl/dest -e '[ -d /var/log/multi/sshd ] || {'
echo >>sshdl/dest -e '  mkdir -p /var/log/multi/sshd'
echo >>sshdl/dest -e '  chown sshdl:sshdl /var/log/multi/sshd'
echo >>sshdl/dest -e '  ln -s multi/sshd/current /var/log/sshd'
echo >>sshdl/dest -e '}'
echo >>sshdl/dest -e 'exec setuidgid sshdl multilog t /var/log/multi/sshd'
chmod 754 */{dest,source}
groupadd sshdl
useradd -g sshdl -s /bin/false sshdl
ln -s multi/sshd/current /var/log/sshd

I have not started sshd by default. It would be simple to make 'daemons' depend on sshd, but I wanted to an example for an a service started on the command line. sshd can be started with 'depctl -s sshd', and stopped with 'depctl -k sshd'. The stop command is a bit unsatisfactory because it leaves sshdl running. A better stop command is 'depctl -r sshd', because that takes out everything sshd explicitly depends on.


Contents