Contents

Plan 9 file systems

Introduction

The package plan9 provides some classes which can be used to provide Plan 9 file system servers. The two most important classes are Session9P and Data9P. The former is the class which handles the 9P i/o, and the latter provides the data which forms the file system itself. The logic regarding file permissions, message types and so on is contained in Session9P, so that Data9P remains as abstracted from these details as possible.

To create a file server, we first must provide a subclass of Data9P, implementing the various methods to specify the filesystem to be provided. Then, a Session9P is instantiated, passing the Data9P instance to the constructor. Finally, one of three methods is used to actually provide the server.

The first, io(), accepts a file f as a parameter (usually one end of a pipe). 9P requests are read from this file, processed using the Data9P instance, and responses are written to the same file. io() exits when it encounters end-of-file, or an i/o error, on f. Messages are processed in order, ie the next request is not read before the previous request’s response has been sent.

The second method, async_io() accepts two parameters; a io.Scheduler and a file f. It returns immediately, after starting a io.Task in the scheduler, which reads 9P messages from f. Each request is then further handled in other Tasks. Unlike io(), messages are not processed in strict order, and a particular reply may happen after other incoming requests have been received, and perhaps responded to.

Finally, the third method is io_task(), which is like io(), but runs as a Task inside a Scheduler. 9P messages are thus processed in strict order, but in the background. This is useful for simple servers running as part of a larger program (such as a GUI). It is not useful for standalone server programs, which may as well use io().

Hopefully the above will become clearer with the help of some examples.

An XML file as a file system

The first example takes an XML file, and turns it into a file system. In fact, it can also serve several different XML files at once, each as a separate file tree.

Download xmlfs.icn

import plan9, ipl.server9p, ipl.options, io, util, exception, xml

class XMLData9P(ReadOnlyData9P)
   private
      user, time, root
      
   public new(root)
      user := Files.file_to_string("/env/user") | "none"
      time := Time.get_system_seconds()
      self.root := root
      return
   end

   public override get_root(aname)
      if /root then {
         if *aname = 0 then
            throw("Mount requires an attach parameter (an XML file)")
         return parse_file(aname) | throw(&why)
      } else {
         if *aname ~= 0 then
            throw("Mount expected an empty attach parameter")
         return root
      }
   end

   public override get_parent(p)
      local q
      q := p.get_parent()
      if is(q, XmlElement) then return q
   end

   public override get_child(p, name)
      local c
      every c := !p.children do {
         if is(c, XmlElement) & c.name == name then
            return c
      }
   end

   public override gen_children(p)
      local c
      every c := !p.children do {
         if is(c, XmlElement) then
            suspend c
      }
   end

   public override open(p, mode) return p end
   public override close(p) end
                               
   public override read(p, count, pos)
      local data
      data := get_data(p)
      if pos > *data then
         # EOF
         return
      else
         return data[pos:min(pos + count, *data + 1)]
   end

   public override get_info(p)
      return Info(get_perm(p), time, time, get_length(p), string(p.name), user, user, user)
   end

   public override get_qid(p)
      return Qid(ishift(get_perm(p), -24), 0, serial(p))
   end

   private get_length(p)
      return if is_dir(p) then 0 else *get_data(p)
   end

   private get_data(p)
      return string(p.get_trimmed_string_content())
   end

   private is_dir(p)
      return gen_children(p)
   end

   private get_perm(p)
      return if is_dir(p) then ior(8r555, Mode9.DMDIR) else 8r444
   end
end

procedure parse_file(fn)
   local s, p
   s := Files.file_to_string(fn) | return error("Couldn't open " || fn || ": " || &why)
   s := ucs(s)  | return error("Not UTF-8 format: " || fn)
   p := XmlParser()
   return p.parse(s).get_root_element() | error("Couldn't parse " || fn || ": " || &why)
end

procedure main(a)
   local root
   opts := options(a, get_optl(),
                   "Usage: xmlfs [OPTIONS] [XML FILE]")
   if *a > 0 then
      root := parse_file(a[1]) | stop(&why)
   # If no -s, then we must have an xml file parameter
   if /opts["s"] & /root then
      help_stop("Use the -s option or provide an xml file parameter.")
   server_main(, Session9P(XMLData9P(root)))
end

The main class, XMLData9P, implements the abstract methods in the Data9P class. This provides all the logic needed for a particular filesystem. As mentioned above, an instance of this class is then passed to Session9P, which implements the 9P server, using the methods in the Data9P instance.

In fact, since this is a read-only file system, it subclasses ReadOnlyData9P rather than Data9P. This saves implementing some methods, which will never be called because of the file permissions of the entries in the file system.

Most of the methods in Data9P act on “paths”, which are just arbitrary objects used to keep track of files and directories in the filesystem’s tree. We start with the root path of the tree, which is returned by the get_root method. This method is called when a client mounts the file system and the parameter aname is the attach parameter, which originates in the mount system call. An empty string indicates that the attach parameter is omitted.

The new method for XMLData9P takes an optional parameter, root. If this is provided, then this same value is always returned by the get_root method. In other words, each mount of the file system always provides the same tree, and no attach parameter is allowed. Alternatively, if the root is omitted, then an attach parameter is obligatory in order to specify the particular XML file. In this case, each mount will result in a different file tree.

The “path” returned by get_root() will then be passed back to the other methods in XMLData9P. For example, the get_info method may be called to retrieve permissions and other metadata about the root directory. Also, gen_children() may be called to generate the child “paths”, representing the root’s subdirectories. These will be more objects from the parsed XML tree.

Note how error conditions in XMLData9P are handled using exception.throw. This makes error handling in the library classes easier, and allows methods generating result sequences to signal errors (where failure would be a permissible outcome).

The main procedure uses the package ipl.server9p, which provides procedures useful to all 9P server programs. get_optl() just returns a list of common options, which can be seen by running xmlfs -help, as follows :-

% ./xmlfs -help
Usage: xmlfs [OPTIONS] [XML FILE]
-v                  Verbose mode
-m PATH             Use PATH as the mount point, rather than the default, 
                    /n/xml
-s FILE             Don't mount, instead post the pipe in /srv/FILE
-a                  Use the MAFTER flag when mounting
-b                  Use the MBEFORE flag when mounting
-c                  Use the MCREATE flag when mounting
-C                  Use the MCACHE flag when mounting

