It's user error.

Debu.gs


Inferno, Part 3: Let's Make a Filesystem!

Everything is a filesystem. Everything! This is true to a much greater degree in Plan 9 and Inferno than in other operating systems, even Linux. Using filesystems is one thing, but building them lets you do some really cool things! So we are going to take a closer look at filesystems, 9P, and rolling your own fileserver using the Styxservers library.

There’s a lot of technical detail involved, but nothing difficult. You won’t need to know anything about monoids in the category of endofunctors, just some mundane and well-designed details of a simple protocol. One of the general goals of all of this stuff about Inferno that I’ve been writing so far has been to document things where I found gaps or where the documentation was scattered, since it’s all very easy if you already know it, but figuring it out is often a pain.

I covered this from a usage perspective in a previous article about namespaces. How this all actually works was glossed over, but since we’re going to be making a filesystem, we’ll now need to know how this all works. Luckily, it’s pretty simple! You’ll be able to roll filesystems in no time. It’s easier than building a web server, and you can use them from Linux and OSX, even.

You don’t need to know any of this in order to use the system, but you do need to know most of it in order to create your own fileservers. We’ll cover some background, and then build a couple of simple servers and make them do some cool things.

“That doesn’t sound like much fun.”

He who controls the filesystem controls the universe. If controlling the universe doesn’t sound like much fun to you, then I’m not sure how to help you.

“How’s that?”

Plan 9 and Inferno were engineered to be very good at sharing resources. When I say “resources”, I’m talking about arbitrary resources. Systems that expose a specific type of resource are abundant. You can share a network interface by running a NAT or a VPN. You can share disk space by using NFS or FTP or GlusterFS or any of a plethora of options. You can share computes by means of Hadoop or Map/Reduce grids, or BashReduce clusters or similar, and by somewhat more specialized systems like distcc, SETI@Home, Folding at Home, or GIMPS. You can share screens, keyboards, and mice between computers with tools like VNC and Synergy.

On the other hand, how would you design a system that shared arbitrary resources? Like, everything in the box? Screen, keyboard, microphone, speakers, processing resources, disk space, network hardware, and even access to running processes in memory, like clipboard buffer, /prog (Inferno’s /proc equivalent), the contents of files open in your editor, connections to IRC servers, servos on Lego devices (PDF), everything?

The first thing you’d need is a uniform way to describe resource hierarchies, and a way to ship them over the wire. Bell Labs took the filesystem approach (and I cannot stress enough that maybe it is a good idea to listen to the team that created Unix and C). Nearly every program interacts with files pretty seamlessly. If absolutely everything could be treated as a file and there were a simple distributed filesystem protocol, then any resource could be shared. Rob Pike likes to stress that the case is not that everything is a file, but that everything has a uniform interface, which is the filesystem.

Good News, Bad News, and Schroedinger News

The good news is that you can do a lot of really cool things once you can do this. If you’ve ever wanted to have a file with dynamic contents or if you’ve ever wanted write()s to a file to do interesting things or if you’ve ever felt that things like inotify and Unix domain sockets were cool but not quite powerful (or clean) enough, then you have come to the right place. You can even mount these filesystems in your Linux (via FUSE or an in-kernel driver) or your MacOSX or your BSD or whatever if you don’t run Plan 9 and you want this stuff to be useful outside Inferno. (I prefer to use Inferno to talk to the rest of the system, but your mileage may vary.)

The bad news is that I’m going to raise the barrier to entry a bit. The Inferno shell is really nice, and of course I love it, but Limbo is more conventional, so it has some advantages if you want to make a fileserver. If you know C or Go, it should be easy to follow what’s going on. You can do this sort of thing in the shell (see file2chan(1)‌, for example), but overall it’s easier to use Limbo.

This may be good news or bad news, but I’ve not collapsed the wave function yet so it’s possibly both (although I can assure you that I haven’t killed any cats, and haven’t been directly measuring Cesium decay), and it varies between observers, even. The Schroedinger news is that this entry is going to have to examine some bits and bytes and talk about some necessary but dry things before we get to the parts where you can actually do things with this information. Sorry or you’re welcome, whichever is more appropriate, although I can’t really know without observing your brain.

