Hubfs

Hubfs is a 9p fs which can be used similarly to programs like tmux and GNU screen. Unlike those programs it allows shells from multiple machines to be shared to a single hub and enables free piping of data between processes on different machines. (Screen and tmux need an additional tool like ssh to connect multiple machines and have no ability to multiplex raw pipes.) Due to the use of 9p as the interface, hubfs brings shell inputs and outputs across a network of machines into the file namespace.

Hubfs is found on the sources server in contrib/mycroftiv/hubfs1.1. It can also be installed using fgb's contrib with:

contrib/install mycroftiv/hubfs

BASIC USE

The "hub" command both creates and attaches to hubfs shells. To start a new hubfs just type

hub

to create a new persistent shell. Hubfs uses /srv/hubfs as its default name. To connect to a running hubfs, the same

hub

command will mount and attach to an existing /srv/hubfs. To connect to a hubfs shell on a remote machine, you need access to that machine's /srv. From a client who wishes to connect to an existing shell on the server:

import -a server /srv
hub

By making the remote machine's /srv/hubfs visible in the namespace, the hub command will connect as usual.

The first parameter to the "hub" command is the name of the hubfs. If you wish to use a name other than /srv/hubfs the command

hub name

creates a new hubfs at /srv/name. Following this,

hub name

will attach to the hubfs at /srv/name.

LOCAL AND REMOTE SHELLS AND MOVING BETWEEN THEM

The "hub" command creates a default shell named "io" to begin. When you are working within a connected shell, you can create new shells. These shells can be created on either the machine hosting the hubfs or on the client machine. In other words, you have the option of "sharing a shell back" to the hubfs server from your machine. On a grid of machines, each client will usually share a local shell back to the central hubfs. The following commands are issued from within a connected shell:

%remote name

Creates and connects to a new shell running on the hubfs host.

%local name

Creates and moves to a new shell on the local machine and shares that shell to hubfs.

%attach name

Moves between existing shells attached to the hubfs. It is also possible to connect to existing subshells within a hubfs or create new ones directly from an external shell prompt.

hub srvname shellname

Acts to attach to the given shellname within the hubfs at srvname, or create and attach shellname to that hubfs if an existing subshell of that name is not present.

CONNECTING OTHER OPERATING SYSTEMS

Hubfs is client agnostic. If a system can use 9p, it can mount hubfs and access shells within it, and even share shells back. Here is an example using plan9port and 9pfuse. These actions would usually be automated by small scripts.

On the Plan 9 host:

hub -b unix
mount -c /srv/unix /n/unix
touch /n/unix/un0 /n/unix/un1 /n/unix/un2
aux/listen1 -tv tcp!*!8787 /bin/exportfs -r /n/unix

To connect a client machine such a linux box running p9p:

mkdir hubfs
9pfuse 'tcp!server.ip!8787' hubfs
rc -i <hubfs/un0 >>hubfs/un1 2>>hubfs/un2 &

This shares a shell from the linux box back to the hubfs. At this point the listen command on the server can be terminated. The linux box can now be controlled from any plan 9 system which can mount the hubfs. To control the Plan 9 system from linux:

cat hubfs/io1 &
cat hubfs/io2 &
cat >>hubfs/io0

And you are now attached to and controlling the initial Plan 9 hubfs shell. To attach to the shell on the linux machine from within a Plan 9 system with access to the /srv/unix :

hub unix un

Will connect to the shell running on the linux box.

GENERAL PURPOSE HUBFS: MULTIPLEXED NETWORK PIPES

The examples just shown illustrate that behind the scenes, hubfs itself simply multiplexes pipe-like files. This approach works for "screen-like" functionality in Plan 9, because there is no TTY layer to manage. Multiplexing access to a shell can be done efficiently simply by creating pipelike buffers for each file descriptor and allowing multiple readers and writers access to these files via 9p.