server_main is used to set up the server environment. It creates a pipe, and forks a background process, which will actually act as the server. This process calls the io() method of the Session9P instance, passing in one end of the pipe. As mentioned in the introduction above, this method loops, reading 9P messages, processing them using its instance of Data9P to create responses, which it then writes to the pipe. Eventually, reading from the pipe will fail, meaning the server is no longer referenced, and the loop exits, as does the background server process.

Meanwhile, the foreground process does one of two things with the other end of the pipe. If the -s option was given, then it posts the pipe into the /srv directory, using the name given. Otherwise, it mounts the pipe at the default location, /n/xml (or elsewhere if the -m option was given). The first parameter to server_main provides an optional attach parameter for the mount; in this case it is unnecessary. The foreground process then exits, returning the user to the shell prompt, whilst the background server continues running.

Some example runs should make all this much clearer. Imagine we have the following XML file, in /usr/rparlett/test.xml :-

Download test.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE files>
<files>
  <dir1>
    <one>
       Here are the contents of file one.
    </one>
    <two>
       Here are the contents of the second file.
    </two>
  </dir1>
  <dir2>
    <afile>
      This is another file,
      with two lines of text.
    </afile>
  </dir2>
</files>

Then if we run xmlfs with the -s option, we might have the following interaction :-

% ./xmlfs -s x
xmlfs: serving on /srv/x
% mount /srv/x /n/x /usr/rparlett/test.xml
% du -a /n/x
1   /n/x/dir1/one
1   /n/x/dir1/two
2   /n/x/dir1
1   /n/x/dir2/afile
1   /n/x/dir2
3   /n/x
% cat /n/x/dir1/one
Here are the contents of file one.% 
% unmount /n/x
% rm /srv/x
% xmlfs: exit
%

The first command starts the server in the background and returns the user to the shell prompt. The server is now listening on the pipe in /srv/x. In the next command, we mount an XML file on /n/x. The third parameter, the XML filename, is passed to the get_root method of the Data9P instance, and so this gives us the root XML element for this particular file tree.

Next we do some file operations, and then unmount the directory /n/x. The next line removes the pipe in /srv/x. This removes the last reference to the pipe, and causes it to close, so the server exits, printing a message to the console.

The important thing to note about this mode of operation is that another process could connect to the same server, and mount a different XML file somewhere in its own namespace, quite independently from the first process.

A second example run shows how things happen without the -s option. In this case, the other end of the pipe is used to immediately perform a mount. In this case we have to provide the single XML file which the program will serve. We could also optionally provide a different mountpoint with -m, instead of using the default.

% ./xmlfs /usr/rparlett/test.xml
xmlfs: mounted on /n/xml
% du -a /n/xml
1   /n/xml/dir1/one
1   /n/xml/dir1/two
2   /n/xml/dir1
1   /n/xml/dir2/afile
1   /n/xml/dir2
3   /n/xml
% cat /n/xml/dir2/afile
This is another file,
      with two lines of text.% 
% unmount /n/xml
% xmlfs: exit
%

Note how unmounting the directory /n/xml causes the server to exit, since this mount was the last reference to the server’s pipe. In contrast to the first example, the server in this case cannot be used by another process. It will only ever serve one XML tree.

The final example is rather like the first one, but in this case we specify the XML file at startup time. This means that a single tree is served, but can be mounted by many different processes. Note how the mount command has only two parameters, unlike the first example in which a third parameter was required. Recall that this parameter is passed into the get_root() method as the aname parameter, and has the value of the empty string if omitted in the mount command.

% xmlfs -s x /usr/rparlett/test.xml
xmlfs: serving on /srv/x
% mount /srv/x /n/x
% du -a /n/x
1   /n/x/dir1/one
1   /n/x/dir1/two
2   /n/x/dir1
1   /n/x/dir2/afile
1   /n/x/dir2
3   /n/x
% unmount /n/x
% rm /srv/x
% xmlfs: exit

Directory and file objects

Our next example introduces another set of classes which make writing file systems much easier. The most important is called File9P, and this represents a directory or a regular file. Subclasses of File9P are created in a tree structure representing a directory tree, which can then be connected with a Session9P using a subclass of Data9P, named TreeData9P.

To illustrate this, here is a simple replacement for the ramfs command :-

Download myramfs.icn

import plan9, ipl.server9p

procedure main(a)
   local root, sess
   root := Root9P()
   sess := Session9P(TreeData9P(root))
   push(a, "-c")
   server_main_0(a, sess)   
end

As you can see, it is very small, because all the work is done by the File9P classes. First, a root directory is created. This is an instance of Root9P, which is a subclass of File9P.

The root directory is then wrapped in a TreeData9P, which in turn is used to create a Session9P. The -c option is added to the arguments list. This adds the MCREATE flag to the mount options. Without this flag, creation in the directory tree would be forbidden, making for a rather dull filesystem!

server_main_0 is just a small helper procedure which sets up the command line options and then calls server_main, described previously.

Here is an example of running myramfs :-

% myramfs -m /n/ram
myramfs: mounted on /n/ram
% cpr /usr/rparlett/objecticon/config /n/ram
% du -a /n/ram
0   /n/ram/files/test
2   /n/ram/files/Makedefs
1   /n/ram/files/lib/gui/mkfile
3   /n/ram/files/lib/gui
... lots of output
1   /n/ram/files/examples/Makefile-win32
1   /n/ram/files/examples/Makefile
3   /n/ram/files/examples
96  /n/ram/files
96  /n/ram
% echo hello >/n/ram/hello
% ls -l /n/ram/hello
--rw-rw-rw- M 65 rparlett rparlett 6 Jul 24 19:44 /n/ram/hello
% rm /n/ram/hello
% unmount /n/ram
% myramfs: exit
%

cpr is a little shell utility I use to do a recursive copy. As you can see, this fills up the ram disk with lots of data. Then I create and delete a file, and unmount the filesystem, causing the server to exit.

myramfs can also be run with the -s option, and in this case the directory tree can be mounted by several processes, and the contents shared :-

% myramfs -s ram
myramfs: serving on /srv/ram
% mount -c /srv/ram /n/ram
% echo hello >/n/ram/msg
% unmount /n/ram