A Few Terms and Brief History

9P, the Plan 9 filesystem protocol, was developed alongside the OS. Since remote resource-sharing is a key fundamental concept of distributed computing, an OS based on distributed computing needs a uniform way to interact with resources.

Plan 9 was engineered to be able to share any of a system’s resources with any other collection of systems. In the canonical example, you run a terminal with a nice screen, mouse and keyboard, and this terminal connects to a CPU server with a bunch of speedy processors and plenty of RAM in it, and your files sit on a big fileserver with a bunch of huge, redundant disks. Access to these machines is granted by an auth server. This isn’t the only way to do business, although if you’re running a Plan 9 network, it is the usual way.

The vehicle for this is 9P, which has gone through three major revisions. The first came along with Plan 9. When building Inferno, however, the Styx protocol was created as a simplification of 9P, and then adjusted slightly during its port to Plan 9, with the end result being 9P2000, which is the version of 9P that both Inferno and Plan 9 now use. There have been a few extensions added to Unix-based versions of the protocol to allow it to operate as usual on 9P fileservers (symlinks, setuid bits, etc.), and the result is called “9P2000.u”, but since the extensions aren’t necessary or implemented in Plan 9 or Inferno, we won’t worry about them. Nowadays, 9P2000 is usually referred to as just “9P”, although a lot of the Inferno documentation refers to it as “Styx”, and 9P-related commands and libraries and functions often have “styx” somewhere in their name. I’m going to use “Styx” and “9P” to refer to 9P2000 interchangeably.

By the way, I hope that you clicked on that “cats” link up there by the throwaway Schroedinger joke, and that you expected animated gifs and were sorely disappointed. Because I’m a party pooper.

Let’s Talk about Real Things. This Part is Boring!

…Like the nuts and bolts of the protocol itself. You’ll need an overview of how the protocol works in order to write a fileserver, but there’s no need to memorize it.

The client initiates communication by sending a “T-message” and the server responds with an “R-message”, usually called “Tmsg” and “Rmsg” respectively. A Tmsg from the client followed by an Rmsg from the server is known as a transaction.

A Tmsg contains a tag (a 16-bit unique ID) that the client generates to identify the message, and the Rmsg for a given Tmsg is expected to contain the same tag. The reason for the tag is to allow for clients that send off requests before responses come back; think about asynchronous clients and multiplexing. It does mean that you can only ask a server for 65,535 operations at a time (0xFFFF is a special tag), but hopefully you won’t mind waiting for a bit if you’re asking a server to do so many things simultaneously that you run out of tags.

For any operation the client wants to perform on a file, there is a 32-bit “fid”. Like tags, fids are generated by a client, but are used to identify specific files and directories rather than transactions. The server is expected to have a “Qid” for each file it presents, one byte for the type, four bytes for the version (i.e., a count of modifications), and an eight-byte unique identifier for that object within the filesystem.

There are a few caveats if you’re used to Unix filesystems. For example, the user and group that owns a given file is now a string rather than a numeric UID/GID, since the numeric IDs can vary between systems, and there’s no distinction between local and remote fileservers.

9P messages and transactions

There are 27 message types in 9P: Tversion, Rversion, Tauth, Rauth, Tattach, Rattach, Tflush, Rflush, Twalk, Rwalk, Topen, Ropen, Tcreate, Rcreate, Tread, Rread, Twrite, Rwrite, Tclunk, Rclunk, Tremove, Rremove, Tstat, Rstat, Twstat, Rwstat, and Rerror. For a given Tmsg, the server sends back either a corresponding Rmsg, or Rerror. So, except for Rerror, there are 13 pairs, a client request message and a server response message. A Tmsg from the client and the Rmsg or Rerror from the server constitute one transaction.

