package protocol
import (
"context"
"fmt"
"io"
"log"
"net"
"sync"
)
// Server is a 9P file server
type Server struct {
root Dir
debug bool
mu sync.Mutex
clients map[net.Conn]*clientState
}
// clientState tracks state for a single client connection
type clientState struct {
fids map[uint32]File
msize uint32
}
// NewServer creates a new 9P server with the given root directory
func NewServer(root Dir) *Server {
return &Server{
root: root,
clients: make(map[net.Conn]*clientState),
}
}
// SetDebug enables debug logging
func (s *Server) SetDebug(debug bool) {
s.debug = debug
}
// Serve handles incoming connections on the listener
func (s *Server) Serve(ctx context.Context, listener net.Listener) error {
for {
conn, err := listener.Accept()
if err != nil {
select {
case <-ctx.Done():
return ctx.Err()
default:
log.Printf("accept error: %v", err)
continue
}
}
go s.handleConn(conn)
}
}
// ServeConn handles a single connection (useful for testing)
func (s *Server) ServeConn(conn net.Conn) {
s.handleConn(conn)
}
func (s *Server) handleConn(conn net.Conn) {
defer conn.Close()
state := &clientState{
fids: make(map[uint32]File),
msize: MaxMessageSize,
}
s.mu.Lock()
s.clients[conn] = state
s.mu.Unlock()
defer func() {
// Close all fids that were not explicitly clunked before disconnect.
// This triggers sessionRefFile/sessionRefDir.Close() which decrements
// session reference counts, allowing sessions to be freed when the
// last client using them disconnects.
for _, file := range state.fids {
file.Close() //nolint:errcheck
}
s.mu.Lock()
delete(s.clients, conn)
s.mu.Unlock()
}()
dec := NewDecoder(conn)
enc := NewEncoder(conn)
buf := make([]byte, MaxMessageSize)
for {
msgType, tag, payload, err := dec.ReadMessage()
if err != nil {
if err != io.EOF {
log.Printf("read error: %v", err)
}
return
}
if s.debug {
log.Printf("< %s tag=%d len=%d", MessageName(msgType), tag, len(payload))
}
resp, respType := s.handleMessage(state, msgType, payload, buf)
if s.debug {
log.Printf("> %s tag=%d len=%d", MessageName(respType), tag, len(resp))
}
if err := enc.WriteMessage(respType, tag, resp); err != nil {
log.Printf("write error: %v", err)
return
}
}
}
func (s *Server) handleMessage(state *clientState, msgType uint8, payload []byte, buf []byte) ([]byte, uint8) {
switch msgType {
case Tversion:
return s.handleVersion(state, payload, buf)
case Tattach:
return s.handleAttach(state, payload, buf)
case Twalk:
return s.handleWalk(state, payload, buf)
case Topen:
return s.handleOpen(state, payload, buf)
case Tread:
return s.handleRead(state, payload, buf)
case Twrite:
return s.handleWrite(state, payload, buf)
case Tclunk:
return s.handleClunk(state, payload, buf)
case Tstat:
return s.handleStat(state, payload, buf)
case Tflush:
return s.handleFlush(state, payload, buf)
default:
return s.errorResponse(buf, fmt.Sprintf("unknown message type: %d", msgType))
}
}
func (s *Server) errorResponse(buf []byte, msg string) ([]byte, uint8) {
resp := &RerrorMsg{Ename: msg}
n := resp.Encode(buf)
return buf[:n], Rerror
}
func (s *Server) handleVersion(state *clientState, payload []byte, buf []byte) ([]byte, uint8) {
msg, err := DecodeTversion(payload)
if err != nil {
return s.errorResponse(buf, err.Error())
}
// Negotiate message size
msize := msg.Msize
if msize > MaxMessageSize {
msize = MaxMessageSize
}
state.msize = msize
// Check version - accept both 9P2000 and Styx (Inferno's name)
version := msg.Version
if msg.Version != Version && msg.Version != "Styx" {
version = "unknown"
}
if s.debug {
log.Printf("Version negotiation: client=%q responding=%q msize=%d", msg.Version, version, msize)
}
resp := &RversionMsg{Msize: msize, Version: version}
n := resp.Encode(buf)
return buf[:n], Rversion
}
func (s *Server) handleAttach(state *clientState, payload []byte, buf []byte) ([]byte, uint8) {
msg, err := DecodeTattach(payload)
if err != nil {
return s.errorResponse(buf, err.Error())
}
if _, exists := state.fids[msg.Fid]; exists {
return s.errorResponse(buf, ErrFidInUse.Error())
}
state.fids[msg.Fid] = s.root
resp := &RattachMsg{Qid: s.root.Stat().Qid}
n := resp.Encode(buf)
return buf[:n], Rattach
}
func (s *Server) handleWalk(state *clientState, payload []byte, buf []byte) ([]byte, uint8) {
msg, err := DecodeTwalk(payload)
if err != nil {
return s.errorResponse(buf, err.Error())
}
file, exists := state.fids[msg.Fid]
if !exists {
return s.errorResponse(buf, ErrBadFid.Error())
}
if msg.Fid != msg.Newfid {
if _, exists := state.fids[msg.Newfid]; exists {
return s.errorResponse(buf, ErrFidInUse.Error())
}
}
// Walk the path
qids := make([]Qid, 0, len(msg.Names))
current := file
for _, name := range msg.Names {
dir, ok := current.(Dir)
if !ok {
if s.debug {
log.Printf(" walk: %q is not a directory", name)
}
return s.errorResponse(buf, ErrNotDir.Error())
}
next, err := dir.Lookup(name)
if err != nil {
if s.debug {
log.Printf(" walk: lookup %q failed: %v (walked %d/%d)", name, err, len(qids), len(msg.Names))
}
// Return partial walk
break
}
if s.debug {
log.Printf(" walk: %q -> %T", name, next)
}
qids = append(qids, next.Stat().Qid)
current = next
}
// Only update fid if we walked at least one element (or no elements requested)
if len(qids) == len(msg.Names) {
state.fids[msg.Newfid] = current
} else if s.debug {
log.Printf(" walk: partial walk %d/%d names succeeded; newfid %d NOT registered", len(qids), len(msg.Names), msg.Newfid)
}
resp := &RwalkMsg{Qids: qids}
n := resp.Encode(buf)
return buf[:n], Rwalk
}
func (s *Server) handleOpen(state *clientState, payload []byte, buf []byte) ([]byte, uint8) {
msg, err := DecodeTopen(payload)
if err != nil {
return s.errorResponse(buf, err.Error())
}
file, exists := state.fids[msg.Fid]
if !exists {
return s.errorResponse(buf, ErrBadFid.Error())
}
if err := file.Open(msg.Mode); err != nil {
return s.errorResponse(buf, err.Error())
}
resp := &RopenMsg{
Qid: file.Stat().Qid,
Iounit: 0, // 0 means use msize - overhead
}
n := resp.Encode(buf)
return buf[:n], Ropen
}
func (s *Server) handleRead(state *clientState, payload []byte, buf []byte) ([]byte, uint8) {
msg, err := DecodeTread(payload)
if err != nil {
return s.errorResponse(buf, err.Error())
}
file, exists := state.fids[msg.Fid]
if !exists {
return s.errorResponse(buf, ErrBadFid.Error())
}
// Limit read size to available buffer
count := msg.Count
maxData := state.msize - 4 - 1 - 2 - 4 // size, type, tag, count
if count > maxData {
count = maxData
}
data := make([]byte, count)
n, err := file.Read(data, int64(msg.Offset))
if err != nil && err != io.EOF {
return s.errorResponse(buf, err.Error())
}
resp := &RreadMsg{Data: data[:n]}
rn := resp.Encode(buf)
return buf[:rn], Rread
}
func (s *Server) handleWrite(state *clientState, payload []byte, buf []byte) ([]byte, uint8) {
msg, err := DecodeTwrite(payload)
if err != nil {
return s.errorResponse(buf, err.Error())
}
file, exists := state.fids[msg.Fid]
if !exists {
return s.errorResponse(buf, ErrBadFid.Error())
}
n, err := file.Write(msg.Data, int64(msg.Offset))
if err != nil {
return s.errorResponse(buf, err.Error())
}
resp := &RwriteMsg{Count: uint32(n)}
rn := resp.Encode(buf)
return buf[:rn], Rwrite
}
func (s *Server) handleClunk(state *clientState, payload []byte, buf []byte) ([]byte, uint8) {
msg, err := DecodeTclunk(payload)
if err != nil {
return s.errorResponse(buf, err.Error())
}
file, exists := state.fids[msg.Fid]
if !exists {
return s.errorResponse(buf, ErrBadFid.Error())
}
file.Close()
delete(state.fids, msg.Fid)
resp := &RclunkMsg{}
n := resp.Encode(buf)
return buf[:n], Rclunk
}
func (s *Server) handleStat(state *clientState, payload []byte, buf []byte) ([]byte, uint8) {
msg, err := DecodeTstat(payload)
if err != nil {
return s.errorResponse(buf, err.Error())
}
file, exists := state.fids[msg.Fid]
if !exists {
return s.errorResponse(buf, ErrBadFid.Error())
}
resp := &RstatMsg{Stat: file.Stat()}
n := resp.Encode(buf)
return buf[:n], Rstat
}
func (s *Server) handleFlush(state *clientState, payload []byte, buf []byte) ([]byte, uint8) {
// We don't have async operations to cancel, so just respond OK
resp := &RflushMsg{}
n := resp.Encode(buf)
return buf[:n], Rflush
}