# In another process...
% mount /srv/ram /n/ram
% cat /n/ram/msg
hello
% unmount /n/ram

# In either process...
% rm /srv/ram
% myramfs: exit

Note that the first mount command requires the -c option to allow creation. Also, note how it only takes two parameters (the /srv pipe and the mountpoint). This reflects the fact that myramfs serves up the same tree on every mount (and hence ignores the parameter to get_root()). If we wanted to serve a different tree for each mount, then it would be necessary to subclass FileData9P (the parent of TreeData9P) and implement get_root().

Concurrent requests

One thing these first two file servers have in common is that they immediately respond to each incoming request. More sophisticated servers may wish to respond to one request whilst another is still outstanding. This type of processing can be achieved by using Session9P alongside an instance of io.Scheduler, as demonstrated in the next example, which is called tickfs.

The idea of tickfs is that it serves up a directory of ten files. Reading one of these files produces a series of “*” characters at different rates (from 0.1 to 1 second per character). To achieve this, a delay is introduced into the read.

Download tickfs.icn

import plan9, io, ipl.server9p

global sched

class Ticker(Regular9P)
   private t

   public override read(count, pos)
      sched.curr_task.sleep(t * 100)
      return "*"
   end

   public override new(t)
      Regular9P.new(t)
      self.t := t
      set_fixed_perm(8r444)
      return
   end
end

procedure main(a)
   local root, sess
   sched := Scheduler(100)
   root := Root9P().set_fixed_perm(8r555)
   every root.add_child(Ticker(1 to 10))
   sess := Session9P(TreeData9P(root))
   server_main_0(a, sess, sched)
end

The first thing tickfs does is create a io.Scheduler to use. This is a class which can schedule multiple co-routines, encapsulated in io.Task objects. This scheduler is then passed to the utility procedure server_main_0. This causes it to use the session’s async_io() method to create a Task to read incoming messages. This Task will itself create other Tasks to process these messages. The scheduler is then repeatedly invoked until it is empty of Tasks, at which point the server will exit.

The read method of the File9P subclass (Ticker) uses the current Task to sleep for an appropriate time.

The sleep in the read method won’t cause the interpreter to sleep (like the builtin delay() function); rather it will transfer control back to the Scheduler, and perhaps do other work in other Tasks. At some later point, when the sleep delay has passed, the Task is resumed, and the read() method completes, and a 9P response is sent to the client.

Here is an example interaction with tickfs. The asterisks appear about 3 per second, and the sequence is stopped by the user pressing the delete key.

% tickfs
tickfs: mounted on /n/tick
% ls -ld /n/tick
d-r--r--r-- M 78 rparlett rparlett 0 Jul 25 14:54 /n/tick
% ls -l /n/tick/1
--r--r--r-- M 78 rparlett rparlett 0 Jul 25 14:54 /n/tick/1
% lc /n/tick
1   10  2   3   4   5   6   7   8   9
% cat /n/tick/3
**************%            # One * per 1/3 second until the user interrupts
% unmount /n/tick
% tickfs: exit

Broadcasting messages

The tickfs program above was obviously not useful, but the next example is rather more practical. It is called bcastfs and provides two files, send and recv. The idea is that readers open the recv file and wait for messages (their reads will block). Others open the send file and write messages, and these are sent to all the waiting readers.

Download bcastfs.icn

import plan9, io, util, ipl.server9p, exception

global receivers, sched

class Recv2(QueueFile9P)
   private sleeping

   public sent(s)
      add(s)
      (\sleeping).notify()
   end

   public override read(count, pos)
      while len = 0 do {
         use {
            sleeping := sched.curr_task,
            sleeping.sleep(),
            sleeping := &null
         } | throw(&why)
      }
      return QueueFile9P.read(count, pos)
   end

   public override close()
      delete(receivers, self)
      (\sleeping).interrupt()
   end

   public override new(n)
      QueueFile9P.new(n)
      insert(receivers, self)
      return
   end
end

class Recv(Regular9P)
   public override open(mode)
      return Recv2("recv2").
         set_fixed_perm(perm).
         set_parent(parent)
   end
end

class Send2(StringFile9P)
   public override close()
      if *data > 0 then
         every (!receivers).sent(data)
   end
end

class Send(Regular9P)
   public override open(mode)
      return Send2("send2").
         set_fixed_perm(perm).
         set_parent(parent)
   end
end

procedure main(a)
   local root, sess
   sched := Scheduler(100)
   receivers := set()
   root := Root9P().
      set_fixed_perm(8r555).
      add_child(Send("send").set_fixed_perm(8r222)).
      add_child(Recv("recv").set_fixed_perm(8r444))
   sess := Session9P(TreeData9P(root))
   server_main_0(a, sess, sched)
end

Consider a receiver first. A client opens the recv file for reading with the open system call. On the server the open is handled by the open method of the Recv class. This method does something rather interesting; it creates and returns a different file, an instance of Recv2. This means that the file the client reads from is actually serviced by a different object on the server to the one that served the open call. In other words, the read method of the Recv instance is never called (in fact, it is an undefined optional method). The client doesn’t care about this; as far as it is concerned it is reading from the recv file, the same file as all other readers. In fact, they are each reading from their own unique file.

Recv2 subclasses QueueFile9P which is a File9P which is a regular file and keeps its file data in a list of strings. Writes to the file add to one end of the list, whilst reads remove data from the other end. The add() method also adds to the writing end of the list.

The new method of Recv2 adds itself to a global list of Recv2 instances, there being one for each reader.

The read method of Recv2 may cause the reader Task to go to sleep if there is no data to presently read (indicated by the file size, len, being 0). Before it goes to sleep, it stores the current Task in the variable sleeping, so that other methods may wake it up (or interrupt it) when appropriate. The util.use procedure is used to ensure sleeping is cleared when the Task awakes.

The sent method is invoked by a client sending a message. It notifies the sleeping reader. sleep (and hence use) succeed, and the read calls the parent class’s read method, which removes data from the list of strings and returns it to the client.

If the reader client aborts the read (in the 9P terminology, the request is “flushed”), or closes the file, then the sleeping Task is interrupted. This causes sleep (and use) to fail, causing the read method to throw an exception, which in turn returns an error to the client.

