A modern, performant 9P library for Go.
For API documentation, see the GoDoc.
Refer to 9P's documentation for more details on the protocol.
Download the package in the usual way:
go get github.com/frobnitzem/go-p9p
Now run the server and client. Since there's no authentication, it's safest to put it into a unix socket.
cd $HOME/go/frobnitzem/go-p9p
go run cmd/9ps/main.go -root $HOME/src -addr unix:/tmp/sock9 &
chmod 700 /tmp/sock9
go run cmd/9pr/main.go -addr unix:/tmp/sock9
You should get a prompt,
/ 🐳 >
There's actually a tab-autocomplete to show you commands to run.
Bring the server down with:
kill %
rm /tmp/sock9
To build a filesystem using this library, implement
the FileSys interface, and then pass it to SFileSys
.
For details on the FileSys interface, see filesys.go
.
For examples, see ufs/
and sleepfs/
subdirectories.
For a main program running the ufs server, see cmd/9fs/
.
In case you don't already have a 9p client, try cmd/9fr/
,
but note it is not full-featured.
Alternatively, if you want to implement a server that works
at a lower level of the stack, you can implement a Session
interface directly. See session.go
for the interface
definition and servefs.go
for an implementation at this level.
This package handles the 9P protocol by implementing a stack of layers. For the server, each layer does some incremental translation of the messages "on the wire" into program state. The client is opposite, translating function calls into wire representations, and invoking client callbacks with responses from the server.
Applications can pick one of the layers to interact with. Lower layers generally require more work on the applications side (tracking state, encoding messages, etc.). It's generally recommended to interface only with the uppermost layer's API.
Extending the layers upward to make even simpler interface API-s, as well as implementing proper authentication and session encryption is on the TODO list.
sfilesys.go: SFileSys(fs FileSys) Session
- Creates a Session type from an FileSys
- The FileSys represents a fileserver broken into 3 levels, AuthFile-s, Dirent-s, and Files
- Rather than track Fid-s, calls to Dirent-s create more Dirent and Files.
ssesssion.go: Dispatch(session Session) Handler
- Delegates each type of messages.go:Message to a function call (from Session).
- Tags and TFlush-s are handled at this level, so higher levels do not see them. Instead, they see context.Context objects to indicate potential cancellation.
serveconn.go: ServeConn(ctx context.Context, cn net.Conn, handler Handler) error
- Negotiates protocol (with a timeout of 1 second).
- Calls server loop, reading messages and sending them to the handler.
- Version messages are handled at this level. They have the effect of deleting the current session and starting a new one. (Note: Check this to be sure.)
serverconn.go: (c *conn) serve() error
- Server loop, strips Tags and TFlush messages
- details:
- runs reader and writer goroutines
- maps from Tags to activeRequest structures
- spawns a goroutine to call c.handler.Handle on every non-TFlush
- these handlers get new contexts
- for TFlush, cancels the corresponding call
cfilesys.go: CFileSys(session Session) FileSys
- Creates an FileSys from a Session type
- The FileSys wraps up the session API so the user does not need to see Fid-s or Qid-s, unless you want to.
// Note: we could make into CSession(Handler) (Session, error)
client.go: CSession(ctx context.Context, conn net.Conn) (Session, error)
- negotiates protocol, returns client object
- client object has Walk/Stat/Open/Read methods that appear like synchronous send/receive pairs. Many requests can be sent in parallel, however (e.g. one goroutine each), and the transport will handle them in parallel, doing tag matching to return to the correct call.
transport.go: func newTransport(ctx context.Context, ch Channel) roundTripper
- starts a
handle
goroutine to take messages off the wire and invoke waiting response actions in the client. - roundTripper uses an internal channel to communicate with the handle so that each roundTripper can be synchronous, while the handler actually handles may requests.
channel.go: NewChannel(conn net.Conn, msize int) Channel
- typical argument for codec is codec9p
- called by ServeConn to serialize read/write from the net.Conn
- Channel interface provides ReadFcall, WriteFcall, MSize, SetMSize
channel.go: (ch *channel) WriteFcall(ctx context.Context, fcall *Fcall) error
channel.go: channel.go: (ch *channel) ReadFcall(ctx context.Context, fcall *Fcall) error
- check context for I/O cancellations
- last check on Msize (may result in error)
- for writing, call codec.Marshal, then sendmsg()
- for reading, call readmsg, then codec.Unmarshal
messages.go: newMessage(typ FcallType) (Message, error)
- called by encoding.go to write out structs (e.g. MessageTopen) for each type of message
encoding.go: interface Codec
- provides Marshal, Unmarshal, and Size for converting between
[]byte
and 9P message structs.
-
The server auto-generates calls to File.Close(). This function does nothing on the client side (use Clunk instead).
-
The client
qids = Walk(names...)
returns an Error/Warning when Walk returns a partial, incomplete walk to the destination. This happens if len(names) > 1 and walk returns len(qids) != len(names).
Lookup a Fid:
- assert Fid != NOFID
- lock session, lookup fid, unlock session
- lock ref
- This ensures that the ref was unlocked at some past point.
- if ref.ent == nil, fid is being deleted, unlock and return "no fid"
- return ref in locked state
- Client unlocks when they are done with operation on Fid.
- This forces no parallel operations on Fid at all.
- Convention: clone fids if you want parallel access.
Clunk/Remove a Fid:
- lock session, lookup fid, unlock session
- lock ref, lock session, delete fid:ref, unlock session, unlock ref
- close if ref.file != nil
- clunk/remove ref.ent
- This doesn't work because we may get multiple Walk/Open/Stat requests in parallel with Clunk. We need a way to stop those actions, then clunk.
Auth:
- lock session, lookup afid (ensure not present), create locked aref, store afid:aref, unlock session
- call Auth function to set aref.afile
- unlock afid
Attach:
- if fs.RequireAuth()): a. lock session, lookup aref, unlock session b. lock aref, call ok := aref.afile.Success(), unlock aref c. assert ok
- ref := holdRef [lock session, lookup fid (ensure not present), create locked ref, store afid:aref, unlock session]
- ent, err := Root()
- if err { lock session, delete fid:ref, unlock session, return }
- set ref.ent = ent, unlock ref
Note: Rather than using a locked fid lookup table, we could have the dispatcher do fid lookups, and send requests on a channel dedicated to (a goroutine) serving only that fid. This may be an optimization, or it may not - only lots of work and profiling will tell. So, this has not been tried.
Copyright © 2015 Docker, Inc. Copyright © 2023 UT-Battelle LLC. go-p9p is licensed under the Apache License, Version 2.0. See LICENSE for the full license text.