// 9P filesystem server for ollie sessions. package main import ( "context" "fmt" "hash/fnv" "io" "net" "os" "os/user" "runtime/debug" "strings" "sync" "time" "ollie/cmd/olliesrv/internal/fs" "ollie/cmd/olliesrv/internal/session" olog "ollie/log" "9fans.net/go/plan9" ) type ( syntheticFileInfo = fs.SyntheticFileInfo ) const ( QTDir = plan9.QTDIR QTFile = plan9.QTFILE ) // serverAdmin is the username of the user running olliesrv. // This user has admin access to all files regardless of ownership. var serverAdmin string func init() { if u, err := user.Current(); err == nil { serverAdmin = u.Username } } type GroupTable struct { mu sync.RWMutex groups map[string]map[string]bool } func NewGroupTable() *GroupTable { return &GroupTable{groups: make(map[string]map[string]bool)} } func (g *GroupTable) Populate(group, user string) { g.mu.Lock() defer g.mu.Unlock() if g.groups[group] == nil { g.groups[group] = make(map[string]bool) } g.groups[group][user] = true } func (g *GroupTable) IsMember(group, user string) bool { g.mu.RLock() defer g.mu.RUnlock() return g.groups[group][user] } type fid struct { mu sync.Mutex path string qid plan9.Qid mode uint8 entry fs.File writeBuf []byte readCache []byte // cached content for offset-based paging waitBase string dirCache []byte // Event stream subscription (for event file with filter) eventFilter string eventCh <-chan session.Event eventCancel context.CancelFunc } type connState struct { mu sync.RWMutex fids map[uint32]*fid ctx context.Context cancel context.CancelFunc pending map[uint16]context.CancelFunc uname string } type Server struct { mu sync.RWMutex conns []*connState log *olog.Logger sink *olog.Sink rootTree *fs.Tree groups *GroupTable serveWG sync.WaitGroup shutdownCtx context.Context shutdownCancel context.CancelFunc connMu sync.Mutex activeConns map[net.Conn]struct{} handler map[uint8]func(*connState, *plan9.Fcall, context.Context) *plan9.Fcall } type Config struct { Sink *olog.Sink RootTree *fs.Tree } func New(cfg Config) *Server { ctx, cancel := context.WithCancel(context.Background()) s := &Server{ log: cfg.Sink.NewLogger("9p"), sink: cfg.Sink, rootTree: cfg.RootTree, groups: NewGroupTable(), shutdownCtx: ctx, shutdownCancel: cancel, activeConns: make(map[net.Conn]struct{}), } s.handler = map[uint8]func(*connState, *plan9.Fcall, context.Context) *plan9.Fcall{ plan9.Tversion: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { msize := fc.Msize if msize > 65536 { msize = 65536 } return &plan9.Fcall{Type: plan9.Rversion, Tag: fc.Tag, Msize: msize, Version: "9P2000"} }, plan9.Tauth: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return errFcall(fc, "no auth required") }, plan9.Tflush: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return flush(cs, fc) }, plan9.Tattach: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return attach(s.groups, cs, fc) }, plan9.Twalk: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return walk(cfg.RootTree, cs, fc) }, plan9.Topen: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return open(cfg.RootTree, s.groups, cs, fc) }, plan9.Tcreate: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return create(cfg.RootTree, cs, fc) }, plan9.Tread: func(cs *connState, fc *plan9.Fcall, ctx context.Context) *plan9.Fcall { return read(cfg.RootTree, cs, fc, ctx) }, plan9.Twrite: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return write(cs, fc) }, plan9.Tstat: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return stat(cfg.RootTree, cs, fc) }, plan9.Twstat: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return wstat(cfg.RootTree, cs, fc) }, plan9.Tclunk: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return clunk(cfg.RootTree, cs, fc) }, plan9.Tremove: func(cs *connState, fc *plan9.Fcall, _ context.Context) *plan9.Fcall { return remove(cfg.RootTree, cs, fc) }, } return s } func fileOwnerGroup(root *fs.Tree, path string) (uid, gid string) { info, err := root.Stat(path[1:]) if err == nil { if fi, ok := info.(*syntheticFileInfo); ok { if fi.UID_ != "" || fi.GID_ != "" { return fi.UID_, fi.GID_ } } } if u, err := user.Current(); err == nil { return u.Username, "agent" } return "ollie", "agent" } var permMask = [4]uint32{4, 2, 6, 1} // OREAD, OWRITE, ORDWR, OEXEC func hasPermBits(mode uint8, bits uint32) bool { m := permMask[mode&3] return bits&m == m } const readTimeout = 10 * time.Second func (s *Server) Start(conn net.Conn) { defer conn.Close() s.serveWG.Add(1) defer s.serveWG.Done() s.connMu.Lock() s.activeConns[conn] = struct{}{} s.connMu.Unlock() defer func() { s.connMu.Lock() delete(s.activeConns, conn) s.connMu.Unlock() }() connCtx, connCancel := context.WithCancel(s.shutdownCtx) cs := &connState{ fids: make(map[uint32]*fid), ctx: connCtx, cancel: connCancel, pending: make(map[uint16]context.CancelFunc), } s.mu.Lock() s.conns = append(s.conns, cs) s.mu.Unlock() defer func() { connCancel() s.mu.Lock() for i, c := range s.conns { if c == cs { s.conns = append(s.conns[:i], s.conns[i+1:]...) break } } s.mu.Unlock() }() responses := make(chan *plan9.Fcall, 16) var wg sync.WaitGroup var writerWG sync.WaitGroup writerWG.Add(1) go func() { defer writerWG.Done() for resp := range responses { if err := plan9.WriteFcall(conn, resp); err != nil { s.log.Error("write: %v", err) connCancel() conn.Close() return } } }() stop := func() { connCancel() conn.Close() wg.Wait() close(responses) writerWG.Wait() closeFids(cs) } for { // Set read deadline so we can check for shutdown periodically conn.SetReadDeadline(time.Now().Add(1 * time.Second)) fc, err := plan9.ReadFcall(conn) if err != nil { // Check if shutdown was requested if connCtx.Err() != nil { break } // Timeout - loop and check context again if netErr, ok := err.(net.Error); ok && netErr.Timeout() { continue } if err != io.EOF { s.log.Error("read: %v", err) } break } reqCtx, reqCancel := context.WithCancel(connCtx) cs.mu.Lock() cs.pending[fc.Tag] = reqCancel cs.mu.Unlock() wg.Add(1) go func(fc *plan9.Fcall, ctx context.Context) { start := time.Now() resp := func() (r *plan9.Fcall) { defer func() { if rec := recover(); rec != nil { s.log.Error("panic in handle: %v", rec) fmt.Fprintf(os.Stderr, "panic in handle: %v\n%s\n", rec, debug.Stack()) r = errFcall(fc, "internal error") } s.log.Debug(" -> %s %dµs", fcallTypeName(fc.Type), time.Since(start).Microseconds()) }() h := s.handler[fc.Type] if h == nil { return errFcall(fc, "unsupported operation") } return h(cs, fc, ctx) }() defer func() { reqCancel() cs.mu.Lock() delete(cs.pending, fc.Tag) cs.mu.Unlock() wg.Done() }() select { case responses <- resp: case <-connCtx.Done(): } }(fc, reqCtx) } stop() } func closeFids(cs *connState) { cs.mu.Lock() fids := make([]*fid, 0, len(cs.fids)) for id, f := range cs.fids { delete(cs.fids, id) fids = append(fids, f) } cs.mu.Unlock() for _, f := range fids { if f.entry != nil { _ = f.entry.Close() } } } func flush(cs *connState, fc *plan9.Fcall) *plan9.Fcall { cs.mu.Lock() if cancel, ok := cs.pending[fc.Oldtag]; ok { cancel() } cs.mu.Unlock() return &plan9.Fcall{Type: plan9.Rflush, Tag: fc.Tag} } var fcallTypeNames = map[uint8]string{ plan9.Tversion: "Tversion", plan9.Tauth: "Tauth", plan9.Tattach: "Tattach", plan9.Twalk: "Twalk", plan9.Topen: "Topen", plan9.Tcreate: "Tcreate", plan9.Tread: "Tread", plan9.Twrite: "Twrite", plan9.Tstat: "Tstat", plan9.Twstat: "Twstat", plan9.Tflush: "Tflush", plan9.Tclunk: "Tclunk", plan9.Tremove: "Tremove", } func fcallTypeName(t uint8) string { if name, ok := fcallTypeNames[t]; ok { return name } return fmt.Sprintf("T%d", t) } func errFcall(fc *plan9.Fcall, msg string) *plan9.Fcall { return &plan9.Fcall{Type: plan9.Rerror, Tag: fc.Tag, Ename: msg} } func qidPath(path string) uint64 { if path == "/" { return 0 } h := fnv.New64a() h.Write([]byte(path)) return h.Sum64() } func pathParent(path string) string { if path == "/" { return "/" } i := strings.LastIndex(path, "/") if i == 0 { return "/" } return path[:i] } func pathJoin(parent, name string) string { if parent == "/" { return "/" + name } return parent + "/" + name } func pathBase(path string) string { i := strings.LastIndex(path, "/") if i < 0 { return path } return path[i+1:] } func attach(groups *GroupTable, cs *connState, fc *plan9.Fcall) *plan9.Fcall { cs.mu.Lock() defer cs.mu.Unlock() if _, exists := cs.fids[fc.Fid]; exists { return errFcall(fc, "fid already in use") } cs.uname = fc.Uname qid := plan9.Qid{Type: QTDir, Path: 0} cs.fids[fc.Fid] = &fid{path: "/", qid: qid} if fc.Uname != "" { if session.IsSubagent(fc.Uname) { groups.Populate("subagent", fc.Uname) } else { groups.Populate("agent", fc.Uname) } } // Admin users (CLI, GUI) get full access if fc.Uname == "" || fc.Uname == "admin" || fc.Uname == serverAdmin { groups.Populate("agent", fc.Uname) // admin is in the agent group } return &plan9.Fcall{Type: plan9.Rattach, Tag: fc.Tag, Qid: qid} } func walk(root *fs.Tree, cs *connState, fc *plan9.Fcall) *plan9.Fcall { cs.mu.Lock() defer cs.mu.Unlock() f, ok := cs.fids[fc.Fid] if !ok { return errFcall(fc, "bad fid") } newf := &fid{path: f.path, qid: f.qid} if len(fc.Wname) == 0 { if _, exists := cs.fids[fc.Newfid]; exists { return errFcall(fc, "fid already in use") } cs.fids[fc.Newfid] = newf return &plan9.Fcall{Type: plan9.Rwalk, Tag: fc.Tag, Wqid: []plan9.Qid{}} } wqids := make([]plan9.Qid, 0, len(fc.Wname)) cur := f.path for _, name := range fc.Wname { var next string if name == ".." { next = pathParent(cur) } else { next = pathJoin(cur, name) } info, err := root.Stat(next[1:]) if err != nil { if len(wqids) == 0 { return errFcall(fc, name+": file not found") } break } q := plan9.Qid{Path: qidPath(next)} if info.IsDir() { q.Type = QTDir } wqids = append(wqids, q) cur = next } if len(wqids) == len(fc.Wname) { if _, exists := cs.fids[fc.Newfid]; exists { return errFcall(fc, "fid already in use") } newf.path = cur newf.qid = wqids[len(wqids)-1] cs.fids[fc.Newfid] = newf } return &plan9.Fcall{Type: plan9.Rwalk, Tag: fc.Tag, Wqid: wqids} } func open(root *fs.Tree, groups *GroupTable, cs *connState, fc *plan9.Fcall) *plan9.Fcall { cs.mu.Lock() defer cs.mu.Unlock() f, ok := cs.fids[fc.Fid] if !ok { return errFcall(fc, "bad fid") } if _, err := root.Stat(f.path[1:]); err != nil { delete(cs.fids, fc.Fid) return errFcall(fc, "file not found") } uid, gid := fileOwnerGroup(root, f.path) perm := uint32(makeStat(root, f.path).Mode) & 0777 // CAPABILITY CHECK: Unix file permissions as the agent capability model. // // Each agent file is owned by its agent ID (UID) and belongs to the // "agent" group (GID). Permission bits determine what operations are // allowed: // - Owner bits (0700): only the owning agent // - Group bits (0070): any agent in the "agent" group // - World bits (0007): any client // // This is structural enforcement: if agent A tries to open agent B's // plan file (mode 0600, owned by B), the permission check fails at // the protocol layer. No prompt injection or model persuasion can // bypass a Unix permission check. // // Admin clients (empty uname, "admin", or server owner) bypass checks for management. isAdmin := cs.uname == "" || cs.uname == "admin" || cs.uname == serverAdmin if isAdmin { // Admin bypasses permission checks } else { bits := perm & 7 // world bits if groups.IsMember(gid, cs.uname) { bits |= (perm >> 3) & 7 // group bits } if cs.uname == uid { bits |= (perm >> 6) & 7 // owner bits } if !hasPermBits(fc.Mode, bits) { return errFcall(fc, "permission denied") } } f.mode = fc.Mode if ent, err := root.Open(f.path[1:]); err == nil { f.entry = ent } return &plan9.Fcall{Type: plan9.Ropen, Tag: fc.Tag, Qid: f.qid} } func create(root *fs.Tree, cs *connState, fc *plan9.Fcall) *plan9.Fcall { cs.mu.Lock() defer cs.mu.Unlock() f, ok := cs.fids[fc.Fid] if !ok { return errFcall(fc, "bad fid") } newPath := pathJoin(f.path, fc.Name) rel := newPath[1:] if fc.Perm&plan9.DMDIR != 0 { if err := root.MkdirAll(rel); err != nil { return errFcall(fc, err.Error()) } qid := plan9.Qid{Type: QTDir, Path: qidPath(newPath)} f.path = newPath f.qid = qid f.mode = fc.Mode return &plan9.Fcall{Type: plan9.Rcreate, Tag: fc.Tag, Qid: qid} } if err := root.Create(rel); err != nil { return errFcall(fc, err.Error()) } qid := plan9.Qid{Path: qidPath(newPath)} f.path = newPath f.qid = qid f.mode = fc.Mode if ent, err := root.Open(rel); err == nil { f.entry = ent } return &plan9.Fcall{Type: plan9.Rcreate, Tag: fc.Tag, Qid: qid} } func read(root *fs.Tree, cs *connState, fc *plan9.Fcall, ctx context.Context) *plan9.Fcall { cs.mu.RLock() f, ok := cs.fids[fc.Fid] if !ok { cs.mu.RUnlock() return errFcall(fc, "bad fid") } f.mu.Lock() path := f.path isDir := f.qid.Type&QTDir != 0 entry := f.entry f.mu.Unlock() cs.mu.RUnlock() if isDir { f.mu.Lock() cached := f.dirCache f.mu.Unlock() if fc.Offset == 0 || cached == nil { cached = buildDirData(root, path) f.mu.Lock() if fc.Offset == 0 || f.dirCache == nil { f.dirCache = cached } cached = f.dirCache f.mu.Unlock() } data := sliceDirData(cached, fc.Offset, fc.Count) return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: uint32(len(data)), Data: data} } if entry == nil { return errFcall(fc, "file not opened") } if entry.BlockingReadMode() { // Serve from cache if we already have content (multi-Tread paging). f.mu.Lock() cached := f.readCache f.mu.Unlock() if cached != nil { if int(fc.Offset) >= len(cached) { return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: 0} } return readSlice(fc, cached) } // First Tread: block for content. cs.mu.RLock() f, fidOK := cs.fids[fc.Fid] var base string if fidOK { f.mu.Lock() base = f.waitBase f.mu.Unlock() } cs.mu.RUnlock() waitCtx, waitCancel := context.WithTimeout(ctx, 500*time.Millisecond) defer waitCancel() content, nextBase, err := entry.BlockingRead(waitCtx, base) if err != nil { return errFcall(fc, err.Error()) } if len(content) == 0 { // Timeout: fall back to non-blocking Read for current value. content, _ = entry.Read() } if nextBase != "" { cs.mu.Lock() if f, ok := cs.fids[fc.Fid]; ok { f.mu.Lock() f.waitBase = nextBase f.mu.Unlock() } cs.mu.Unlock() } f.mu.Lock() f.readCache = content f.mu.Unlock() return readSlice(fc, content) } if entry.StreamMode() { // Special handling for event file — always use per-fid subscription if path == "event" || path == "/event" { cs.mu.RLock() f, fidOK := cs.fids[fc.Fid] // Use 9P connection context (not request context) so subscription // lives until the client disconnects or clunks the fid clientConnCtx := cs.ctx cs.mu.RUnlock() if !fidOK { return errFcall(fc, "bad fid") } f.mu.Lock() filter := f.eventFilter eventCh := f.eventCh f.mu.Unlock() // Create subscription on first read (filter defaults to "*" for all events) if eventCh == nil { if filter == "" { filter = "*" } subCtx, cancel := context.WithCancel(clientConnCtx) ch := session.SubscribeEventsFiltered(subCtx, filter) f.mu.Lock() f.eventFilter = filter f.eventCh = ch f.eventCancel = cancel f.mu.Unlock() eventCh = ch } // Read from subscription — blocks until event arrives select { case ev, ok := <-eventCh: if !ok { return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: 0} } line := ev.Topic if ev.Payload != "" { line += " " + ev.Payload } content := []byte(line + "\n") count := int(fc.Count) if count > len(content) { count = len(content) } return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: uint32(count), Data: content[:count]} case <-ctx.Done(): return errFcall(fc, "interrupted") } } // Default stream handling (no filter) cs.mu.RLock() f, fidOK := cs.fids[fc.Fid] var base string if fidOK { f.mu.Lock() base = f.waitBase f.mu.Unlock() } cs.mu.RUnlock() waitCtx, waitCancel := context.WithCancel(ctx) defer waitCancel() content, nextBase, err := entry.BlockingRead(waitCtx, base) if err != nil { return errFcall(fc, err.Error()) } if nextBase != "" { cs.mu.Lock() if f, ok := cs.fids[fc.Fid]; ok { f.mu.Lock() f.waitBase = nextBase f.mu.Unlock() } cs.mu.Unlock() } count := int(fc.Count) if count > len(content) { count = len(content) } return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: uint32(count), Data: content[:count]} } cs.mu.RLock() f, ok = cs.fids[fc.Fid] cs.mu.RUnlock() if !ok { return errFcall(fc, "bad fid") } f.mu.Lock() cached := f.readCache f.mu.Unlock() if cached != nil { return readSlice(fc, cached) } type result struct { data []byte err error } ch := make(chan result, 1) go func() { data, err := entry.Read() ch <- result{data, err} }() timer := time.NewTimer(readTimeout) defer timer.Stop() select { case r := <-ch: if r.err != nil { return errFcall(fc, r.err.Error()) } f.mu.Lock() f.readCache = r.data cached := f.readCache f.mu.Unlock() return readSlice(fc, cached) case <-ctx.Done(): entry.Close() return errFcall(fc, ctx.Err().Error()) case <-timer.C: entry.Close() return errFcall(fc, "read timeout") } } func readSlice(fc *plan9.Fcall, content []byte) *plan9.Fcall { var data []byte off := int(fc.Offset) if off < len(content) { end := off + int(fc.Count) if end > len(content) { end = len(content) } data = content[off:end] } return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: uint32(len(data)), Data: data} } func write(cs *connState, fc *plan9.Fcall) *plan9.Fcall { cs.mu.Lock() f, ok := cs.fids[fc.Fid] if !ok { cs.mu.Unlock() return errFcall(fc, "bad fid") } f.mu.Lock() path := f.path entry := f.entry // Special handling for event file: write sets the filter if path == "/event" || path == "event" { filter := strings.TrimSpace(string(fc.Data)) if filter == "" { filter = "*" } f.eventFilter = filter // Clear any existing subscription (will be recreated on next read) if f.eventCancel != nil { f.eventCancel() f.eventCancel = nil } f.eventCh = nil f.mu.Unlock() cs.mu.Unlock() return &plan9.Fcall{Type: plan9.Rwrite, Tag: fc.Tag, Count: uint32(len(fc.Data))} } if entry != nil && entry.RdwrMode() { f.readCache = nil } if entry == nil || !entry.RdwrMode() { end := int(fc.Offset) + len(fc.Data) if end > len(f.writeBuf) { grown := make([]byte, end) copy(grown, f.writeBuf) f.writeBuf = grown } copy(f.writeBuf[fc.Offset:], fc.Data) f.mu.Unlock() cs.mu.Unlock() return &plan9.Fcall{Type: plan9.Rwrite, Tag: fc.Tag, Count: uint32(len(fc.Data))} } f.mu.Unlock() cs.mu.Unlock() if err := entry.Write(fc.Data); err != nil { return errFcall(fc, err.Error()) } return &plan9.Fcall{Type: plan9.Rwrite, Tag: fc.Tag, Count: uint32(len(fc.Data))} } func stat(root *fs.Tree, cs *connState, fc *plan9.Fcall) *plan9.Fcall { cs.mu.RLock() f, ok := cs.fids[fc.Fid] cs.mu.RUnlock() if !ok { return errFcall(fc, "bad fid") } f.mu.Lock() path := f.path f.mu.Unlock() if _, err := root.Stat(path[1:]); err != nil { cs.mu.Lock() delete(cs.fids, fc.Fid) cs.mu.Unlock() return errFcall(fc, "file not found") } dir := makeStat(root, path) stat, err := dir.Bytes() if err != nil { return errFcall(fc, err.Error()) } return &plan9.Fcall{Type: plan9.Rstat, Tag: fc.Tag, Stat: stat} } func wstat(root *fs.Tree, cs *connState, fc *plan9.Fcall) *plan9.Fcall { cs.mu.Lock() f, ok := cs.fids[fc.Fid] cs.mu.Unlock() if !ok { return errFcall(fc, "bad fid") } newDir, err := plan9.UnmarshalDir(fc.Stat) if err != nil { return &plan9.Fcall{Type: plan9.Rwstat, Tag: fc.Tag} } f.mu.Lock() path := f.path f.mu.Unlock() oldName := pathBase(path) if newDir.Name == "" || newDir.Name == oldName { return &plan9.Fcall{Type: plan9.Rwstat, Tag: fc.Tag} } if err := root.Rename(path[1:], newDir.Name); err != nil { return errFcall(fc, err.Error()) } // GC stale fids on this connection referencing the old path oldPrefix := path var stale []fs.File cs.mu.Lock() for id, fid := range cs.fids { fid.mu.Lock() fidPath := fid.path entry := fid.entry fid.mu.Unlock() if fidPath == oldPrefix || strings.HasPrefix(fidPath, oldPrefix+"/") { delete(cs.fids, id) if entry != nil { stale = append(stale, entry) } } } cs.mu.Unlock() for _, entry := range stale { _ = entry.Close() } return &plan9.Fcall{Type: plan9.Rwstat, Tag: fc.Tag} } func clunk(root *fs.Tree, cs *connState, fc *plan9.Fcall) *plan9.Fcall { cs.mu.Lock() f, ok := cs.fids[fc.Fid] var path string var data []byte var writable bool var isRequest bool if ok { f.mu.Lock() mode := f.mode path = f.path data = append([]byte(nil), f.writeBuf...) entry := f.entry if mode&3 == plan9.OWRITE || mode&3 == plan9.ORDWR { writable = true if entry != nil { isRequest = entry.RdwrMode() } } // Cancel any event subscription if f.eventCancel != nil { f.eventCancel() } f.mu.Unlock() delete(cs.fids, fc.Fid) } cs.mu.Unlock() if f != nil { f.mu.Lock() entry := f.entry f.mu.Unlock() if entry != nil { _ = entry.Close() } } if writable && !isRequest { input := strings.TrimSpace(string(data)) e, err := root.Open(path[1:]) if err != nil { return errFcall(fc, err.Error()) } if err := e.Write([]byte(input)); err != nil { return errFcall(fc, err.Error()) } } return &plan9.Fcall{Type: plan9.Rclunk, Tag: fc.Tag} } func remove(root *fs.Tree, cs *connState, fc *plan9.Fcall) *plan9.Fcall { cs.mu.Lock() f, ok := cs.fids[fc.Fid] if ok { delete(cs.fids, fc.Fid) } cs.mu.Unlock() var entry fs.File if ok { f.mu.Lock() entry = f.entry path := f.path f.mu.Unlock() if entry != nil { _ = entry.Close() } if err := root.Delete(path[1:]); err != nil { return errFcall(fc, err.Error()) } return &plan9.Fcall{Type: plan9.Rremove, Tag: fc.Tag} } return errFcall(fc, "bad fid") } func (s *Server) Kill() { s.shutdownCancel() session.Shutdown() done := make(chan struct{}) go func() { s.serveWG.Wait() close(done) }() select { case <-done: case <-time.After(3 * time.Second): s.log.Warn("shutdown: timed out waiting for Serve goroutines") } } func buildDirData(root *fs.Tree, path string) []byte { var dirs []plan9.Dir if path == "/" { entries, err := root.List() if err == nil { for _, e := range entries { dirs = append(dirs, makeStat(root, "/"+e.Name())) } } } else { entries, err := root.Readdir(path[1:]) if err == nil { for _, e := range entries { fpath := path + "/" + e.Name() d := makeStat(root, fpath) if info, err := e.Info(); err == nil { d.Atime = uint32(info.ModTime().Unix()) d.Mtime = uint32(info.ModTime().Unix()) } dirs = append(dirs, d) } } } var allData []byte for _, d := range dirs { b, err := d.Bytes() if err != nil { continue } allData = append(allData, b...) } return allData } func sliceDirData(allData []byte, offset uint64, count uint32) []byte { if offset >= uint64(len(allData)) { return nil } remaining := allData[offset:] var result []byte for len(remaining) >= 2 { entrySize := int(remaining[0]) | int(remaining[1])<<8 total := entrySize + 2 if total > len(remaining) { break } if uint32(len(result)+total) > count { break } result = append(result, remaining[:total]...) remaining = remaining[total:] } return result } func makeStat(root *fs.Tree, path string) plan9.Dir { base := pathBase(path) if path == "/" { base = "." } info, err := root.Stat(path[1:]) if err != nil { info = &syntheticFileInfo{Name_: base, Mode_: 0444} } isDir := info.IsDir() qid := plan9.Qid{Path: qidPath(path)} if isDir { qid.Type = QTDir } uid, gid := fileOwnerGroup(root, path) dir := plan9.Dir{ Qid: qid, Mode: plan9.Perm(info.Mode()), Name: base, Uid: uid, Gid: gid, Muid: uid, } if fi, ok := info.(*syntheticFileInfo); ok { dir.Qid.Vers = fi.Vers_ } if !isDir { sz := info.Size() if sz == 0 { sz = 4096 } dir.Length = uint64(sz) } return dir }