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 Task
s. 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.
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.
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
:-
<?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
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 :-
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()
.
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.
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 Task
s to process these messages. The scheduler is then repeatedly invoked until it is empty of Task
s, 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 Task
s. 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
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.
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
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 :-
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
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.
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 read
s 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.
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.
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).
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.
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
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.
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.
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.
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
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
.
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 File
s 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.
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!