The sending file also uses the idiom that returns a new file on an open. In this case a Send2 instance is returned into which the client writes its message. It subclasses StringFile9P which is a File9P which holds its file content as a string (in the instance variable data). When this file is closed, the close() method is invoked and the contents of the file form a new message. This is then broadcast to all the receivers currently listening, by invoking each one’s send method.

Note that Send could have been implemented in a slightly simpler way, as follows :-

class Send(Regular9P)
   public write(s, pos)
      if *s > 0 then 
         every (!receivers).sent(s)
      return *s
   end
end

With this implementation, each write will send a single message, as opposed to the sequence of open, one or more writes and a close. The length of a send would be limited to the maximum length allowed in a single 9P write message.

Here is some example output from bcastfs. One window sets up the server, another receives messages, and a third window sends two messages, the second spread over three lines.

# In window 1
% bcastfs -s bc
bcastfs: serving on /srv/bc
# ... we now do something in windows 2 and 3 ...
% rm /srv/bc
% bcastfs: exit

# In window 2
% mount /srv/bc /n/bc
% ls -l /n/bc
--r--r--r-- M 55 rparlett rparlett 0 Aug 11 11:00 /n/bc/recv
---w--w--w- M 55 rparlett rparlett 0 Aug 11 11:00 /n/bc/send
% cat /n/bc/recv
hello
a
  b
    c
% unmount /n/bc

# In window 3
% mount /srv/bc /n/bc
% echo hello >/n/bc/send
% cat >/n/bc/send
a
  b
    c
^D% 
% unmount /n/bc

Reference counting

One facility which Server9P provides is the ability to keep track of the number of references to a particular path, and to notify the Data9P when a path’s reference count reaches zero, meaning that no further calls to Data9P will be made using that particular path as a parameter.

As an example of how this could be useful, consider re-inventing the filesystem provided by mntgen, which is normally mounted on /n, to provide temporary mount directories. This could be implemented as follows :-

Download unreffs.icn

import plan9, ipl.server9p, io

class MyTreeData9P(TreeData9P)
   private sess

   public set_session(sess)
      self.sess := sess
      link
   end

   public override path_unreferenced(p)
      local q
      while sess.is_path_unreferenced(p) &
         p.is_empty() &
         q := p.get_parent() do 
      {
         if \opts["v"] then io.write("REMOVE: ", p.name, " ", image(p))
         q.remove_child(p)
         p := q
      }
   end

   public override get_child(p, x)
      return p.get_child(x) | {
         if \opts["v"] then io.write("CREATE: ", x)
         p.create_child(x, p.uid, p.perm)
   }
   end
end

procedure main(a)
   local root, sess, data
   root := Root9P()
   sess := Session9P(data := MyTreeData9P(root)).set_track_refs(&yes)
   data.set_session(sess)
   server_main_0(a, sess)   
end

Note that the Data9P instance in this case is a subclass of TreeData9P, which overrides two methods. Firstly, get_child() is modified so that reference to an unknown directory name automatically creates a new directory. Secondly, path_unreferenced() is overridden so that directories which are no longer referenced are removed, when they are empty.

Here is an interaction with unreffs, in verbose mode, in which a directory a/b is created and then removed automatically.

% unreffs -v
GOT:record plan9.TVersion#1(tag=65535;msize=8216;version="9P2000")
SEND:record plan9.RVersion#1(tag=65535;msize=8216;version="9P2000")
GOT:record plan9.TAttach#1(tag=2;fid=261;afid=4294967295;uname="rparlett";aname="")
INC:object plan9.Root9P#1(12) to 1
INC:object plan9.Root9P#1(12) to 2
DEC:object plan9.Root9P#1(12) to 1
SEND:record plan9.RAttach#1(tag=2;qid="\x80\x00\x00\x00\x00\x01\x00\x00\x00\x00\x00\x00\x00")
unreffs: mounted on /n/unref
% mkdir -p /n/unref/a/b
GOT:record plan9.TWalk#1(tag=2;fid=261;newfid=239;wname=list#51[])
INC:object plan9.Root9P#1(12) to 2
INC:object plan9.Root9P#1(12) to 3
DEC:object plan9.Root9P#1(12) to 2
SEND:record plan9.RWalk#1(tag=2;wqid=list#62[])
GOT:record plan9.TStat#1(tag=2;fid=239)
INC:object plan9.Root9P#1(12) to 3
DEC:object plan9.Root9P#1(12) to 2
SEND:record plan9.RStat#1(tag=2;stat="H\x00\x00\x00\x00\x00\x00\x00 ... \b\x00rparlett")
GOT:record plan9.TClunk#1(tag=2;fid=239)
INC:object plan9.Root9P#1(12) to 3
DEC:object plan9.Root9P#1(12) to 2
DEC:object plan9.Root9P#1(12) to 1
SEND:record plan9.RClunk#1(tag=2)
GOT:record plan9.TWalk#2(tag=2;fid=261;newfid=239;wname=list#97["a"])
INC:object plan9.Root9P#1(12) to 2
CREATE:a
INC:object plan9.TableDir9P#1(12) to 1
INC:object plan9.TableDir9P#1(12) to 2
DEC:object plan9.Root9P#1(12) to 1
DEC:object plan9.TableDir9P#1(12) to 1
SEND:record plan9.RWalk#2(tag=2;wqid=list#108["\x80\x00\x00\x00\x00\x02\x00\x00\x00\x00\x00\x00\x00"])
GOT:record plan9.TStat#2(tag=2;fid=239)
INC:object plan9.TableDir9P#1(12) to 2
DEC:object plan9.TableDir9P#1(12) to 1
SEND:record plan9.RStat#2(tag=2;stat="H\x00\x00\x00\x00\x00\x00\x00 ... \b\x00rparlett")
GOT:record plan9.TClunk#2(tag=2;fid=239)
INC:object plan9.TableDir9P#1(12) to 2
DEC:object plan9.TableDir9P#1(12) to 1
DEC:object plan9.TableDir9P#1(12) to 0
REMOVE:a object plan9.TableDir9P#1(12)
SEND:record plan9.RClunk#2(tag=2)
GOT:record plan9.TWalk#3(tag=2;fid=261;newfid=239;wname=list#148["a","b"])
INC:object plan9.Root9P#1(12) to 2
CREATE:a
INC:object plan9.TableDir9P#2(12) to 1
CREATE:b
INC:object plan9.TableDir9P#3(12) to 1
INC:object plan9.TableDir9P#3(12) to 2
DEC:object plan9.Root9P#1(12) to 1
DEC:object plan9.TableDir9P#2(12) to 0
DEC:object plan9.TableDir9P#3(12) to 1
SEND:record plan9.RWalk#3(tag=2;wqid=list#159["\x80\x00 ... \x00\x00","\x80\x00 ... \x00\x00"])
GOT:record plan9.TStat#3(tag=2;fid=239)
INC:object plan9.TableDir9P#3(12) to 2
DEC:object plan9.TableDir9P#3(12) to 1
SEND:record plan9.RStat#3(tag=2;stat="H\x00\x00\x00\x00\x00\x00\x00 ... \b\x00rparlett")
GOT:record plan9.TClunk#3(tag=2;fid=239)
INC:object plan9.TableDir9P#3(12) to 2
DEC:object plan9.TableDir9P#3(12) to 1
DEC:object plan9.TableDir9P#3(12) to 0
REMOVE:b object plan9.TableDir9P#3(12)
REMOVE:a object plan9.TableDir9P#2(12)
SEND:record plan9.RClunk#3(tag=2)
% unmount /n/unref
GOT:record plan9.TClunk#4(tag=2;fid=261)
INC:object plan9.Root9P#1(12) to 2
DEC:object plan9.Root9P#1(12) to 1
DEC:object plan9.Root9P#1(12) to 0
SEND:record plan9.RClunk#4(tag=2)
% Fids(0)
Open counts(0)
Ref counts(0)
/usr/rparlett/schmobj/misc
  0 r  M   38 (0000000000000001 0 00)  8192      860 /dev/cons
  1 w  M   38 (0000000000000001 0 00)  8192    65965 /dev/cons
  2 w  M   38 (0000000000000001 0 00)  8192    65965 /dev/cons
  3 r  c    0 (0000000000000004 0 00)     0       72 /dev/cputime