This is actually a powerful general purpose mechanism which can be used for more than just shells. Any textual/bytestream application or data stream can be attached to hubfs to multiplex access and persist the data. This can be used to create grid processing pipelines using shell commands with no additional clustering layer. For instance, suppose there is a textual data stream and it is desired to search this data stream for a number of different terms, then create a new data stream that is the composite of all the chosen matches.

mount -c /srv/hubfs /n/hubfs
touch /n/hubfs/streamin /n/hubfs/streamout
cat /lib/words >>/n/hubfs/streamin

Each processing node on the grid issues commands such as

import -a hubserver /srv
mount /srv/hubfs /n/hubfs
grep -b foo /n/hubfs/streamin >>/n/hubfs/streamout

The next machine greps for "bar", the next machine greps for "baz", etc. The result of this is an automatic "fan out" of the greps for different terms to different processing nodes and then creation of an output data stream at "streamout" which consists of all matches found by all nodes.

HUBFS FOR POWER USERS: FREEZE/MELT, DEVICE SAVING, TRICKS

Hubfs has some features and related scripts which offer additional capabilities for users. The most notable is probably the ability to "freeze" the flow of data in the hubfiles and allow them to treated as random-access static files, then "melt" them to resume the active flow of data. This enables the following workflow:

mount /srv/hubfs /n/hubfs
echo freeze >/n/hubfs/ctl
tar czf hublog.tgz /n/hubfs
echo melt >/hubfs/ctl

The result of this is that the buffer contents (the input/output history) of every file descriptor attached to the hubfs is saved as a tarball. If you look at this tarball, each file descriptor for each shell will have a file with its stream - so the io0 file contains the user input, the io1 file contains the standard output, and the io2 file contains the standard error output.

One standard use of this functionality would be to log irc channels where the irc programs are running inside hubfs. The author uses irc7 and the irc client from contrib/andrey and periodically updates logs with a freeze/melt cycle of the hubfs containing 4 different irc channels.

An issue users may notice is that programs such as "lc" which need to know the width of the window they are running within will not know about the width of the window a shell within hubfs is running. This issue can be dealt with by the use of "device saving" and "device retrieval" which has many applications. contrib/mycroftiv/rootlessnext/scripts contains the "savedevs" and "getdevs" scripts which can be used to connect a shell within hubfs to the active window to enable programs such as "lc" to run correctly, and also enable the use of some graphical programs, although they will not be multiplexed to other clients. Example usage, presuming a hubfs already exists and is accessible to be used:

savedevs myterm
hub
%io getdevs myterm

and now the shell within the hubfs will be aware of the current window size and track its changes properly.

An example of the kind of trick which hubfs enables with some practical and impractical uses:

