ollie/cmd/olliesrv/server.go

1080 lines
25 KiB
Go

// 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
}