unreffs: exit

An interactive console file

The next example shows how a file can be used to provide an interactive console. This could perhaps provide a debugging interface into a program.

Download confs.icn

import plan9, io, util, ipl.server9p, exception

global sched

class Serv2(QueueFile9P)
   private reading, done_flag

   public override write(s, pos)
      local t
      case t := trim(s, ' \n\r\t') of {
         "one": sendln("first command output")
         "two": sendln("second command output")
         "date": sendln(&dateline)
         "q" : done()
         default: sendln("odd command:" ||  image(t))
      }
      send("1> ")
      return *s
   end

   public sendln(s)
      send(s || "\n")
   end

   public send(s)
      add(s)
      (\reading).notify()
   end

   public done()
      (\reading).interrupt()
      done_flag := &yes
   end

   public override read(count, pos)
      /reading | throw("Already reading")
      /done_flag | throw("Session finsihed")
      while len = 0 do {
         use {
            reading := sched.curr_task,
            reading.sleep(),
            reading := &null
         } | throw(&why)
      }
      return QueueFile9P.read(count, pos)
   end

   public override close()
      done()
   end

   public override new(n)
      QueueFile9P.new(n)
      sendln("Enter q to exit")
      send("1> ")
      return
   end
end

class Serv(Regular9P)
   public override open(mode)
      return Serv2("serv2").
         set_fixed_perm(perm).
         set_parent(parent)
   end
end

procedure main(a)
   local root, sess
   sched := Scheduler(100)
   root := Root9P().set_fixed_perm(8r555)
   root.add_child(Serv("serv").set_fixed_perm(8r666))
   sess := Session9P(TreeData9P(root))
   server_main_0(a, sess, sched)
end

The root directory in this case contains a single read-write file, serv. We again use the idiom that opening this file actually returns a distinct file, in this case an instance of Serv2.

The idea is that a client connects to the serv file using the con program, and then has an interactive session. For example :-

% confs
confs: mounted on /n/con
% con -C /n/con/serv
Enter q to exit
1> one
first command output
1> two
second command output
1> date
Saturday, August 13, 2016  9:25 pm
1> q
% unmount /n/con
% confs: exit
confs: exit

Note that the -C option must be given to con, so that it takes care of line editing, and passes each edited line in a single write.

This server makes use of QueueFile9P, as did bcastfs above. This means that the send and sendln methods append to the file, whilst reads consume from the beginning, sleeping if the file is currently empty.

One interesting point is how the “q” command interrupts the reader, causing it to return an error response to the client. This causes con to exit. If we didn’t provide this, then the user of con would have to press the control- combination to access con’s escape prompt, and then enter “q”. con would then exit, and the outstanding read would be flushed and the sleep interrupted. If you are interested in seeing how this takes place, try running confs with the -v option to see the messages and their generated responses.

Serving a program as a file

This example uses a program to service a single file. Each time the file is opened, the program is run, and its input and output provide the input and output for operations on the file. The program can be an interactive one if desired. It uses a io.Scheduler, together with the non-blocking I/O features described on this page, and thus can serve multiple clients and requests simultaneously.

Download progfs.icn

import plan9, io, ipl.server9p, ipl.options, exception, posix, util

global sched, prog

class Prog2(Regular9P)
   private reading, writing, cin, cout, pid

   public override write(s, pos)
      local f
      /writing | throw("Already writing")
      use {
         writing := sched.curr_task,
         use {
            f := TaskStream(cout, writing).set_close_underlying(&no),
            f.writes1(s)
         },
         writing := &null
      } | throw(&why)
      return *s
   end

   public override read(count, pos)
      local f, s
      /reading | throw("Already reading")
      return use {
         reading := sched.curr_task,
         use {
            f := TaskStream(cin, reading).set_close_underlying(&no),
            # For some reason, con won't exit on eof, but only on a
            # read error, so we send an error on eof, if the "ee"
            # option is set.
            if s := f.in(count) then {
               if /s & \opts["ee"] then
                  error("End of file")
               else
                  s
            }
         },
         reading := &null
      } | throw(&why)
   end

   public override close()
      # Note that any read or write messages should have been flushed,
      # and their Tasks interrupted, so it should be safe to close
      # these files.
      cin.close()
      cout.close()
      System.wait(pid)
   end

   private setup()
      local l
      l := FileStream.pipe() | fail
      (l |||:= FileStream.pipe(, FileOpt9.OCEXEC) &
       pid := System.fork()) | {
          save_why{ lclose(l) }
          fail
       }
      if pid > 0 then {
         # Parent
         cin := NonBlockStream(l[1], Stream.READ, 65536)
         cout := NonBlockStream(l[4], Stream.WRITE,, 65536).
            set_write_on_close(NonBlockStream.BACKGROUND_FLUSH)
         l[2].close()
         l[3].close()
         return
      } else {
         # Child
         l[2].dup2(FileStream.stdout) | stop(&why)
         l[3].dup2(FileStream.stdin) | stop(&why)
         l[2].dup2(FileStream.stderr) | stop(&why)
         lclose(l)
         if /prog then
            System.exec("/bin/rc", ["-i"]) | stop(&why)
         else
            System.exec("/bin/rc", ["-c", prog]) | stop(&why)
         syserr("Not reached")
      }
   end

   public override new(n)
      Regular9P.new(n)
      setup() | throw(&why)
      return
   end