A version transaction is for (predictably) version negotiation. Auth is for in-band authentication; it’s not often used, as the authentication is usually handled before the fileserver even sees the client. Attach establishes the user and group that the client represents and the fid the client will use for the root directory of the filesystem; it’s the general entry point. Once you have the fid for the root of the filesystem, you can walk around it. Walk transactions get you to different points in the filesystem: you can walk one step at a time, from a directory to an entry in the directory (either a file or a subdirectory) or to the containing directory. Then there’s clunk, which discards a fid. Flush aborts a request.

Once you have a fid, you can carry out normal filesystem operations, like open, create, read, write, remove, stat, all of which are self-explanatory. Wstat transactions alter the information given by stat (provided the fileserver allows it), so, under the surface, this is what carries out chmod et al.

State of the Protocol

9P is a stateful protocol. This doesn’t mean the connection has to be kept alive per se (although we’re going to use TCP, 9P has a pretty good tolerance for variations in the underlying protocol; the Styx on a Brick project used IR). At the 9P level, there’s a shared state between the server and the client. Implementing a stateful protocol may sound like a pain if you’re used to implementing web services, until you remember that web services as they’re known today usually need to keep some sort of state, and recognizing state does help if you don’t want to have to pack session state (or an ID that points to a data store where you keep the session state) into a cookie, and unpack it with each request. If you compare 9P to, say, OAuth2 session management (which can tax both the programmer and the computer), having a little state in the protocol seems pretty reasonable. (One might legitimately wonder why people bother with HTTP for stateful applications to begin with.)

Making handling state a bit easier, Plan 9 is pretty big on Tony Hoare’s Communicating Sequential Processes. So Go and C (via Plan 9’s libthread, not to be confused with the sort of thing implemented in pthreads, which do sequential processes without the “communicating” part) implement CSP. A lot of other languages are built on this model (like Erlang) or have added it (like Clojure). Since we’re using Inferno, though, it’s more relevant for our purposes that Limbo has CSP-style concurrency built in.

Of course, you can drop CSP and just stick to state machines if you are Alan Cox. He’s a pretty cool guy.

Limbo!

It’s a great language. It’s like Go, due to the significant overlap of the teams that did both. Go came later, and follows more or less the same path, with some improvements, but with less OS support for the things Limbo does. So a few things are nicer in Go, a few are nicer in Limbo.

You won’t have to know Limbo to make sense of this, since I’ll be explaining any novel constructs inline. If you want to learn the language, the incredible Brian W. Kernighan wrote an introduction to it called A Descent into Limbo, the late and incredible Dennis Ritchie has written a somewhat more formal description of Limbo, and there’s a book if you’d like to read it, Inferno Programming with Limbo by Phillip Stanley-Marbell. They’re really great resources, and I recommend them if you plan on doing Limbo coding.

Styxservers

It’s a library that does basically what you want if what you want is to build Styx fileservers. In the words of styxservers(2)‌:

When writing a file server, there are some commonly performed tasks that are fiddly or tedious to implement each time. Styxservers provides a framework to automate some of these routine tasks. In particular, it helps manage the fid space, implements common default processing for protocol messages, and assists walking around the directory hierarchy and reading of directories. Other tasks, such as defining the structure of the name space, and reading and writing files in it, are left to the file server program itself.

If that didn’t make sense, please re-read this entry from the beginning. If it still doesn’t make any sense, then I’m doing a bad job, and maybe you should try Charles Forsyth’s version.

Let’s Actually Make a Real Filesystem Now

I can say with absolute, unwavering certainty that Ori Pomerantz is correct when he says this:

When the first caveman programmer chiseled the first program on the walls of the first cave computer, it was a program to paint the string “Hello, world” in Antelope pictures. Roman programming textbooks began with the “Salut, Mundi” program. I don’t know what happens to people who break with this tradition, but I think it’s safer not to find out.

So we’ll start off with hellofs. We’ll produce a filesystem that says “Hello, World!” first. You can grab the source code and follow along. It starts off with the usual share of includes/imports/definitions, and then gets to this:

Qroot, Qhello, Qmax: con iota;
tab := array[] of {
	(Qroot, ".", Sys->DMDIR|8r555),
	(Qhello, "hello", 8r444),
};

That’s the heart of the filesystem, essentially. We set up a few constants to represent the two qids the server will be using, and then make a little table using them to represent the top-level directory and a file inside it. They’re both read-only.