grep -b FOO /n/myirc/plan9chan1 |while(echo `{read} >>/tmp/foolog) echo 'echo THERES ONE' >>/n/myirc/plan9chan0

That command is used on hubs running an irc client. It monitors the channel for mentions of FOO, logs them to a file, and makes the user shout THERES ONE into the channel whenever FOO is uttered. A similar concept can be used to do things such as trigger auto-plumb of .png files to the plumber when directories containing them are ls.

STDERR SYNCRHONIZATION

The simplicity of multiplexing shells just by buffering and multiplexing their file descriptors provides many benefits, but does have a few unexpected consequences. One such is the lack of enforced synchronization between shell file descriptors. When a large amount of data is transmitted on stdout and a small amount on stderr, it is a natural consequence of the parallelism and independence of the client readers that the client will finish reading the messages from stderr before finishing reading the the messages sent from stdout. This is expected behavior but contrary to user expectation that the shell prompt will only appear after the output of a command has been completed. To address this, the user can set a delay on a given file descriptor in the hubshell client.

%err 300

Will add 300 milliseconds of delay before printing messages from stderr on that shell. This usually improves the user experience when working with a remote machine over WAN. There is no negative consequence from these seemingly misplaced prompts, commands are received and sent normally regardless of the visual location of the prompt in the output stream as seen by the client on-screen. The lack of synchronization is client-side and the result of parallelized reads - the prompt or error messages are always printed at the normal time on the server end.

PARANOID MODE

Paranoid mode is a nonstandard mode of operation designed for the transmission of large continuous data streams. Hubfs is optimized for the type of data transmission typical of shell usage - bursts of data, where the size of a single burst is less than 500kb and often much smaller. It prioritizes interactivity and provides a semantics where writers are allows allowed to write without needing to wait for client readers to read data previously sent. This is different than conventional blocking write semantics on pipes.

As a result, normal hubfs operation is often unsuited to a scenario where you are sending a continuous stream of data of a size of megabytes or more. The hazard is that writers will write data faster than remote readers can read it, and the writer will overwrite the portion of the buffer that the client has not yet read, resulting in corruption of the data stream and loss of data. "paranoid" mode is provided to avoid this. In the event it is desired to send a large continuous stream of data much larger than the hubfs static circular buffer of 700k, the command

echo fear >/n/hubfs/ctl

puts hubfs into paranoid mode, where the speed of writes is gated by the ability of readers to catch up. This mode incurs a considerable performance penalty and cpu usage tax and is not needed for normal use by interactive shells.

echo calm >/n/hubfs/ctl

returns hubfs to standard operation.

GRIO AND OTHER RELATED TOOLS

To make access to the persistent shells with hubfs as easy as possible, the modified rio(1) grio (found at sources/contrib/mycroftiv/grio) automatically creates /srv/riohubfs.$user and provides a new menu option, Hub, to sweep out a window in the same manner as New, but rather than creating a new rc(1), it connects to the existing hubfs session. The effect is that when you make a new window, you are 'back where you were before'. The standard New options remains as before.

On a grid of machines, if the central cpu server hosts the main hubfs, and user includes

import -a cpuserver /srv &

in their profile, then when grio is started, it will automatically make use of that pre-existing central hubfs. Assuming all nodes also use %local to share shells back to the central hubfs server, this gives the user persistent access to shells on machines on each node of their grid available directly from the creation of a new rio window.

Hubfs and grio are independent parts of a larger toolkit of Plan 9 namespace software found in sources/contrib/mycroftiv/rootlessnext and with full documentation and papers at ANTS.

ARCHITECTURE AND INTERNALS

Mostly unmentioned in this discussion has been the hubshell client, which is conventionally used for creating and moving between hubfs shells. Hubfs itself simply creates pipe-like files. It is not aware of what applications are being connected to the files, it simply provides buffers and answers read and write requests. Hubfs.c itself is deliberately simplistic and was evolved from the sample ramfs implementation in the distribution at /sys/src/lib9p/ramfs.c.

Hubshell.c is the main client of hubfs. The %commands are intercepted and handled by hubshell, which knows how to create new rc(1) shells attached to hubfs and to move between them. In the standard mode of use, there are several chains-of-cats between the user and the shell. The input sent by the users (and output they read) is actually that of hubshell, which forks 3 cats to transfer data between itself and the hubfiles - and those hubfiles are essentially "buffered cats" connected to an rc.

The hub wrapper script checks the namespace at /n/name or /srv/name for an available hubfs and creates a new hubfs if necessary, then starts a hubshell connected to the target.

Internally, hubfs creates a statically sized buffer for each hubfile, then writers write data circularly from beginning to end and wrap around when they fill the buffer. Each hub maintains two "spindles" of 9p requests, one for reads and one for writes. Writes are always appended but the location of each reader needs to be tracked individually. Hubfs is usually single-process and single-threaded, but paranoid mode changes this and in paranoid mode hubfs uses rfork and qlocks to separate the flow of control handling write requests from that handling read requests.

The pre-existing consolefs(4) in the standard distribution has a fairly similar architecture and is focused on management of connected serial consoles.