Contents

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(Data9P)
   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 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 get_parent(p)
      local q
      q := p.get_parent()
      if is(q, XmlElement) then return q
   end

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

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

   public open(p, mode) return p end
   public close(p) end
                               
   public remove_child(p1, p2) end
   public create_child(p, name, uid, perm) end
   public is_empty(p) end
   public modified(p, uid) end
   public write(p, s, pos) end
   public set_info(p, x) end

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

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

   public 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(8r444, 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 utf8 format: " || fn)
   p := XmlParser()
   return p.parse(s).get_root_element() | error("couldn't parse " || fn || ": " || &why)
end

procedure usage(opts)
   usage_impl(" [OPTIONS] [XML FILE]", opts)
end

procedure main(a)
   local root
   opts := options(a, get_optl(), usage)
   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
      usage()
   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.

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.

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, representing a directory.

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 read(pos, count)
      sched.curr_task.sleep(t * 100)
      return "*"
   end

   public 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(8r444)
   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 to 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 read(pos, count)
      while len = 0 do {
         use {
            sleeping := sched.curr_task,
            sleeping.sleep(),
            sleeping := &null
         } | throw(&why)
      }
      return QueueFile9P.read(pos, count)
   end

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

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

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

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

class Send(Regular9P)
   public 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(8r444).
      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 deferred 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 built-in filesystem '#s/slashn', 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
      return self
   end

   public 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 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 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 read(pos, count)
      /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(pos, count)
   end

   public close()
      done()
   end

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

class Serv(Regular9P)
   public 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(8r444)
   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 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 read(pos, count)
      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("eof")
               else
                  s
            }
         },
         reading := &null
      } | throw(&why)
   end

   public 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{ every (!l).close() }
          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)
         every (!l).close()
         if /prog then
            System.exec("/bin/rc", ["-i"]) | stop(&why)
         else
            System.exec("/bin/rc", ["-c", prog]) | stop(&why)
         syserr("Not reached")
      }
   end

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

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

procedure usage(opts)
   usage_impl(" [OPTIONS] [PROG]", opts)
end

procedure main(a)
   local root, name, l
   l := get_optl() ||| 
      [Opt("n", string, "The name of the single file to serve under the mountpoint; default \"prog\""),
       Opt("ee",, "Send a read error on EOF; this makes the con client program exit on EOF")]
   opts := options(a, l, usage)
   sched := Scheduler(100)
   # May be null, meaning run an interactive shell.
   prog := a[1]
   name := \opts["n"] | "prog"
   root := Root9P().
      set_fixed_perm(8r444).
      add_child(Prog(name).set_fixed_perm(8r666))
   server_main(, Session9P(TreeData9P(root)), sched)
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 close()
      parent.parent.remove_child(parent)
   end
end

class CloneFile(StringFile9P)
   private clone_count

   public 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 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 close()
      if *data > 0 then
         dialog.received.log_str(ucs(data), 100)
   end
end

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

class Get(Regular9P)
   public 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.

Contents