Advanced Namespace Tools blog 11 February 2017

Adding Interrupt Note-Passing to Hubfs Clients

In a surprising (to me) turn of events, adding this feature turned out to be easier than dealing with end-of-file messages as described in the previous post. I'm going to try to go into a bit more detail about some general Plan 9 topics in this post, and how they relate to standard unix mechanisms.

Let's start with the practical user-focused issue: when using a standard shell window in rio, the Plan 9 window manager, pressing the 'delete' key will interrupt the process running within it. The equivalent action in standard unix is ctrl-c. Previously, hubfs had no facility for connecting a locally pressed 'delete' key into an interrupt for the other processes reading/writing to the hubs (aka the processes running "within" the hubfs session.)

I was expecting it to be a complex hassle to add this feature, but it turned out to be much easier than I expected. I'll start by providing some background on how unix does things in comparison to Plan 9.

Unix Signals

As more or less everyone (even me) knows, Unix and posix have a simple but limited interprocess communication method called "signals". There are 30-something standard signals, and they are a bit of a mess. Some are for OS error conditions, some are for limited interprocess communication, and there are several varieties to tell a process to exit.

Programs interact with signals by creating signal handler functions. When a signal is sent to a process, the OS pauses its execution and invokes the provided signal handler. In an event the program doesn't handle the signal specifically, a default behavior may be chosen by the host os kernel. Signals are sent either by using the kill (2) library function or using "kill" from an interactive shell. The use of ctrl-c to send an interrupt is a special case implemented by the kernel TTY layer interacting with the shell's job management facility.

Plan 9 Notes

Plan 9's general design principle is to take Unix ideas and make them both simpler and more consistent, and in doing so, make them more flexible. Plan 9 implements "notes" rather than signals, with two main differences:

Even though the /note and /notepg (notes for a whole group of processes are written to notepg and will be received by every process in the note group) files receive arbitrary strings, in practice most notes are still conventionalized along similar lines to traditional Unix: "interrupt", "hangup", "alarm", and a family of system-generated notes such as "sys: write on closed pipe" account for the majority of notes in practice. The handling mechanisms are similar also, with functions registered via atnotify (2). The rc shell offers the ability to define handling functions for the most common notes just by creating functions such as "fn sighup{}" which will be invoked when a hangup note is received.

How 9front rcpu does Note-Passing

Instead of ssh for accessing remote systems, Plan 9 has traditionally used a command called "cpu". The original cpu(1) exists in 9front still, but has been largely superseded by rcpu(1), which functions equivalently but is implemented as a small shell script. One thing that both cpu and rcpu have to do is pass notes such as the delete-key generated interrupt from the local to the remote side. As preparation for adding this feature to hubfs, it made sense to study how rcpu does it. Here is an edited excerpt of rcpu's fn client{}:

bind '#|' /mnt/cpunote || exit
</fd/0 exec $exportfs -r / &
</dev/null >/mnt/cpunote/data1 {
	fn sigkill { echo -n kill >/mnt/cpunote/data1 }
	fn sighup { echo -n hangup >/mnt/cpunote/data1 }
	fn sigint { status=interrupted }
	wait
	while(~ $status interrupted) {
		echo -n interrupt
		wait
	}
}

And here is part of the fn server{}:

mount -nc /fd/0 /mnt/term || exit
[ mainproc setup omitted ]
if(test -d /mnt/term/mnt/cpunote) {
	rfork e
	mainproc=$apid
	{cat; echo -n hangup} </mnt/term/mnt/cpunote/data >/proc/$mainproc/notepg &
	wait $mainproc
}

So, on the client side we create a pipe and put it at /mnt/cpunote. This will be found at /mnt/term/mnt/cpunote on the server side, because the client's exportfs of its root is mounted at /mnt/term. Then, we define interrupt handling fns which receive notes and write them to the /mnt/cpunote/data1 pipe input. The server side just runs "cat" to read the notes from the pipe output and write them to to the notepg file of the main process (run earlier, with process id saved in $apid).

This example shows a lot of the brilliance of Plan 9's design: something seemingly complex, like communicating notes/signals to processes running on another machine, can be done from the shell with very few lines of code. The ubiquitous 9p protocol elides the distinction between local and remote, and consistent use of file-based interfaces let standard utilities be used from the shell to accomplish most tasks.

Note-Passing using the Hub script and Hubshell client

To implement similar functionality for hubfs and its clients requires a similar, but not identical, technique. The main architectural difference is disconnection/reconnection. A simple pipe won't work, because new clients won't have that pipe in their namespace when they attach. We need something that provides similar functionality to a pipe, but will be persistently available in the namespace to any client that connects. Fortunately, there is a convenient tool available for creating such a thing: hubfs itself!

Because hubfs itself just handles the input and output to hubfiles and doesn't know or care about what program might be doing the reading and writing, all of the logic for this goes in the Hubshell client program and Hub wrapper script, which work together to set up hubfs-connected shells. The Hub wrapper script only needed two lines of code added around the line that connects an rc to the hubfiles:

touch /n/$srvname/$attach^0.note
rc -i </n/$srvname/$attach'0' >/n/$srvname/$attach'1' >[2]/n/$srvname/$attach'2' &
cat /n/$srvname/$attach^0.note >/proc/$apid/notepg &

That makes an additional hubfile, and then starts a cat to copy anything written to it to the notegroup of the hubfs-connected shell. Unlike the system used by Unix TTY/shell interrupts, the controlling shell and the processes it starts do remain in the same note group. With this in place, we just need to add a suitable interrupt-note handler function to Hubshell:

/* receive interrupt messages (delete key) and pass them through to attached shells */
int
sendinterrupt(void *regs, char *notename)
{
	char notehub[SMBUF];
	int notefd;
	if(strcmp(notename, "interrupt") != 0)
		return 0;
	sprint(notehub, "%s%s.note", hubdir, basehub);
	if((notefd = open(notehub, OWRITE)) == -1){
		fprint(2, "can't open %s\n", notehub);
		return 1;
	}
	fprint(notefd, "interrupt");
	close(notefd);	
	return 1;
}

Then during the setup portion of main() we register the note handler:

atnotify(sendinterrupt, 1);

And that's all! Now you can press 'delete' to interrupt remote processes running in remote hubs, and it behaves just like the standard rio/rc interface. Additional shells added with %remote or %local will also have their own note-passing hubs, because they are also created just by invoking the "hub" script.

I had slacked for years without implementing this, because I foolishly assumed that it would be way more difficult than it actually was. I should have remembered that in Plan 9, it is almost always simpler and easier than you were expecting, because of the consistency and clarity of the core principles the system is built upon.