con iota may have looked a bit funny; it’s the Limbo equivalent of #defineing constants or making an enum in C (but more powerful: it allows for things like x, y, z: con 1<<iota for bitfields, etc.). And then there’s the more uniform syntax for alternative bases, where you separate the base from the number with an r, but that was probably obvious.

Once we hit init, the Limbo equivalent of main, we load up the modules we’ll be using, figure out the user that we’ll be presenting as the owner of the files (which is just a cosmetic touch, as they’re globally readable and never writable), and then call the initializers for the styx library and the styxservers library. Nothing serious. Next up, we start the Navigator.

The Navigator!

That’s one of the two important bits of the fileserver! The Navigator handles filesystem navigation, which in our case is very simple. Here’s how we start it up inside init:

	navch := chan of ref Navop;
	spawn navigator(navch);
	nav := Navigator.new(navch);

First we give it a channel through which it will receive navigation operations (like Tstat or Twalk), then we spawn it, which creates a new Inferno process (the Limbo equivalent of Go’s go, which in POSIX C terms is more like pthread_create than like fork), and then initialize a Navigator object and give it the channel to send its navigation operations through. The navigator() function looks like this:

navigator(c: chan of ref Navop)
{
	loop: while(1) {
		navop := <-c;
		pick op := navop {
		Stat =>
			op.reply <-= (dir(int op.path), nil);
			
		Walk =>
			if(op.name == "..") {
				op.reply <-= (dir(Qroot), nil);
				continue loop;
			}
			case int op.path&16rff {
			Qroot =>
				for(i := 1; i < Qmax; i++) {
					if(tab[i].t1 == op.name) {
						op.reply <-= (dir(i), nil);
						continue loop;
					}
				}
				op.reply <-= (nil, Enotfound);
			* =>
				op.reply <-= (nil, Enotdir);
			}
			
		Readdir =>
			for(i := 0; i < op.count && i + op.offset < (len tab) - 1; i++)
				op.reply <-= (dir(Qroot+1+i+op.offset), nil);
			op.reply <-= (nil, nil);
		}
	}
}

It’s a big loop, and at the beginning we do a blocking read on the channel (the navch passed down above), so this process (remember that we spawn‌ed it earlier) starts by doing nothing until it gets something to do. Simple enough!

Navop‌s come through the channel, and we process them by first using the pick statement to see what navigation operation we have. A Navop is a “pick ADT”. Conceptually, you can treat the ADT and accompanying pick statement like a switch statement in C that checks the tag in a union (although, unlike C, Limbo has some type- and memory-safety requirements that prevent you from looking at fields inside a pick ADT without first pick‌ing it).

So we’ve got one of Stat, Walk, or Readdir, which have been translated from 9P messages into this ADT. Stat is the simplest: we have a qid (path). The Navop has a channel called reply, which is how this process is expected to respond. Since this is a simple operation on a simple filesystem, we just pass the data back that way. The extra nil in our reply tuple is for errors, which we can’t have in this case, our filesystem being static.

Walk is slightly trickier, as it’s the heart of the navigator. Since we serve a single-level filesystem with only a single file in it, all attempts to walk up (i.e., ..) simply go to the root, so we send the root back in those cases and skip the rest with the continue. Next up, we look at the current path and see if it’s the root (i.e., Qroot, the qid of the root directory of our modest fileserver). Again, since it’s a single-level filesystem, we just loop through all the entries in our table, looking for one that matches the supplied name, and reply with that if we find one. If we don’t, we send nil for the directory entry and Enotfound for the error field.

Readdir you should be able to reason about if you understood the above. We only have one directory, the root, so any attempt to read a directory will go there, as it’s the only directory and the client must have opened it if they’re reading it, so there’s no need for a check to find the directory we’re reading in this case. The odd-looking bits are for managing the offset and read size. To indicate that we’re done, we send (nil, nil): no contents, and no errors.

The Server Loop

The last two lines of init look like this:

	(tc, srv) := Styxserver.new(fildes(0), nav, big Qroot);
	servloop(tc, srv);

