diff --git a/cmd/toolsrv/main.go b/cmd/toolsrv/main.go index 6473b11..9026cb1 100644 --- a/cmd/toolsrv/main.go +++ b/cmd/toolsrv/main.go @@ -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") -} diff --git a/cmd/toolsrv/server.go b/cmd/toolsrv/server.go new file mode 100644 index 0000000..3577c94 --- /dev/null +++ b/cmd/toolsrv/server.go @@ -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 +}