cmd/toolsrv: implement 9P protocol server

- server.go: Full 9P2000 protocol handling
  - version, auth, attach, walk, open, read, write, clunk, stat
  - Uses fsedsl.Tree for all operations
  - Handles directories and files
  - Supports rdwr files (RequestRespMode)

- main.go: Remove placeholder serve9P, use new implementation

The toolsrv binary now serves a working 9P filesystem with the
namespace defined in toolsrv/spec9p.go:
  /register, /ctl, /tools, /proc/*, /info
This commit is contained in:
Levi Neely 2026-08-10 17:53:01 +02:00
parent 427cadeb29
commit 75f364dd85
2 changed files with 413 additions and 11 deletions

View File

@ -120,14 +120,3 @@ func main() {
go serve9P(runCtx, conn, tree)
}
}
// serve9P handles a single 9P connection.
// TODO: This needs to implement the 9P protocol using the fsedsl tree.
// For now this is a placeholder - the actual implementation should follow
// the pattern in cmd/olliesrv/server.go.
func serve9P(ctx context.Context, conn net.Conn, tree *fsedsl.Tree) {
defer conn.Close()
// TODO: Implement 9P protocol handling
// This would use plan9.ReadFcall/WriteFcall and dispatch to tree operations
fmt.Fprintf(os.Stderr, "toolsrv: new connection (9P serving not yet implemented)\n")
}

413
cmd/toolsrv/server.go Normal file
View File

@ -0,0 +1,413 @@
// server.go - 9P filesystem server for toolsrv.
package main
import (
"context"
"fmt"
"io"
"net"
"os"
"sync"
"ollie/fsedsl"
"9fans.net/go/plan9"
)
// fid represents an open file handle.
type fid struct {
path string
qid plan9.Qid
mode uint8
opened bool
entry fsedsl.File
readBuf []byte // buffer for read data
readDone bool // true if file has been read
dirCache []byte
}
// connState holds per-connection state.
type connState struct {
mu sync.RWMutex
fids map[uint32]*fid
ctx context.Context
uname string
}
// serve9P handles a single 9P connection using the fsedsl tree.
func serve9P(ctx context.Context, conn net.Conn, tree *fsedsl.Tree) {
defer conn.Close()
cs := &connState{
fids: make(map[uint32]*fid),
ctx: ctx,
}
for {
select {
case <-ctx.Done():
return
default:
}
fc, err := plan9.ReadFcall(conn)
if err != nil {
if err != io.EOF {
fmt.Fprintf(os.Stderr, "toolsrv: read error: %v\n", err)
}
return
}
resp := handleFcall(cs, fc, tree)
if err := plan9.WriteFcall(conn, resp); err != nil {
fmt.Fprintf(os.Stderr, "toolsrv: write error: %v\n", err)
return
}
}
}
func handleFcall(cs *connState, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall {
switch fc.Type {
case plan9.Tversion:
return handleVersion(fc)
case plan9.Tauth:
return handleAuth(fc)
case plan9.Tattach:
return handleAttach(cs, fc, tree)
case plan9.Twalk:
return handleWalk(cs, fc, tree)
case plan9.Topen:
return handleOpen(cs, fc, tree)
case plan9.Tread:
return handleRead(cs, fc, tree)
case plan9.Twrite:
return handleWrite(cs, fc, tree)
case plan9.Tclunk:
return handleClunk(cs, fc)
case plan9.Tstat:
return handleStat(cs, fc, tree)
default:
return errFcall(fc, fmt.Sprintf("unsupported: %d", fc.Type))
}
}
func errFcall(fc *plan9.Fcall, msg string) *plan9.Fcall {
return &plan9.Fcall{Type: plan9.Rerror, Tag: fc.Tag, Ename: msg}
}
func handleVersion(fc *plan9.Fcall) *plan9.Fcall {
msize := fc.Msize
if msize > 65536 {
msize = 65536
}
return &plan9.Fcall{Type: plan9.Rversion, Tag: fc.Tag, Msize: msize, Version: "9P2000"}
}
func handleAuth(fc *plan9.Fcall) *plan9.Fcall {
// Simple auth - accept all for now
// TODO: Implement shared secret verification
return &plan9.Fcall{Type: plan9.Rauth, Tag: fc.Tag, Aqid: plan9.Qid{Type: plan9.QTAUTH}}
}
func handleAttach(cs *connState, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall {
cs.mu.Lock()
cs.uname = fc.Uname
cs.fids[fc.Fid] = &fid{
path: "/",
qid: plan9.Qid{Type: plan9.QTDIR, Path: 0},
}
cs.mu.Unlock()
return &plan9.Fcall{Type: plan9.Rattach, Tag: fc.Tag, Qid: plan9.Qid{Type: plan9.QTDIR, Path: 0}}
}
func handleWalk(cs *connState, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall {
cs.mu.RLock()
f, ok := cs.fids[fc.Fid]
cs.mu.RUnlock()
if !ok {
return errFcall(fc, "unknown fid")
}
newPath := f.path
var qids []plan9.Qid
for _, name := range fc.Wname {
if name == ".." {
if newPath != "/" {
lastSlash := len(newPath) - 1
for lastSlash > 0 && newPath[lastSlash] != '/' {
lastSlash--
}
if lastSlash == 0 {
newPath = "/"
} else {
newPath = newPath[:lastSlash]
}
}
} else {
if newPath == "/" {
newPath = "/" + name
} else {
newPath = newPath + "/" + name
}
}
// Check if path exists in tree
treePath := newPath
if treePath == "/" {
treePath = ""
} else {
treePath = treePath[1:] // strip leading /
}
info, err := tree.Stat(treePath)
if err != nil {
if len(qids) == 0 {
return errFcall(fc, "not found")
}
break
}
var qt uint8 = plan9.QTFILE
if info.IsDir() {
qt = plan9.QTDIR
}
qids = append(qids, plan9.Qid{Type: qt, Path: pathHash(newPath)})
}
if len(qids) == len(fc.Wname) && fc.Newfid != fc.Fid {
var lastQid plan9.Qid
if len(qids) > 0 {
lastQid = qids[len(qids)-1]
} else {
lastQid = f.qid
}
cs.mu.Lock()
cs.fids[fc.Newfid] = &fid{
path: newPath,
qid: lastQid,
}
cs.mu.Unlock()
} else if len(qids) == len(fc.Wname) && len(qids) > 0 {
cs.mu.Lock()
f.path = newPath
f.qid = qids[len(qids)-1]
cs.mu.Unlock()
}
return &plan9.Fcall{Type: plan9.Rwalk, Tag: fc.Tag, Wqid: qids}
}
func handleOpen(cs *connState, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall {
cs.mu.Lock()
defer cs.mu.Unlock()
f, ok := cs.fids[fc.Fid]
if !ok {
return errFcall(fc, "unknown fid")
}
// Directories don't need an entry
if f.qid.Type&plan9.QTDIR != 0 {
f.opened = true
f.mode = fc.Mode
return &plan9.Fcall{Type: plan9.Ropen, Tag: fc.Tag, Qid: f.qid, Iounit: 8192}
}
path := f.path
if path == "/" {
path = ""
} else {
path = path[1:] // strip leading /
}
entry, err := tree.Open(path)
if err != nil {
return errFcall(fc, err.Error())
}
f.entry = entry
f.opened = true
f.mode = fc.Mode
f.readBuf = nil
f.readDone = false
return &plan9.Fcall{Type: plan9.Ropen, Tag: fc.Tag, Qid: f.qid, Iounit: 8192}
}
func handleRead(cs *connState, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall {
cs.mu.RLock()
f, ok := cs.fids[fc.Fid]
cs.mu.RUnlock()
if !ok {
return errFcall(fc, "unknown fid")
}
if !f.opened {
return errFcall(fc, "not open")
}
if f.qid.Type&plan9.QTDIR != 0 {
return handleDirRead(cs, f, fc, tree)
}
if f.entry == nil {
return errFcall(fc, "no entry")
}
// For rdwr files, check if we've already read
if f.entry.RequestRespMode() && f.readDone {
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Data: nil}
}
// Buffer read data if not already done
if f.readBuf == nil {
data, err := f.entry.Read()
if err != nil && err != io.EOF {
return errFcall(fc, err.Error())
}
f.readBuf = data
}
// Handle offset/count
offset := int(fc.Offset)
if offset >= len(f.readBuf) {
f.readDone = true
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Data: nil}
}
end := offset + int(fc.Count)
if end > len(f.readBuf) {
end = len(f.readBuf)
}
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Data: f.readBuf[offset:end]}
}
func handleDirRead(cs *connState, f *fid, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall {
if fc.Offset == 0 || f.dirCache == nil {
path := f.path
if path == "/" {
path = ""
} else {
path = path[1:]
}
entries, err := tree.Readdir(path)
if err != nil {
return errFcall(fc, err.Error())
}
var buf []byte
for _, e := range entries {
var qt uint8 = plan9.QTFILE
mode := plan9.Perm(0644)
if e.IsDir() {
qt = plan9.QTDIR
mode = plan9.Perm(plan9.DMDIR | 0755)
}
dir := plan9.Dir{
Name: e.Name(),
Qid: plan9.Qid{Type: qt, Path: pathHash(f.path + "/" + e.Name())},
Mode: mode,
Uid: "toolsrv",
Gid: "toolsrv",
}
data, _ := dir.Bytes()
buf = append(buf, data...)
}
f.dirCache = buf
}
offset := int(fc.Offset)
if offset >= len(f.dirCache) {
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Data: nil}
}
end := offset + int(fc.Count)
if end > len(f.dirCache) {
end = len(f.dirCache)
}
return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Data: f.dirCache[offset:end]}
}
func handleWrite(cs *connState, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall {
cs.mu.Lock()
f, ok := cs.fids[fc.Fid]
cs.mu.Unlock()
if !ok {
return errFcall(fc, "unknown fid")
}
if !f.opened {
return errFcall(fc, "not open")
}
if f.entry == nil {
return errFcall(fc, "no entry")
}
err := f.entry.Write(fc.Data)
if err != nil {
return errFcall(fc, err.Error())
}
return &plan9.Fcall{Type: plan9.Rwrite, Tag: fc.Tag, Count: uint32(len(fc.Data))}
}
func handleClunk(cs *connState, fc *plan9.Fcall) *plan9.Fcall {
cs.mu.Lock()
delete(cs.fids, fc.Fid)
cs.mu.Unlock()
return &plan9.Fcall{Type: plan9.Rclunk, Tag: fc.Tag}
}
func handleStat(cs *connState, fc *plan9.Fcall, tree *fsedsl.Tree) *plan9.Fcall {
cs.mu.RLock()
f, ok := cs.fids[fc.Fid]
cs.mu.RUnlock()
if !ok {
return errFcall(fc, "unknown fid")
}
path := f.path
if path == "/" {
path = ""
} else {
path = path[1:]
}
info, err := tree.Stat(path)
if err != nil {
return errFcall(fc, err.Error())
}
var qt uint8 = plan9.QTFILE
mode := plan9.Perm(0644)
if info.IsDir() {
qt = plan9.QTDIR
mode = plan9.Perm(plan9.DMDIR | 0755)
}
name := info.Name()
if name == "" {
name = "/"
}
dir := plan9.Dir{
Name: name,
Qid: plan9.Qid{Type: qt, Path: pathHash(f.path)},
Mode: mode,
Uid: "toolsrv",
Gid: "toolsrv",
}
data, _ := dir.Bytes()
return &plan9.Fcall{Type: plan9.Rstat, Tag: fc.Tag, Stat: data}
}
func pathHash(p string) uint64 {
var h uint64 = 5381
for _, c := range p {
h = h*33 + uint64(c)
}
return h
}