end

class Prog(Regular9P)
   public override open(mode)
      return Prog2("prog2").
         set_fixed_perm(perm).
         set_parent(parent)
   end
end

procedure main(a)
   local root, name, l, ff
   l := get_optl() ||| 
      [Opt("n", string, "The name of the single file to serve under the mountpoint; default \"prog\""),
       Opt("p",, "Use RFNAMEG when forking the child process"),
       Opt("ee",, "Send a read error on EOF; this makes the con client program exit on EOF")]
   opts := options(a, l,
                   "Usage: progfs [OPTIONS] [PROG]")
   sched := Scheduler(100)
   # May be null, meaning run an interactive shell.
   prog := a[1]
   name := \opts["n"] | "prog"
   if \opts["p"] then
      ff := ForkOpt.RFNAMEG
   root := Root9P().
      set_fixed_perm(8r555).
      add_child(Prog(name).set_fixed_perm(8r666))
   server_main(, Session9P(TreeData9P(root)), sched, ff)
end

Here are some examples of using this filesystem. The first example uses the date command to provide a file which produces the date when read.

% progfs date
progfs: mounted on /n/prog
% ls -l /n/prog
--rw-rw-rw- M 64 rparlett rparlett 0 Nov 12 14:49 /n/prog/prog
% cat /n/prog/prog
Sat Nov 12 14:49:35 GMT 2016
% unmount /n/prog
progfs: exit

The next example is interactive. The tr program is used to capitalize each line read as input. Note that it is necessary to use the control-backslash escape to exit con, using its quit command.

% progfs 'tr [a-z] [A-Z]'
progfs: mounted on /n/prog
% con -C /n/prog/prog
one
ONE
two
TWO
three
THREE
^\
>>> q
% unmount /n/prog
progfs: exit

The final example shows the rc shell program (the default) being used to service the file. The -ee flag is used to help the con program to exit conveniently; it sends an error to the reader on end of file.

% progfs -ee
progfs: mounted on /n/prog
% echo $pid
83
% con -C /n/prog/prog
% echo in sub-shell
in sub-shell
% echo $pid
234
% exit
# Now we're back in our own shell
% echo $pid
83
% unmount /n/prog
progfs: exit

Some programs aren’t suitable to be used with this filesystem. For example, it would be tempting to set up a filesystem using gzip, so that writing to a file, and then reading from it, would produce compressed data. This wouldn’t work however, because gzip relies on the end-of-file on its input to signal the end of data. Only then will it output the final section of compressed data. But since we are communicating via a single file, this is not possible (closing the output closes the input too, before we can read it).

The clone file technique

This small example illustrates a technique which provides a separate directory of files for each client. This technique is found in several filesystems in Plan 9, notably the draw device and the net device. A client begins by opening a file (usually called “new” or “clone”). Reading this file gives the client the name of a new directory containing files for its use. The directory is removed when the clone file is closed.

In this small example the client directory contains a single file with simple string data.

Download clonefs.icn

import plan9, ipl.server9p

class CtrlFile(StringFile9P)
   public override close()
      parent.parent.remove_child(parent)
   end
end

class CloneFile(StringFile9P)
   private clone_count

   public override open(mode)
      local ctrl, data
      clone_count +:= 1
      ctrl := CtrlFile("ctrl").
         set_data(clone_count)
      data := StringFile9P("data").
         set_data("Here is some data")
      parent.add_child(TableDir9P(clone_count).
            add_child(ctrl).
            add_child(data))
      return ctrl
   end

   public override new(n)
      StringFile9P.new(n)
      clone_count := 0
      return
   end
end

procedure main(a)
   local root, sess
   root := Root9P().add_child(CloneFile("clone"))
   sess := Session9P(TreeData9P(root))
   server_main_0(a, sess)   
end

The following shell script can be used to test the filesystem.

#!/bin/rc
{
   id=`{cat <[0=4]}
   echo Id is $id
   echo Data is `{cat /n/clone/$id/data}
} <>[4]/n/clone/clone

The following output results :-

% clonefs
clonefs: mounted on /n/clone
% clonetest.sh 
Id is 1
Data is Here is some data
% clonetest.sh 
Id is 2
Data is Here is some data
% unmount /n/clone
% clonefs: exit

Serving a filesystem from a GUI

The examples so far have all been dedicated file servers. It is possible however for a program to act as a file server incidental to its main activity. The next example is a gui program which simply presents two text areas, and provides a file server with two files. Writing to one file writes to the top pane, whilst reading from the other returns the contents of the bottom pane.

Download guifs.icn

import
   gui, ipl.options, plan9, ipl.server9p

global dialog

class GuiFSDialog(Dialog)
   private readable
      output,
      received

   ...
end

class SendEdit(StringFile9P)
   public override close()
      if *data > 0 then
         dialog.received.log_str(ucs(data), 100)
   end
end

class Send(Regular9P)
   public override open(mode)
      return SendEdit("send-edit").
         set_fixed_perm(perm).
         set_parent(parent)
   end
end

class Get(Regular9P)
   public override open(mode)
      return StringFile9P("get-copy").
         set_fixed_perm(perm).
         set_data(dialog.output.get_contents_str()).
         set_parent(parent)
   end
end