The first line creates a Styxserver and gives it the file descriptor to use (in this case, stdin; we’ll come back to that detail), the Navigator that we created above, and the qid of our root directory, which we cast because qids are 64 bits. It returns a channel of Tmsgs (tc) and the new Styxserver (srv), which we pass along to our servloop() function.

Here’s the server:

servloop(tc: chan of ref Tmsg, srv: ref Styxserver)
{
	loop: while((tmsg := <-tc) != nil) {
		pick tm := tmsg {
		Open =>
			srv.default(tm);

		Read =>
			f := srv.getfid(tm.fid);
			if(f.qtype & Sys->QTDIR) {
				srv.default(tm);
				continue loop;
			}
			case int f.path {
			Qhello =>
				srv.reply(styxservers->readstr(tm, greeting));
			* =>
				srv.default(tm);
			}
		* =>
			srv.default(tmsg);
		}
	}
}

It’s another big loop, as expected. First it reads Tmsgs from the channel, and then we do another pick to get at the contents of the message. Where the navigator handles (ahem) navigating the filesystem, here we deal with the contents of the files themselves. If we get a Topen we actually just…punt! The library can handle the replies for opening files with the information we’ve given it already: we have given it the ability to read directories, we’ve given it permissions for our qids, so it can reason about all of these things. (If you’re interested in the specifics of how it does this, see /appl/lib/styxservers.b:/^Styxserver\.default (or around line 425 if you’re not using acme).

Read is the interesting bit. First we extract the fid from the message, which you might remember is the client’s identifier for an object in the filesystem, and we get a Fid object back from the server, which has an attached qtype field, i.e., the type of object for this qid. If it’s a directory, we again defer to the server, which will end up deferring to the Navigator after handling some of the lower-level bits.

Otherwise, we do a case statement. In this case, we’ve hard-coded individual files, of which we have only one. If a client tries to read /hello from our filesystem, we reply by sending back the string in the constant greeting. readstr() here takes the Tmsg and the string, and produces an appropriate Rmsg by taking into account things like offset and the size of the read that the client has requested. If they’re reading some other file, we send the default reply.

For any other type of message, once again we just lean on the library to handle it. Clunks and writes and flushes and everything.

Let’s Mount the Filesystem!

So the reason we expose the filesystem on standard output is that this is actually the easiest and simplest way to do this. Inferno’s tooling is very well-designed: we don’t have to build everything, just the fileserver. Network I/O, authentication, encryption, that’s all handled elsewhere, by software that plugs straight into ours, so although we could write it, we don’t need to. An example of this is mount(1)‌, which works with a network address or a block that serves a filesystem over stdio. Let’s see it in action by compiling and running our humble fileserver:

% limbo -gw hellofs.b
% mount {hellofs} /n/hellofs
% lc /n/hellofs
hello
% cat /n/hellofs/hello
Hello, World!
% 

Look at that, it worked! After getting a basic understanding of the protocol, a working filesystem is not that tricky! Let’s try showing the filesystem to the rest of the world.

Exporting

We can get it out to the rest of the world very simply:

% listen -A 'tcp!*!18519' { export /n/hellofs & }

You can also use styxlisten(1)‌, which is only slightly different, and doesn’t involve spawning an export(4)‌ per connection. Or, because every instance of hellofs is essentially the same, you could even use hellofs instead of the export. (Note that we’re using the -A flag to skip authentication.)

Let’s Mount it Under Linux!

There’s the aforementioned v9fs support in the kernel, but it’s somewhat less friendly to operate directly. I like sqweek’s 9mount tools for this task:

$ mkdir /tmp/hellofs
$ 9mount 'tcp!localhost!18519' /tmp/hellofs/
$ ls -lh /tmp/hellofs/
total 512
-r--r--r-- 1 4294967294 4294967294 14 Dec 31  1969 hello
$ cat /tmp/hellofs/hello 
Hello, World!
$ 

Well, the uid/gid bits are a little ugly, but there it is, in all its glory! If you’ve got P9P or wmii installed, you can use tools included with those without mounting the filesystem directly:

$ 9p -a 'tcp!localhost!18519' ls -l
--r--r--r-- M 0 pete pete 14 Dec 31  1969 hello
$ 9p -a 'tcp!localhost!18519' read hello
Hello, World!
$ wmiir -a 'tcp!localhost!18519' ls -l /
--r--r--r-- pete pete    14 1969-12-31 16:00 hello
$ wmiir -a 'tcp!localhost!18519' read hello
Hello, World!

Hey, it got my name right this time! (This is not an option for a Linux kernel driver, since it’s got to translate things into something a POSIX environment can understand.) There’s a long list of libraries that implement 9P, server-side as well as client-side, so knock yourself out with novel ways to greet the world if you like.

Let’s take a closer look at the protocol, and then try a slightly more ambitious filesystem.

A Closer Look at the Protocol

If you got the source code and have been following along, you probably noticed the lines trace: con 0; and styxservers->traceset(trace);. You can set that constant to 1 to get a dump of all the protocol messages. The styxservers library has the option to dump all of the Tmsgs and Rmsgs to stderr, which it does by calling .text() on the messages as they come in or go out. You can do this yourself with individual Tmsgs and Rmsgs by calling .text() on them and printing that to stderr (you can’t use stdout, remember: your debugging messages will trample the protocol messages and make your client give up on trying to understand the server). Let’s set trace to 1 and see what actually happens under the hood:

% unmount /n/hellofs
% limbo -gw hellofs.b
% mount {hellofs} /n/hellofs
<- Tmsg.Version(65535,8216,"9P2000")
-> Rmsg.Version(65535,8216,"9P2000")
<- Tmsg.Attach(47,168,4294967295,"pete","")
-> Rmsg.Attach(47,Qid(16r0,0,16r80))
% lc /n/hellofs
<- Tmsg.Stat(47,168)
-> Rmsg.Stat(47,Dir(".","pete","pete",Qid(16r0,0,16r80),8r20000000555,0,0,0,16r0,0))
<- Tmsg.Walk(47,168,111,nil)
-> Rmsg.Walk(47,array[] of {})
<- Tmsg.Open(47,111,0)
-> Rmsg.Open(47,Qid(16r0,0,16r80),8192)
<- Tmsg.Read(47,111,0,2048)
-> Rmsg.Read(47,array[62] of byte)
<- Tmsg.Read(47,111,62,2048)
-> Rmsg.Read(47,array[0] of byte)
<- Tmsg.Clunk(47,111)
-> Rmsg.Clunk(47)
hello
% cat /n/hellofs/hello
<- Tmsg.Walk(47,168,156,array[] of {"hello"})
-> Rmsg.Walk(47,array[] of {Qid(16r1,0,16r00)})
<- Tmsg.Open(47,156,0)
-> Rmsg.Open(47,Qid(16r1,0,16r00),8192)
<- Tmsg.Read(47,156,0,8192)
-> Rmsg.Read(47,array[14] of byte)
Hello, World!
<- Tmsg.Read(47,156,14,8192)
-> Rmsg.Read(47,array[0] of byte)
<- Tmsg.Clunk(47,156)
-> Rmsg.Clunk(47)
% unmount /n/hellofs
<- Tmsg.Clunk(47,168)
-> Rmsg.Clunk(47)
% 

First after we mount it, you can see the version info exchange, and the attach message. Then when we do the lc, it opens the directory, reads it, and then clunks it. The cat opens up the file, reads 14 bytes, prints them, and then gets no more bytes on further attempts to read, so it clunks it and terminates. Finally, we unmount, which clunks the fid we got from our initial attach, indicating that we’re done with it. It’s pretty straightforward, right? I hope?

Slightly More Ambitious: slightlyfs

Given that fileservers are simple to build, you can do a lot of cool stuff. There are a lot of examples in both Plan 9 and Inferno. You can have a look at acme(4)‌ to see the filesystem that acme(1)‌ exports. MJL built hgfs (among several other filesystems; he’s been prolific), which lets you view a Mercurial repository as a filesystem. Mycroftiv wrote hubfs, which multiplexes I/O, adding screen/tmux/dtach-like functionality to programs that use stdio (and even do things like edit and replay the I/O). Of course, there are also more conventional examples that serve up files on disk, like 9660srv(4)‌ and memfs(4)‌.

You could go crazy with it if you wanted. You could build a filesystem that automatically computes and adds SHA-1 sums for files that you write to it on the fly (but be careful about clients seeking to different offsets!) and that passes the writes through to another filesystem for storage.

Let’s try something a little less ambitious. Just slightly more ambitious than hellofs. We could do something that returns the time, but you might have noticed that /dev/time already does this (see cons(3)‌), and in fact, the library calls to get the time just use that file, so that’s a little cheap, I guess.

Just slightly!

…Although it’s still a bit over the top. Follow along with the source if you dare!

We’re going to add a few features to the filesystem to give a taste, a mere inkling, of what you can do. First we’ll need to include a few more libraries, which I’ve dropped above the include for draw.m:

include "rand.m";
	rand: Rand;
include "daytime.m";
	daytime: Daytime;
include "bufio.m"; bufio: Bufio;
	Iobuf: import bufio;
include "mhttp.m"; http: Http;
	Url, Hdrs, Req, Resp: import http;

Note that we’re using MJL’s HTTP library, so you’ll have to install that if you want all of the new bits we’re adding to work.

Then we’ll add three more entries to the list of constants and the files table:

Qroot, Qhello, Qfortune, Qcat, Qtick, Qmax: con iota;
tab := array[] of {
	(Qroot, ".", Sys->DMDIR|8r555),
	(Qhello, "hello", 8r444),
	(Qfortune, "fortune", 8r444),
	(Qcat, "cat.gif", 8r444),
	(Qtick, "tick", 8r444),
};

Then let’s add a place to stuff the fortunes and a channel for ticks, which I put under the global for the username:

fortunes: array of string;
tickch: chan of int;

Then we’ll load and initialize those libraries as necessary, so here’s our new init():

init(nil: ref Draw->Context, nil: list of string)
{
	sys = load Sys Sys->PATH;
	styx = checkload(load Styx Styx->PATH, Styx->PATH);
	styxservers = checkload(load Styxservers Styxservers->PATH, Styxservers->PATH);
	rand = checkload(load Rand Rand->PATH, Rand->PATH);
	daytime = checkload(load Daytime Daytime->PATH, Daytime->PATH);
	bufio = checkload(load Bufio Bufio->PATH, Bufio->PATH);
	http = checkload(load Http Http->PATH, Http->PATH);
	
	user = readfile("/dev/user");
	if(user == nil)
		user = "hellofs";

	styx->init();
	styxservers->init(styx);
	styxservers->traceset(trace);

	readfortunes();
	rand->init(daytime->now());
	http->init(bufio);

	tickch = chan of int;
	spawn ticker(tickch);

	navch := chan of ref Navop;
	spawn navigator(navch);

	nav := Navigator.new(navch);
	(tc, srv) := Styxserver.new(fildes(0), nav, big Qroot);
	servloop(tc, srv);
}

Yes, we’re seeding the PRNG with the current time. This is fine for fortunes.

Since we haven’t altered the hierarchy, the navigator is fine as-is. But since all three of the new files have varying contents, we’ll have to keep their contents fixed across reads to different clients. Styxservers luckily provides a generic .data field that it keeps with Fid‌s, for stuffing arbitrary what-have-ye’s. So we’ll fix the contents on Topen by filling in that field in servloop() and then clients that seek all over won’t have a problem:

		Open =>
			f := srv.getfid(tm.fid);
			case int f.path {
			Qfortune =>
				f.data = array of byte pickfortune();
			Qcat =>
				f.data = hget("http://edgecats.net/");
			Qtick =>
				tick := <-tickch;
				f.data = array of byte sys->sprint("%d\n", tick);
			* =>
				;
			}
			srv.default(tm);

The srv.getfid() is just like with the Tread, but here we’re doing nothing but setting the .data field depending on the file the client opens up.

Since we’re squirreling data away on open, we’ll need to alter what we send on a read. Luckily, this is easy: we use styxservers->readbytes, which does for an array of bytes what styservers->readstr() does for strings. We just add this under the read handler for Qhello:

			Qfortune or Qcat or Qtick =>
				srv.reply(styxservers->readbytes(tm, f.data));

Since we stuck the data in f.data for all of them, we can handle it uniformly.

There are some other additions to support these new bits, like pickfortune() and hget(). Those are not so much about filesystems, so you can read them in the source if you’re interested. Let’s build and mount the new filesystem and look at the three new additions individually:

% limbo -gw slightlyfs.b
% mount {slightlyfs} /n/s

/fortune

% cat /n/s/fortune
We are all interested in the future, for that is where you and I are going to spend the rest of our lives.  --Criswell, Plan 9 from Outer Space
% cat /n/s/fortune
And remember my friend, future events such as these will affect you in the future.  --Criswell, Plan 9 from Outer Space

That should probably be self-explanatory. At startup we read /lib/games/fortunes, split it into lines, and then give a random one every time the client opens the file up. Inferno only ships with 8 fortunes, but it’s very simple to bind a different file over that path. (Plan 9 ships with several more, 9front in particular ships with a wide array of files that work, including the usual Plan 9 fortunes, with some additions.) If you’ve ever wanted to rotate your mail signatures, you could actually use this filesystem for that, by pointing your .sig at it (by binding it or with the more pedestrian symlink).

/tick

This one is more interesting than it looks:

% cat /n/s/tick
0
% cat /n/s/tick
1
% cat /n/s/tick
2

On the surface, it is just a counter. But, critically, it is a synchronized counter. Try doing the opens all at once, but delay some of them:

% {sleep 1; cat} < /n/s/tick & {sleep 2; cat} < /n/s/tick & cat /n/s/tick; sleep 2
5
3
4

Remember the read from tickch on Open and the ticker() that we spawn? Here’s what that function looks like:

ticker(c: chan of int)
{
	i := 0;
	while(1) {
		c <-= i++;
	}
}

Simple, right? But it guarantees a unique sequential number for each open by a client, which has a number of uses around the house (provided you do distributed computing around your house). If you add a /tock that gives the last number written to it, then you have implemented a mutex that you can expose to the network, using Lamport’s bakery algorithm, but with filesystem-based message passing rather than shared memory. You’d probably want to use the IPints libarary if you are concerned about a 32-bit integer wrapping around.

/cat.gif

For /cat.gif, we make an HTTP request to edgecats.net (which was created by my late friend Carlo Flores, who has been memorialized by Thomas Dunlap, whose site is really fun). This gets us exactly what you expect. Since opening involves making an HTTP request and reading the body back, there’s a noticable latency on opening the file, but there should be very little on reading it. You could get rid of some of the latency here by spawning a process to read the body and send its contents back through a channel, and then reading the channel whenever the client needs the data. Exercise left to the reader.

In Closing

If I’ve done my job, you’ve got the basics of 9P and enough knowledge to understand the varied fileservers and what’s so cool about all of this. I like filesystems as an abstraction, maybe because I’ve been raised on Unix. But reading and writing files is one of the few abstractions that, in practice, actually does work cross-platform, across nearly every OS, in the shell as well as in more heavyweight languages.

“Why did this take so long?”

Because I break promises to the internet! I believe that I’ve mentioned this before.

I could not think of a tactful way to plug my consulting business, so we’re going to just do this: This is a plug for my consulting business. (At least it was honest enough to keep me from feeling too dirty.)

As usual, I’m indebted to MJL, who has written enough Limbo code to choke a horse. Consequently, if I have trouble figuring something out, 90% of the time I can find a project he wrote that contains code that solves my problem. Mycroftiv helped with the article itself when it was a draft, and he’s building a castle and also is about to drop some updates to ANTS, which is amazing but also hard to explain.

HN discussion

Looks like there is a discussion on Hacker News.

Tags: code inferno


<< Previous: "Brainfind a Stranger in the Alps"
Next: "Making Music with Computers: Two Unconventional Approaches" >>