1080 lines
25 KiB
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
|
|
}
|