procedure main(a)
   local root, sess
   opts := options(a, get_optl())
   root := Root9P().
      set_fixed_perm(8r444).
      add_child(Send("send").set_fixed_perm(8r222)).
      add_child(Get("get").set_fixed_perm(8r444))
   sess := Session9P().set_data(TreeData9P(root))
   gui_main(, sess, &no, create {{
      dialog := GuiFSDialog()
      dialog.show_modal()
   }})
end

gui_main is a library procedure in the server9p package, which is similar to the server_main procedure described previously. It also creates a pipe and forks a child process, which runs the body of the program, passed as a co-expression. The child process also creates a Task for the file server (using one of the given Session9P’s methods), and this is run by the gui library’s own io.Scheduler, contained in the gui.Dispatcher class. The file server task therefore runs seamlessly alongside the gui library’s other background tasks, such as blinking cursors, tooltips and so on.

Meanwhile, the parent process either mounts the other end of the pipe, or posts it in /srv, according to the command line options. Unlike server_main however, it waits for the child process to exit, and then unmounts the mount or removes the /srv file.

A dynamically changing directory

Some of the above examples show how to create a dynamic file whose contents are determined at the point of each call to read(). It is quite easy to do something similar for directories. This provides an alternative to the rather static system provided by TableDir9P class, which stores a directory’s children in a table.

By way of an example, consider a program which wishes to provide a simple “phone book” database as a filesystem. It doesn’t wish to read the entire database into memory as a static structure for obvious reasons (and the database contents may change too). Therefore it consults the database for each filesystem request as needed.

At the top level of the filesystem there are 26 static child directories, one for each letter of the alphabet. The actual database entries then appear under these directories.

The following implementation holds the notional phone book database in a simple static table.

Download phonefs.icn

import plan9, ipl.server9p

global db

#
# Example of a dynamic read-only Dir9P.
#

class Letter(Dir9P)
   public override get_child(x)
      local s
      if s := member(db, x) then
         return StringFile9P(x).
            set_data(s || "\n").
            set_fixed_perm(8r444).
            set_parent(self)
   end

   public override gen_children()
      local t
      every t := key(db) do
         if match(name, t) then
            suspend get_child(t)
   end
end

procedure main(a)
   local root, sess

   db := table(,
               "Kelly, Grace", "+70 6267 849422",
               "Mansfield, Jayne", "+28 9966 778016",
               "Monroe, Marilyn", "+19 3435 226024",
               "Novak, Kim", "+64 8137 206255",
               "Dandridge, Dorothy", "+17 3086 530043",
               "Dee, Ruby", "+35 4193 021480",
               "Wood, Natalie", "+18 4724 979516",
               "Dean, James", "+81 1091 778385",
               "Wayne, John", "+26 6630 226929",
               "Hudson, Rock", "+23 1293 642244",
               "Curtis, Tony", "+21 5814 721165",
               "Presley, Elvis", "+78 0598 343952",
               "Gable, Clark", "+40 2972 740853",
               "Grant, Cary", "+30 9933 838496",
               "Bergman, Ingrid", "+89 2910 389517",
               "Tierney, Gene", "+22 8319 234505",
               "Fontaine, Joan", "+92 1726 078001",
               "Wright, Teresa", "+85 5380 981622",
               "Jones, Jennifer", "+36 4042 270838",
               "Stanwyck, Barbara", "+33 4207 427618",
               "Lemmon, Jack", "+93 6870 534136",
               "Newman, Paul", "+12 4983 137123",
               "Andrews, Julie", "+78 1223 184098"
               )
               
   root := Root9P().set_fixed_perm(8r555)
   every root.add_child(Letter(!&ucase).set_fixed_perm(8r555))

   sess := Session9P(TreeData9P(root))
   server_main_0(a, sess)   
end

The top-level directories are each a custom subclass of Dir9P, Letter. This class represents a read-only directory, and thus only needs to implement the two methods shown (a writable directory must implement the other optional methods in Dir9P). At each request the database is searched, and child files are generated. Note that each request generates distinct file objects, even though they represent the same database entry.

An example interaction is as follows :-

% ./phonefs
phonefs: Mounted on /n/phone
% ls /n/phone
A  B  C  D  E  F  G  H  I  J  K  L  M  N  O  P  Q  R  S  T  U  V  W  X  Y  Z
% ls /n/phone/D
'Dandridge, Dorothy'  'Dean, James'  'Dee, Ruby'
% cat '/n/phone/D/Dean, James'
+81 1091 778385
% unmount /n/phone
phonefs: Exit

Relaying a tree of files

The next example serves up its files from the most basic of sources - another tree of files. There is of course a program in Plan 9 to do this, called srvfs.

Download mysrvfs.icn

import plan9, ipl.server9p, io, util, exception

global sched

record File(fpath,       # A FilePath giving the source location
            depth,       # The depth in the source tree
            worker,      # A FileWorker for background i/o
            qpath        # An integer, the qid path
            )

class SrvData9P(ReadOnlyData9P)
   private const
      qpath_table,        # A table of string paths to unique qid path values;
                          #     this is a one-to-one mapping
      qpath_seq,          # For generating qid paths
      timeout             # Timeout for FileWorker i/o

   public new()
      qpath_table := table()
      qpath_seq := create seq()
      timeout := 100000
      return
   end

   # Return an integer qid path for the string path s
   private get_qpath(s)
      local i
      (i := member(qpath_table, s)) |
         insert(qpath_table, s, i := @qpath_seq)
      return i
   end

   private mk_file(fpath, depth, worker)
      return File(fpath, depth, worker, get_qpath(fpath.str()))
   end

   public override get_root(aname)
      Files.is_directory(aname) | throw("Not a directory: " || aname)
      return mk_file(FilePath(aname).canonical(), 0)
   end

   public override get_parent(p)
      if p.depth > 0 then
         return mk_file(p.fpath.parent(), p.depth - 1)
   end

   public override get_child(p, name)
      local t
      t := p.fpath.child(name)
      if Files.access(t.str()) then
         return mk_file(t, p.depth + 1)
   end

   private gen_children1(fp, s)
      local t
      repeat {
         t := s.read_line() | throwf("Failed to read dir: %w")
         if /t then
            fail
         if Files.is_relative_dir(t) then
            next
         suspend fp.child(t)
      }
   end

   public override gen_children(p)
      local s, ps
      ps := p.fpath.str()
      suspend use_seq {
         s := DirStream(ps) | throwf("Couldn't open dir '%s': %w", ps),
         mk_file(gen_children1(p.fpath, s), p.depth + 1)
      }
   end

   private poll_and_await(w)
      local x
      # Poll will fail if we were flushed (ie: interrupted).
      x := sched.curr_task.poll([w, Poll.IN], timeout) | fail
      return if /x then
         error("Timeout")
      else
         w.await()
   end

   public override open(p, mode)
      local w
      (mode = (FileOpt9.OREAD | FileOpt9.OEXEC)) | read_only()
      w := FileWorker(, 16 * 1024)
      if w.op_open(p.fpath.str(), mode) & poll_and_await(w) then
         return mk_file(p.fpath, p.depth, w)
      else {
         save_why{ w.close() }
         throw(&why)
      }
   end

   public override read(p, count, pos)
      local w, s
      w := \p.worker | throw("File not open")
      w.await() | throw(&why)
      # A zero length read can occur, but is not allowed by op_pread().
      if count = 0 then
         return
      if w.op_pread(count, pos) & s := w.get_buff(poll_and_await(w)) then
         # await() may return 0, indicating an empty string and eof
         return s
      else
         throw(&why)
   end

   public override close(p)
   end

   public override get_info(p)
      local st, s
      s := p.fpath.str()
      st := Files.stat(s) | throwf("Failed to stat '%s': %w", s)
      return Info(st.mode, st.atime, st.mtime, st.size, st.name, st.uid, st.gid, st.muid)
   end

   public override get_qid(p)
      local st, s
      s := p.fpath.str()
      st := Files.stat(s) | throwf("Failed to stat '%s': %w", s)
      return Qid(st.qtype, st.qvers, p.qpath)
   end

   public override path_unreferenced(p)
      (\p.worker).close()
   end
end

procedure main(a)
   local sess
   sched := Scheduler(100)
   sess := Session9P(SrvData9P()).set_track_refs(&yes)
   server_main_1(a, sess, sched)
end

For the sake of simplicity, this file system is a read-only one. Thus, like the XML example above, it subclasses ReadOnlyData9P. In this case however, we have no control over the permissions of the entries in the file system (they are just copied from the source), so some of these “write” methods may actually be invoked. They just throw an appropriate exception, which is returned as an error to the client.

The FileWorker class (explained on this page) is used to provide asynchronous background i/o to open and read files. This allows the server to service several requests concurrently, using a scheduler, as described above for the “tick” file system.

A record, File, is used to represent a file in the tree being served. Several Files might represent the same target file, for example if two clients had the same file open at the same time. Data9P’s reference counting is used to ensure that a FileWorker is closed when a File is no longer referenced by the server. This is preferable to closing the FileWorker in the close() method, because that depends on the Open message completing succesfully after the open() method returns. In this case there is a small, but conceivable, chance of that not happening. The Qid of the newly-opened file must be returned, and the get_qid() method can throw an exception if the stat() system call fails. If it did, then the Open request would fail and an error would be returned to the client, and no corresponding Close request would ensue.

One challenge with this server is calculating the Qid for a particular File. A Qid is made up of three integer parts, path (64 bits), version (32 bits) and type (8 bits). To see the Qid for a file, use “ls -q”. The Qid path must be unique for each file in the tree, and mustn’t change over time.

The Qid version and type can just be copied from the source file, but the path (a 64-bit integer) cannot be copied, since the source file tree might be comprised of files from several different servers, each with its own Qid path numbering scheme.

The Qid path is used for two things. Firstly, it is used by the client (normally the Plan 9 kernel) to cache file contents. To see this in action, try running the following commands :-

% echo "test file contents" >/tmp/testfile.txt
% ./mysrvfs /tmp -v -C

Now, in a second window sharing the namespace, run the following command twice :-

% cat /n/mysrv/testfile.txt

If you look carefully at the server output, you will see that the first cat caused the following “read” message :-

GOT: record plan9.TRead#1(tag=2;fid=332;offset=0;count=8192)
...
SEND: record plan9.RRead#1(tag=2;data="\"test file contents\"\n")

But the second (and any subsequent) cat commands do not have such a “read”, because that part of the file content is cached by the client (there are other messages of course, to check that the file hasn’t changed).

The second thing the Qid path is used for is to handle files which have the exclusive bit (set with “chmod +l”). A simple table is kept by the Session9P instance to count open Qid paths, and a request to open an exclusive file is rejected if its Qid path open count isn’t zero. To check that this works, create such a file and then serve it as part of the underlying file system for mysrvfs. For example :-

% ramfs -m /n/r
% echo hello >/n/r/hello
% chmod +l /n/r/hello
% ls -l /n/r
-lrw-rw-r-- M 916 rparlett rparlett 6 Dec 31 15:39 /n/r/hello
% ./mysrvfs /n/r
mysrvfs: Mounted on /n/mysrv

Now, in a window sharing the same namespace, run the command

% tail -f /n/mysrv/hello
hello

Now try to run the same command in a different window. The second command should give an error, because the file is already open :-

% tail -f /n/mysrv/hello
tail: /n/mysrv/hello: '/n/mysrv/hello' Exclusive use file already open

In order to ensure a correct Qid path value, mysrvfs simply keeps a table of the target file path names, mapped to unique integers drawn from a simple ascending sequence. The downside of this scheme is that this table might go quite large (and it never gets smaller). An alternative would be to hash the file path names, but of course the worry then would be that a collision may occur.

Performance

Unfortunately mysrvfs shows that there is a significant cost to using Session9P with a scheduler in an asynchronous manner. By way of illustration consider running wc on a file of 7MB, accessed via different file servers.

In its raw state, the file is accessed via u9fs, and wc takes 0.7s. Using a modified synchronous version of mysrvfs, wc takes 2.6s. The same program using a scheduler (and hence using asynchronous I/O for the 9P communication only), takes 19s. Finally, using mysrvfs with asynchronous reads from the source file too, takes 26.6s.

These overheads would be vastly reduced if the message size used by the Plan 9 kernel were larger. It currently uses a little over 8KB, and wc uses about 900 round trips to read the file. In an age of megabits and terabytes, the Plan 9 kernel is rather stuck in the past!

Contents