Documentation
¶
Overview ¶
Package client implements a 9P2000.L/9P2000.u wire-level client over any net.Conn. The primary surface is the Conn type, a multiplexed connection that dispatches responses by tag so callers can issue concurrent requests without external synchronization.
Concurrency Model ¶
Conn is safe for concurrent use by multiple goroutines, modeled on database/sql.DB. A single read goroutine per Conn decodes R-messages and delivers each to the waiting caller via a per-tag response channel. Writes are serialized with a mutex around internal/wire.WriteFramesLocked. Natural back-pressure on the bounded tag free-list (see WithMaxInflight) caps the in-flight request count without a separate semaphore.
Authentication Scope ¶
This package supports [Tattach] with afid=NoFid only. The Tauth/afid handshake is not implemented — the common case (Q, Linux v9fs default trans=tcp) is no-auth, and every concrete consumer known at v1.3.0 falls in that bucket. Future milestones may add Tauth if a concrete consumer requires it.
Dialects: .L Primary, .u Best-Effort ¶
9P2000.L is the primary dialect and has full feature parity — every operation in the client (attach, walk, open, read, write, clunk, flush) and the advanced operations (symlinks, xattr, locks, statfs, rename) are implemented for .L.
9P2000.u is best-effort. The operations with a .u equivalent (Twalk, Tread, Twrite, Tclunk, Tcreate, Tstat, Twstat, Tremove, Tflush, Tversion, Tattach) work against a .u-negotiated Conn. The .L-only operations (Tgetattr, Tsetattr, Tlopen, Tlcreate, Txattrwalk/Txattrcreate, Tlock/Tgetlock, Treadlink, Tmknod, Tsymlink, Tlink, Trename, Trenameat, Tunlinkat, Tstatfs) return ErrNotSupported on a .u-negotiated Conn.
The dialect is chosen by auto-detect: the Conn proposes 9P2000.L and downgrades to 9P2000.u if the server's Rversion carries that string.
Default msize ¶
The default proposed msize is 1 MiB (1 << 20). This matches the Linux kernel's v9fs client default so that `mount -t 9p -o trans=tcp` against a non-ninep server does not silently downsize to a mismatched message size. Override with WithMsize. The server's Rversion msize caps the proposal; the negotiated msize is the minimum of the two.
Note that the ninep server's default maximum msize is 4 MiB — the asymmetry is intentional. Server-to-server callers (e.g. ninep→ninep local) can bump with WithMsize if profiling shows a win.
Errors ¶
9P error responses from the server are surfaced as a *Error value that wraps a proto.Errno. Both Rlerror (9P2000.L) and Rerror (9P2000.u) decode to this same type; callers match with the idiomatic Go pattern:
if errors.Is(err, proto.EACCES) {
// ...
}
Use proto.Errno constants rather than syscall.Errno for portability — the proto↔syscall bridge is platform-specific (see Error.Is godoc).
File Handle ¶
The File type is the primary high-level API for 9P file operations. It implements io.Reader, io.Writer, io.Closer, io.Seeker, io.ReaderAt, and io.WriterAt — so any Go package that consumes those interfaces (io.Copy, bufio, encoding/json, compress/gzip, net/http Body) can read from and write to 9P files directly without adapter code.
Obtain a *File via Conn.Attach (root of the filesystem), Conn.OpenFile (open by path), Conn.Create (create and open), Conn.OpenDir (open directory for enumeration via File.ReadDir), File.Walk (navigate to a child without opening), or File.Clone (duplicate at the same server-side node for parallel I/O). Every *File must be closed via File.Close; second calls to Close are a no-op returning nil (the intentional deviation from os.File semantics is documented on File.Close).
Fid lifecycle is managed implicitly. Callers never see proto.Fid values on the high-level path — Conn.Attach allocates the root fid, Conn.OpenFile allocates per-open fids, File.Clone allocates clone fids, and File.Close releases them on clunk.
Seek semantics ¶
File.Seek is a pure client-side arithmetic operation. 9P's Tread/Twrite carry the offset on every request, so there is no server-side seek state. SeekStart and SeekCurrent never touch the wire. SeekEnd uses a cached size field populated by File.Sync, which issues Tgetattr on .L (or Tstat on .u) to refresh. Callers that need SeekEnd relative to fresh size after concurrent writes invoke Sync first; SeekEnd on a file whose size has not been cached returns 0 for SeekEnd(0) and an error guiding the caller to Sync for any negative offset.
Concurrency and parallelism ¶
Each *File has a private mutex that serializes Read, Write, ReadAt, and WriteAt calls on the same handle. Callers wanting parallel I/O on the same server-side file spawn a File.Clone per goroutine; each clone has its own fid, its own offset, and its own mutex. The underlying Conn is goroutine-safe per database/sql.DB semantics, so N clones can issue N in-flight requests that overlap on the server.
Advanced Operations ¶
Phase 21 adds the 9P operations beyond read/write/walk/open/clunk: symbolic links, extended attributes, POSIX locks, filesystem statistics, and path manipulation (rename / remove / link / mknod / setattr).
## Symbolic Links
- Conn.Symlink — create a symlink at a path. (.L-only)
- File.Readlink — read a symlink's target. (.L-only)
## Extended Attributes (.L-only)
- File.XattrGet / File.XattrSet / File.XattrList / File.XattrRemove
- All four hide the 9P two-phase protocol (Txattrwalk → Tread-loop → Tclunk for get; Clone + Txattrcreate + Twrite + Tclunk for set).
- Callers needing streaming access use Raw.Txattrwalk directly.
## POSIX Locks (.L-only)
- File.Lock / File.Unlock / File.TryLock / File.GetLock
- Blocking Lock uses exponential backoff (10ms → 500ms cap); tests can override via WithLockPollSchedule.
- Ctx cancellation unconditionally releases any granted lock via a background-ctx Tlock(UNLCK) — belt-and-braces against the cancel-during-grant race.
## Filesystem Statistics
- File.Stat — dialect-neutral metadata (p9u.Stat on both .L and .u). On .L, internally uses Tgetattr; on .u, uses Tstat.
- File.Getattr — rich .L-specific proto.Attr (includes NLink, Blocks, BTime, Gen, DataVersion dropped by Stat). (.L-only)
- File.Setattr — write metadata (chmod/chown/truncate via proto.SetAttr.Valid bitmask). (.L-only)
- File.Statfs — filesystem-level stats (by value, not pointer). (.L-only)
- File.Sync — refresh the File's cachedSize from the server (Tgetattr on .L, Tstat on .u); backs Seek(SeekEnd) after concurrent writes.
## Path Manipulation
- Conn.Rename — rename across directories. (.L uses Trenameat; .u returns ErrNotSupported.)
- Conn.Remove — remove file or directory (auto-detects QTDIR for Tunlinkat's AT_REMOVEDIR flag on .L). (.L-only)
- Conn.Link — create a hard link. (.L-only)
- Conn.Mknod — create a device / fifo / socket node. (.L-only)
## Dialect Compatibility
Methods marked .L-only return a wrapped ErrNotSupported on a 9P2000.u-negotiated Conn. Use errors.Is to discriminate. The single .u-only Raw primitive is Raw.Tstat — File.Stat dispatches on dialect so callers never see this asymmetry.
## Not Supported
The Tauth afid handshake is not implemented. Conn.Attach always passes NoFid; authentication must be handled at the transport layer (TLS, SSH). See the "Authentication Scope" section above.
Chmod / Chown / Truncate convenience helpers are deferred — callers invoke File.Setattr with the appropriate proto.SetAttr.Valid bitmask. A future ergonomic pass may add wrappers if consumer demand surfaces.
Twstat is intentionally unexposed on the high-level surface. .u callers that need path-rename or metadata-write semantics compose Raw primitives directly.
Raw Sub-Surface ¶
The Raw type returned by Conn.Raw exposes direct 9P wire operations with explicit fid arguments — Raw.Read, Raw.Write, Raw.Walk, Raw.Clunk, Raw.Flush, Raw.Lopen, Raw.Lcreate, Raw.Open, Raw.Create, Raw.Attach. Plus Raw.AcquireFid and Raw.ReleaseFid integrate with the Conn's fid allocator for callers doing fully-explicit lifecycle management.
Raw is the escape hatch for callers that need to pipeline T-messages manually, track fids in a parallel data structure, or port an existing 9P client that expects wire-level primitives. The high-level File surface handles offset tracking, fid lifecycle, and io.* interface conformance — use it for typical read/write workloads and fall through to Raw only when the high-level shape does not fit.
SEED-001 Resolution ¶
The v1.3.0 client API design resolved SEED-001 (see .planning/seeds/) as a sync-primary + async-escape shape. File is a synchronous, io.*-composable handle; Raw is the async/pipeline- friendly escape hatch. This departs from hugelgupf/p9 (ReadAt / WriteAt only, no io.Reader) and docker/go-p9p (raw T/R only, no File) — the motivation is composition with the Go standard I/O ecosystem. Callers that need pipelined writes (e.g. 128 KiB chunks issued in parallel) use Raw over the goroutine-safe Conn.
Example ¶
package main
import (
"context"
"fmt"
"io"
"log"
"net"
"os"
"github.com/dotwaffle/ninep/client"
"github.com/dotwaffle/ninep/server"
"github.com/dotwaffle/ninep/server/memfs"
)
func main() {
// Setup a memory filesystem server for the example.
gen := new(server.QIDGenerator)
root := memfs.NewDir(gen).
AddFile("hello.txt", []byte("hello, 9p!"))
srv := server.New(root)
l, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
log.Fatal(err)
}
defer func() { _ = l.Close() }()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
go func() {
if err := srv.Serve(ctx, l); err != nil {
// server closed
}
}()
// 1. Dial the server.
nc, err := net.Dial("tcp", l.Addr().String())
if err != nil {
log.Fatal(err)
}
defer func() { _ = nc.Close() }()
c, err := client.Dial(ctx, nc)
if err != nil {
log.Fatal(err)
}
defer func() { _ = c.Close() }()
// 2. Attach to the root.
f, err := c.Attach(ctx, "nobody", "")
if err != nil {
log.Fatal(err)
}
defer func() { _ = f.Close() }()
// 3. Open the file.
hello, err := c.OpenFile(ctx, "hello.txt", os.O_RDONLY, 0)
if err != nil {
log.Fatal(err)
}
defer func() { _ = hello.Close() }()
// 4. Read the file.
data, err := io.ReadAll(hello)
if err != nil {
log.Fatal(err)
}
fmt.Printf("%s\n", data)
}
Output: hello, 9p!
Example (ConcurrentAccess) ¶
Example_concurrentAccess demonstrates parallel reads from a single file via client.File.Clone. Each clone has its own fid and independent offset, so the ReadAt calls do not contend on the shared per-File mutex.
In tests, prefer clienttest.MemfsPair(tb, build, opts...) — this example hand-rolls the harness because godoc Example functions take no testing.TB. See TestExample_ConcurrentAccess_ViaClienttest for the idiomatic in-test shape.
package main
import (
"context"
"fmt"
"net"
"os"
"sync"
"time"
"github.com/dotwaffle/ninep/client"
"github.com/dotwaffle/ninep/server"
"github.com/dotwaffle/ninep/server/memfs"
)
// exampleHarness boots a memfs server over net.Pipe and returns a
// dialed *client.Conn plus a cleanup closure. Self-contained
// alternative to [clienttest.MemfsPair] for use inside godoc Example
// functions, which cannot accept a [*testing.T].
//
// In tests, prefer clienttest.MemfsPair(tb, build, opts...) — it
// applies the same net.Pipe + memfs + server.New + client.Dial boot
// sequence but registers cleanup via tb.Cleanup and fails the test
// loudly on contract violations. See the TestExample_*_ViaClienttest
// trio below for the idiomatic in-test shape.
//
// Returns (nil, nil) on any boot error so example bodies can early-
// return without producing output — any deviation from the //Output:
// assertion fails the example, which is the desired behaviour.
func exampleHarness(build func(root *memfs.MemDir)) (*client.Conn, func()) {
cliNC, srvNC := net.Pipe()
gen := &server.QIDGenerator{}
root := memfs.NewDir(gen)
if build != nil {
build(root)
}
srv := server.New(root, server.WithMaxMsize(65536))
srvCtx, srvCancel := context.WithCancel(context.Background())
srvDone := make(chan struct{})
go func() {
defer close(srvDone)
srv.ServeConn(srvCtx, srvNC)
}()
dialCtx, dialCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer dialCancel()
conn, err := client.Dial(dialCtx, cliNC, client.WithMsize(65536))
if err != nil || conn == nil {
_ = cliNC.Close()
srvCancel()
<-srvDone
return nil, nil
}
return conn, func() {
_ = conn.Close()
srvCancel()
_ = srvNC.Close()
<-srvDone
}
}
func main() {
conn, cleanup := exampleHarness(func(root *memfs.MemDir) {
root.AddFile("big.bin", make([]byte, 4096))
})
if conn == nil {
return
}
defer cleanup()
if _, err := conn.Attach(context.Background(), "example", ""); err != nil {
fmt.Println("attach:", err)
return
}
f, err := conn.OpenFile(context.Background(), "big.bin", os.O_RDONLY, 0)
if err != nil {
fmt.Println("open:", err)
return
}
defer func() { _ = f.Close() }()
const parallel = 4
var wg sync.WaitGroup
for i := range parallel {
clone, err := f.Clone(context.Background())
if err != nil {
continue
}
wg.Add(1)
go func(i int, c *client.File) {
defer wg.Done()
defer func() { _ = c.Close() }()
buf := make([]byte, 16)
_, _ = c.ReadAt(buf, int64(i)*16)
}(i, clone)
}
wg.Wait()
fmt.Println("4 clones completed")
}
Output: 4 clones completed
Example (ReadFile) ¶
Example_readFile demonstrates reading a file's contents by path via the high-level client.Conn.OpenFile + io.ReadAll idiom.
In tests, prefer clienttest.MemfsPair(tb, build, opts...) — this example hand-rolls the harness because godoc Example functions take no testing.TB. See TestExample_ReadFile_ViaClienttest for the idiomatic in-test shape.
package main
import (
"context"
"fmt"
"io"
"net"
"os"
"time"
"github.com/dotwaffle/ninep/client"
"github.com/dotwaffle/ninep/server"
"github.com/dotwaffle/ninep/server/memfs"
)
// exampleHarness boots a memfs server over net.Pipe and returns a
// dialed *client.Conn plus a cleanup closure. Self-contained
// alternative to [clienttest.MemfsPair] for use inside godoc Example
// functions, which cannot accept a [*testing.T].
//
// In tests, prefer clienttest.MemfsPair(tb, build, opts...) — it
// applies the same net.Pipe + memfs + server.New + client.Dial boot
// sequence but registers cleanup via tb.Cleanup and fails the test
// loudly on contract violations. See the TestExample_*_ViaClienttest
// trio below for the idiomatic in-test shape.
//
// Returns (nil, nil) on any boot error so example bodies can early-
// return without producing output — any deviation from the //Output:
// assertion fails the example, which is the desired behaviour.
func exampleHarness(build func(root *memfs.MemDir)) (*client.Conn, func()) {
cliNC, srvNC := net.Pipe()
gen := &server.QIDGenerator{}
root := memfs.NewDir(gen)
if build != nil {
build(root)
}
srv := server.New(root, server.WithMaxMsize(65536))
srvCtx, srvCancel := context.WithCancel(context.Background())
srvDone := make(chan struct{})
go func() {
defer close(srvDone)
srv.ServeConn(srvCtx, srvNC)
}()
dialCtx, dialCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer dialCancel()
conn, err := client.Dial(dialCtx, cliNC, client.WithMsize(65536))
if err != nil || conn == nil {
_ = cliNC.Close()
srvCancel()
<-srvDone
return nil, nil
}
return conn, func() {
_ = conn.Close()
srvCancel()
_ = srvNC.Close()
<-srvDone
}
}
func main() {
conn, cleanup := exampleHarness(func(root *memfs.MemDir) {
root.AddStaticFile("hello.txt", "hello world\n")
})
if conn == nil {
return
}
defer cleanup()
if _, err := conn.Attach(context.Background(), "example", ""); err != nil {
fmt.Println("attach:", err)
return
}
f, err := conn.OpenFile(context.Background(), "hello.txt", os.O_RDONLY, 0)
if err != nil {
fmt.Println("open:", err)
return
}
defer func() { _ = f.Close() }()
data, err := io.ReadAll(f)
if err != nil {
fmt.Println("read:", err)
return
}
fmt.Printf("%s", data)
}
Output: hello world
Example (WriteFile) ¶
Example_writeFile demonstrates creating and writing a new file via client.Conn.Create + client.File.Write.
In tests, prefer clienttest.MemfsPair(tb, build, opts...) — this example hand-rolls the harness because godoc Example functions take no testing.TB. See TestExample_WriteFile_ViaClienttest for the idiomatic in-test shape.
package main
import (
"context"
"fmt"
"net"
"os"
"time"
"github.com/dotwaffle/ninep/client"
"github.com/dotwaffle/ninep/server"
"github.com/dotwaffle/ninep/server/memfs"
)
// exampleHarness boots a memfs server over net.Pipe and returns a
// dialed *client.Conn plus a cleanup closure. Self-contained
// alternative to [clienttest.MemfsPair] for use inside godoc Example
// functions, which cannot accept a [*testing.T].
//
// In tests, prefer clienttest.MemfsPair(tb, build, opts...) — it
// applies the same net.Pipe + memfs + server.New + client.Dial boot
// sequence but registers cleanup via tb.Cleanup and fails the test
// loudly on contract violations. See the TestExample_*_ViaClienttest
// trio below for the idiomatic in-test shape.
//
// Returns (nil, nil) on any boot error so example bodies can early-
// return without producing output — any deviation from the //Output:
// assertion fails the example, which is the desired behaviour.
func exampleHarness(build func(root *memfs.MemDir)) (*client.Conn, func()) {
cliNC, srvNC := net.Pipe()
gen := &server.QIDGenerator{}
root := memfs.NewDir(gen)
if build != nil {
build(root)
}
srv := server.New(root, server.WithMaxMsize(65536))
srvCtx, srvCancel := context.WithCancel(context.Background())
srvDone := make(chan struct{})
go func() {
defer close(srvDone)
srv.ServeConn(srvCtx, srvNC)
}()
dialCtx, dialCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer dialCancel()
conn, err := client.Dial(dialCtx, cliNC, client.WithMsize(65536))
if err != nil || conn == nil {
_ = cliNC.Close()
srvCancel()
<-srvDone
return nil, nil
}
return conn, func() {
_ = conn.Close()
srvCancel()
_ = srvNC.Close()
<-srvDone
}
}
func main() {
conn, cleanup := exampleHarness(nil)
if conn == nil {
return
}
defer cleanup()
if _, err := conn.Attach(context.Background(), "example", ""); err != nil {
fmt.Println("attach:", err)
return
}
f, err := conn.Create(context.Background(), "new.txt", os.O_WRONLY, 0o644)
if err != nil {
fmt.Println("create:", err)
return
}
defer func() { _ = f.Close() }()
n, err := f.Write([]byte("hello, 9P\n"))
if err != nil {
fmt.Println("write:", err)
return
}
fmt.Println("wrote", n, "bytes")
}
Output: wrote 10 bytes
Index ¶
- Variables
- func BackoffFor(schedule []time.Duration, i int) time.Duration
- func DefaultLockBackoff() []time.Duration
- type Conn
- func (c *Conn) Attach(ctx context.Context, uname, aname string) (*File, error)
- func (c *Conn) AttachFid(ctx context.Context, fid proto.Fid, uname, aname string) (proto.QID, error)
- func (c *Conn) Close() error
- func (c *Conn) Clunk(ctx context.Context, fid proto.Fid) error
- func (c *Conn) Create(ctx context.Context, p string, flags int, mode os.FileMode) (*File, error)
- func (c *Conn) CreateFid(ctx context.Context, fid proto.Fid, name string, perm proto.FileMode, ...) (proto.QID, uint32, error)
- func (c *Conn) Dialect() string
- func (c *Conn) Flush(ctx context.Context, oldTag proto.Tag) error
- func (c *Conn) Lcreate(ctx context.Context, fid proto.Fid, name string, flags uint32, ...) (proto.QID, uint32, error)
- func (c *Conn) Link(ctx context.Context, existingPath, newPath string) error
- func (c *Conn) Lopen(ctx context.Context, fid proto.Fid, flags uint32) (proto.QID, uint32, error)
- func (c *Conn) Mknod(ctx context.Context, parentPath, name string, mode proto.FileMode, ...) (*File, error)
- func (c *Conn) Msize() uint32
- func (c *Conn) Open(ctx context.Context, fid proto.Fid, mode uint8) (proto.QID, uint32, error)
- func (c *Conn) OpenDir(ctx context.Context, p string) (*File, error)
- func (c *Conn) OpenFile(ctx context.Context, p string, flags int, mode os.FileMode) (*File, error)
- func (c *Conn) Raw() *Raw
- func (c *Conn) Read(ctx context.Context, fid proto.Fid, offset uint64, count uint32) ([]byte, error)
- func (c *Conn) Remove(ctx context.Context, p string) error
- func (c *Conn) Rename(ctx context.Context, fromPath, toPath string) error
- func (c *Conn) Root() *File
- func (c *Conn) Shutdown(ctx context.Context) error
- func (c *Conn) Symlink(ctx context.Context, linkPath, target string) (*File, error)
- func (c *Conn) Walk(ctx context.Context, fid, newFid proto.Fid, names []string) ([]proto.QID, error)
- func (c *Conn) Write(ctx context.Context, fid proto.Fid, offset uint64, data []byte) (uint32, error)
- type Error
- type File
- func (f *File) Chmod(ctx context.Context, mode os.FileMode) error
- func (f *File) Chown(ctx context.Context, uid, gid uint32) error
- func (f *File) Clone(ctx context.Context) (*File, error)
- func (f *File) Close() error
- func (f *File) Fid() proto.Fid
- func (f *File) GetLock(ctx context.Context, lt LockType) (*Lock, error)
- func (f *File) Getattr(ctx context.Context, mask proto.AttrMask) (proto.Attr, error)
- func (f *File) Lock(ctx context.Context, lt LockType) error
- func (f *File) Qid() proto.QID
- func (f *File) Read(p []byte) (int, error)
- func (f *File) ReadAt(p []byte, off int64) (int, error)
- func (f *File) ReadAtCtx(ctx context.Context, p []byte, off int64) (int, error)
- func (f *File) ReadCtx(ctx context.Context, p []byte) (int, error)
- func (f *File) ReadDir(n int) ([]os.DirEntry, error)
- func (f *File) Readlink(ctx context.Context) (string, error)
- func (f *File) Seek(offset int64, whence int) (int64, error)
- func (f *File) Setattr(ctx context.Context, attr proto.SetAttr) error
- func (f *File) Stat(ctx context.Context) (p9u.Stat, error)
- func (f *File) Statfs(ctx context.Context) (proto.FSStat, error)
- func (f *File) Sync() error
- func (f *File) Truncate(ctx context.Context, size uint64) error
- func (f *File) TryLock(ctx context.Context, lt LockType) (bool, error)
- func (f *File) Unlock(ctx context.Context) error
- func (f *File) Walk(ctx context.Context, names []string) (*File, error)
- func (f *File) Write(p []byte) (int, error)
- func (f *File) WriteAt(p []byte, off int64) (int, error)
- func (f *File) WriteAtCtx(ctx context.Context, p []byte, off int64) (int, error)
- func (f *File) WriteCtx(ctx context.Context, p []byte) (int, error)
- func (f *File) XattrGet(ctx context.Context, name string) ([]byte, error)
- func (f *File) XattrList(ctx context.Context) ([]string, error)
- func (f *File) XattrRemove(ctx context.Context, name string) error
- func (f *File) XattrSet(ctx context.Context, name string, data []byte, flags uint32) error
- type Lock
- type LockType
- type Option
- func WithLockPollSchedule(schedule []time.Duration) Option
- func WithLogger(logger *slog.Logger) Option
- func WithMaxInflight(n int) Option
- func WithMeter(mp metric.MeterProvider) Option
- func WithMsize(n uint32) Option
- func WithRequestTimeout(d time.Duration) Option
- func WithTracer(tp trace.TracerProvider) Option
- func WithVersion(v proto.Version) Option
- type Raw
- func (r *Raw) AcquireFid() (proto.Fid, error)
- func (r *Raw) Attach(ctx context.Context, fid proto.Fid, uname, aname string) (proto.QID, error)
- func (r *Raw) Clunk(ctx context.Context, fid proto.Fid) error
- func (r *Raw) Create(ctx context.Context, fid proto.Fid, name string, perm proto.FileMode, ...) (proto.QID, uint32, error)
- func (r *Raw) Flush(ctx context.Context, oldTag proto.Tag) error
- func (r *Raw) Lcreate(ctx context.Context, fid proto.Fid, name string, flags uint32, ...) (proto.QID, uint32, error)
- func (r *Raw) Lopen(ctx context.Context, fid proto.Fid, flags uint32) (proto.QID, uint32, error)
- func (r *Raw) Open(ctx context.Context, fid proto.Fid, mode uint8) (proto.QID, uint32, error)
- func (r *Raw) Read(ctx context.Context, fid proto.Fid, offset uint64, count uint32) ([]byte, error)
- func (r *Raw) ReleaseFid(fid proto.Fid)
- func (r *Raw) Tgetattr(ctx context.Context, fid proto.Fid, mask proto.AttrMask) (proto.Attr, error)
- func (r *Raw) Tgetlock(ctx context.Context, fid proto.Fid, lt proto.LockType, start, length uint64, ...) (p9l.Rgetlock, error)
- func (r *Raw) Tlink(ctx context.Context, dfid, fid proto.Fid, name string) error
- func (r *Raw) Tlock(ctx context.Context, fid proto.Fid, lt proto.LockType, flags proto.LockFlags, ...) (proto.LockStatus, error)
- func (r *Raw) Tmknod(ctx context.Context, dfid proto.Fid, name string, ...) (proto.QID, error)
- func (r *Raw) Treadlink(ctx context.Context, fid proto.Fid) (string, error)
- func (r *Raw) Tremove(ctx context.Context, fid proto.Fid) error
- func (r *Raw) Trename(ctx context.Context, fid, dfid proto.Fid, name string) error
- func (r *Raw) Trenameat(ctx context.Context, oldDirFid proto.Fid, oldName string, newDirFid proto.Fid, ...) error
- func (r *Raw) Tsetattr(ctx context.Context, fid proto.Fid, attr proto.SetAttr) error
- func (r *Raw) Tstat(ctx context.Context, fid proto.Fid) (p9u.Stat, error)
- func (r *Raw) Tstatfs(ctx context.Context, fid proto.Fid) (proto.FSStat, error)
- func (r *Raw) Tsymlink(ctx context.Context, dfid proto.Fid, name, target string, gid uint32) (proto.QID, error)
- func (r *Raw) Tunlinkat(ctx context.Context, dirFid proto.Fid, name string, flags uint32) error
- func (r *Raw) Txattrcreate(ctx context.Context, fid proto.Fid, name string, attrSize uint64, flags uint32) error
- func (r *Raw) Txattrwalk(ctx context.Context, fid, newFid proto.Fid, name string) (uint64, error)
- func (r *Raw) Walk(ctx context.Context, fid, newFid proto.Fid, names []string) ([]proto.QID, error)
- func (r *Raw) Write(ctx context.Context, fid proto.Fid, offset uint64, data []byte) (uint32, error)
- type Session
- type SessionOption
Examples ¶
Constants ¶
This section is empty.
Variables ¶
var ( // ErrClosed is returned when a request is made against, or blocked on, // a Conn that has been Closed or whose underlying net.Conn returned an // I/O error that caused shutdown. ErrClosed = errors.New("client: connection closed") // ErrFlushed is returned as part of the error chain when a request's // context is cancelled mid-flight and the server's Rflush arrives // BEFORE the original response — signaling that the server // acknowledged the Tflush and aborted the original request (D-05, // D-08). The chain is constructed via // // fmt.Errorf("9p: flushed tag %d: %w", oldTag, // errors.Join(ctx.Err(), ErrFlushed)) // // so callers can discriminate: // // errors.Is(err, context.Canceled) — "I cancelled" // errors.Is(err, context.DeadlineExceeded) — "deadline expired" // errors.Is(err, ErrFlushed) — "server saw my Tflush and Rflushed" // errors.Is(err, ErrClosed) — "connection died mid-request" // // Per D-11, ErrFlushed is distinct from ErrClosed — operationally // meaningful to tell "server acked my flush" from "connection is gone". // If the ORIGINAL R arrives before the Rflush (R-first path, D-05), // ErrFlushed is NOT in the chain — only ctx.Err() is wrapped. Callers // that want "was it flushed?" match on ErrFlushed; callers that want // "did caller cancel or deadline expire?" match on context.Canceled / // context.DeadlineExceeded. ErrFlushed = errors.New("client: request flushed") // ErrNotSupported is returned when a method is called on a Conn whose // negotiated dialect does not support the operation — e.g. a // 9P2000.L-only method invoked on a .u-negotiated Conn (see package // doc for the full .L-only list). This sentinel is distinct from // [proto.ENOTSUP] (errno 95); dialect-guard checks compare against // ErrNotSupported, server-returned errno responses compare against // proto.ENOTSUP. ErrNotSupported = errors.New("client: operation not supported by negotiated dialect") // ErrVersionMismatch is returned by the Conn constructor when the // server's Rversion carries a version string that is neither // "9P2000.L" nor "9P2000.u". ErrVersionMismatch = errors.New("client: version mismatch") // ErrMsizeTooSmall is returned by the Conn constructor when the // negotiated msize (min of client proposal and server cap) is below // the minimum required to carry a useful payload. ErrMsizeTooSmall = errors.New("client: msize too small") // ErrFidExhausted is returned when the per-Conn fid counter has run // past proto.NoFid (2^32 - 2 allocations). Unreachable under any // practical workload; documented for completeness. Callers that // encounter it should Dial a new Conn. ErrFidExhausted = errors.New("client: fid space exhausted") // ErrDialectInvariant signals that Conn.dialect holds a value outside // the {protocolL, protocolU} set expected after Dial. Dialect is set // once at negotiation time and never mutated afterwards, so reaching // this sentinel means either (1) a future refactor forgot to update a // dialect switch, or (2) memory corruption. It is wrapped into the // default arm of dialect switches in session helpers so callers can // errors.Is against a stable sentinel rather than string-matching the // wrapping fmt.Errorf. ErrDialectInvariant = errors.New("client: dialect invariant violated (programmer error)") )
Sentinel errors exposed by the client package. Callers match these with errors.Is for lifecycle-level error discrimination. Per-operation 9P errors (EACCES, ENOENT, etc.) are surfaced as a *Error wrapping a proto.Errno — use errors.Is with the proto.Errno constant for those.
Functions ¶
func BackoffFor ¶
BackoffFor returns the sleep duration for iteration i from schedule. Exposed as a test hook to validate cap behavior at i >= len(schedule). Production callers should never need this.
func DefaultLockBackoff ¶
DefaultLockBackoff returns the default exponential backoff schedule used by File.Lock. Exposed as a test-only hook via an exported symbol so black-box tests can assert the curve without reaching into package internals.
The returned slice is a defensive copy; mutations by the caller do not affect the Conn-wide default.
Types ¶
type Conn ¶
type Conn struct {
// contains filtered or unexported fields
}
Conn is a 9P client connection. Safe for concurrent use by multiple goroutines per D-07; modeled on database/sql.DB. All T-message writes are serialized through writeMu; all R-message reads come from a single read goroutine that dispatches into per-tag response channels stored in inflight.
Lifetime invariants:
- Construction is via Dial. Fields below are set once during Dial and then read-only for the Conn's lifetime (except inflight's internal mutex, writeMu, and the sync.WaitGroup counters).
- closeCh is closed exactly once via closeOnce. After close, the read goroutine exits, writeT returns an error, and tagAllocator.acquire returns ErrClosed.
- readerWG tracks the single read goroutine. callerWG tracks every op-method goroutine that has an acquired tag. Both are drained by Close/Shutdown (Plan 19-05).
func Dial ¶
Dial returns a live Conn after running 9P version negotiation over nc. The client proposes 9P2000.L with WithMsize's value (default 1 MiB; see client/options.go).
Per D-09 (.planning/phases/19/19-CONTEXT.md), the server's Rversion is accepted when it carries one of three strings:
- "9P2000.L" — full .L codec + message set.
- "9P2000.u" — .u codec + .u message set (Unix extensions explicit).
- "9P2000" — bare. Linux v9fs treats this as a .u-compatible alias (the kernel client proposes .u and the server may echo the bare string). Dial maps this to the .u codec to match that convention.
Any other version string yields ErrVersionMismatch. The negotiated msize is min(client proposal, server Rversion.Msize); a result below minMsize (256) yields ErrMsizeTooSmall.
The supplied ctx is honored only during the Tversion round-trip — its deadline is applied to nc via SetDeadline and cleared before Dial returns on success. For request-level cancellation (Phase 22) use per-op contexts on the returned *Conn.
On any error Dial leaves nc in whatever state the caller provided: it does NOT close nc on its own, so a caller may reuse the connection (e.g. re-dial with a different proposed version).
func (*Conn) Attach ¶
Attach binds this Conn to a filesystem mount. Issues Tattach(rootFid, NoFid, uname, aname) where rootFid is freshly allocated from this Conn's fid pool. Returns a *File representing the root directory.
Authentication (afid != NoFid) is not supported in v1.3.0 -- see package doc. Attach always passes NoFid for the afid field; use Conn.AttachFid (or Raw.Attach) if wire-level control is needed.
The returned *File is also cached on the Conn as the implicit root for subsequent Conn.OpenFile / Conn.Create calls. Callers may Attach multiple times; the most recent successful Attach becomes the new implicit root (the prior root File is NOT auto-Closed -- callers that care track both).
Per D-05 / D-19: ctx is respected for the Tattach round-trip.
func (*Conn) AttachFid ¶
func (c *Conn) AttachFid(ctx context.Context, fid proto.Fid, uname, aname string) (proto.QID, error)
AttachFid associates fid with the root of the file tree named by aname and establishes the session for user uname. This is the low-level wire op; Phase 20's Conn.Attach wraps it to return a *File with an allocator- owned fid. Per D-17/D-18 Phase 19 supports only afid=NoFid (no authentication); Tauth is not implemented. aname selects the mount point, server-defined; the empty string is the conventional "default" root.
Returns the root QID on success, or a *Error translated from Rlerror/Rerror on server-side failure.
Reachable via Raw.Attach for callers that manage fids themselves.
func (*Conn) Close ¶
Close initiates an orderly shutdown with a default 5-second drain deadline per D-22. Semantics:
- Signal shutdown (close closeCh, close nc, cancel all inflight). This unblocks callers parked on respCh (cancelAll closes every respCh), tagAllocator.acquire (closeCh select arm), and readLoop (nc.Close surfaces as a read error).
- Wait up to 5s for caller goroutines (callerWG) to return.
- Wait for the read goroutine (readerWG) to exit — unbounded because step 1 closed nc which will surface as a read error immediately.
Close does NOT return while caller or read goroutines are still running (D-24 — no background reaping). Safe to call from multiple goroutines; idempotent via closeOnce.
Close returns nil even if the drain deadline fires — the deadline is a log-and-proceed signal, not an error condition (consumers have no recovery path from "server wedged our goroutine").
func (*Conn) Clunk ¶
Clunk releases fid. After a successful clunk, fid is no longer valid; the server deallocates any associated state. Errors from Rlerror/Rerror surface as *Error; type-mismatch as a descriptive error.
func (*Conn) Create ¶
Create walks from the root to the parent directory of path, then issues Tlcreate (.L) or Tcreate (.u) for the basename. Returns an open *File positioned at offset 0.
On .L, gid defaults to 0 ("server default"). On .u, extension is empty (regular file). Callers needing non-default gid or .u extensions should use Raw.Lcreate / Raw.Create directly.
Only the permission bits of mode (mode & 0o7777) are honored; os.FileMode type bits (os.ModeDir, os.ModeSymlink, os.ModeSetuid, etc.) are masked off before the value is encoded on the wire. The os.FileMode type-bit encoding does not align with 9P's DM* bits (e.g. os.ModeSymlink is 1<<26 while DMSYMLINK is 1<<25), so forwarding them verbatim would corrupt the wire mode silently. Callers needing to create non-regular files should use the dedicated ops (Tmkdir/Tsymlink, wired in a later phase).
After a successful Lcreate/Tcreate, the fid that was walked to the parent is mutated server-side to refer to the newly-created file (9P semantics). The returned *File wraps that same fid.
func (*Conn) CreateFid ¶
func (c *Conn) CreateFid(ctx context.Context, fid proto.Fid, name string, perm proto.FileMode, mode uint8, extension string) (proto.QID, uint32, error)
CreateFid is the 9P2000.u create-and-open wire operation. Requires a .u-negotiated Conn. Phase 20's Conn.Create wraps this and the .L-only Conn.Lcreate behind a dialect-neutral session method; use CreateFid (or Raw.Create) only when explicit fid control is needed.
perm is the file-mode + type bits; mode is the 9P2000.u open mode; extension is the .u Extension field (symlink target, device spec, etc. — empty for regular files).
func (*Conn) Dialect ¶
Dialect returns the negotiated 9P dialect: "9P2000.L" or "9P2000.u". Callers that branch on dialect for advanced ops should compare against these exact strings.
The dialect is chosen by Dial's Tversion round-trip per Phase 19 D-09 (see client/doc.go "Dialects"). A bare "9P2000" response is normalized to "9P2000.u" at Dial time (Linux v9fs kernel convention); Dialect never returns "9P2000". Immutable for the Conn's lifetime.
func (*Conn) Flush ¶
Flush asks the server to abort the request identified by oldTag. Per the 9P spec the server responds with Rflush regardless of whether oldTag matches an outstanding request. As such, a nil return does NOT confirm the original request was cancelled — the request may have completed before Flush was received.
Phase 19 does not auto-invoke Flush on ctx cancellation; that wiring lives in Phase 22 (CLIENT-04). This method is the raw wire-level primitive for callers that need it directly.
func (*Conn) Lcreate ¶
func (c *Conn) Lcreate(ctx context.Context, fid proto.Fid, name string, flags uint32, mode proto.FileMode, gid uint32) (proto.QID, uint32, error)
Lcreate creates and opens a new file named name in the directory referenced by fid. After a successful Lcreate, fid is mutated server-side to refer to the newly-created file (not the parent directory); this matches Plan 9 and the Linux v9fs kernel client. Requires a .L-negotiated Conn.
flags is the POSIX open flag set (O_RDWR, O_CREAT already implied, etc.). mode is the POSIX permission bits + file-type. gid is the group to assign to the new file (zero for "use the server default").
func (*Conn) Link ¶
Link creates a hard link at newPath pointing at the existing file at existingPath. Both entries reference the same underlying inode after the call returns — reads and writes on either name see the same data.
Requires a 9P2000.L-negotiated Conn; returns wrapped ErrNotSupported on .u (9P2000.u has no Tlink wire op).
Both paths must be non-root. The parent directory of newPath must exist (Link does not create parents). The server is required to reject cross-device / cross-mount links — surfaces as a *Error carrying the server's errno (typically EXDEV or EPERM).
Fid lifecycle: Link acquires two fids (source file, dest parent dir); both are clunked and released on every exit path.
func (*Conn) Lopen ¶
Lopen opens an existing file referenced by fid with the given POSIX open flags (O_RDONLY, O_RDWR, etc.). Requires a 9P2000.L-negotiated Conn (D-19, D-20); on a .u Conn returns ErrNotSupported without touching the wire.
Returns the file's QID and the server's suggested iounit (the maximum bytes the server is willing to return in a single Rread or accept in a single Twrite; a value of 0 means "unknown, use msize").
func (*Conn) Mknod ¶
func (c *Conn) Mknod(ctx context.Context, parentPath, name string, mode proto.FileMode, major, minor, gid uint32) (*File, error)
Mknod creates a device/fifo/special-file node named name under the directory at parentPath. Mode carries the POSIX mode bits — the high-order bits select the node type (S_IFIFO/S_IFCHR/S_IFBLK/S_IFSOCK etc.), the low-order bits are the permission bits. major/minor identify the device; gid sets the owning group.
The returned *File is a stat-only handle bound to the new node — 9P has no "open a device node" mechanism at this layer; the fid is useful for File.Close (release the fid) and capability-level operations that do not require Tlopen/Topen.
Requires a 9P2000.L-negotiated Conn; returns wrapped ErrNotSupported on .u (Tmknod is .L-only; .u callers historically encoded device nodes via the Tcreate extension field).
parentPath may be "/" (or equivalent: "", "."). name must be a single path component; callers cannot create intermediate directories via Mknod.
Fid lifecycle: acquires up to two fids (parent dir + newly-created node). Both are clunked and released on every exit path — the parent dirFid at method exit, the newFid only on post-Tmknod walk failure. On success the newFid lives on as the returned *File.fid until File.Close.
func (*Conn) Msize ¶
Msize returns the negotiated maximum message size, in bytes. Set at Dial time as the minimum of the client's proposed msize (see WithMsize) and the server's Rversion.Msize cap. Immutable for the Conn's lifetime -- 9P does not support mid-connection renegotiation from the client side in this library.
Used by File.Read / File.Write (Plan 20-03+) to clamp each Tread/Twrite payload so the encoded frame fits within the negotiated msize after per-message framing overhead.
func (*Conn) Open ¶
Open is the 9P2000.u file-open operation. Requires a .u-negotiated Conn (D-19, D-20); on a .L Conn returns ErrNotSupported.
mode is a 9P2000.u open mode (OREAD=0, OWRITE=1, ORDWR=2, OEXEC=3 with optional flag bits in the upper bits). Returns QID + iounit.
func (*Conn) OpenDir ¶
OpenDir walks from the Attach'd root to p and opens it as a directory. Convenience wrapper over Conn.OpenFile with flags=0, mode=0; returns an opened *File suitable for File.ReadDir.
The returned *File must be Closed by the caller. On .L, flags=0 (== O_RDONLY) opens the directory fid for Tread/Treaddir. On .u, [posixToNinepMode] maps 0 to OREAD which likewise opens for read-only directory enumeration (though Phase 20's File.ReadDir is .L-only -- .u directory enumeration is deferred; see Q4).
Empty path, "/", and "." all open the root directory (equivalent to a zero-step Twalk followed by Tlopen/Topen).
func (*Conn) OpenFile ¶
OpenFile walks from the root (set by a prior Conn.Attach) to path, then opens the walked-to fid with the given POSIX-style flags and mode. Signature mirrors os.OpenFile for caller familiarity.
On a 9P2000.L Conn, issues Tlopen with flags passed through verbatim (they are POSIX open flags that .L accepts directly). On a 9P2000.u Conn, translates flags to 9P2000.u mode bits (O_RDONLY -> 0, O_WRONLY -> 1, O_RDWR -> 2; higher POSIX bits dropped) and issues Topen.
The mode parameter is currently unused by OpenFile (it is reserved for Create-style call signatures parallelism); the created/opened permission bits on Create pass through Conn.Create's perm argument instead.
Error-path fid lifecycle (Pitfall 2 / Pitfall 3):
- Walk fails BEFORE server binding: reserved fid is released.
- Walk succeeds, Lopen/Topen fails: walked fid is Tclunked then released.
func (*Conn) Raw ¶
Raw returns the Raw sub-surface for this Conn. The returned value is a thin wrapper -- repeated calls do not allocate beyond the returned pointer and methods on the returned Raw delegate 1:1 to the corresponding Conn methods.
func (*Conn) Read ¶
func (c *Conn) Read(ctx context.Context, fid proto.Fid, offset uint64, count uint32) ([]byte, error)
Read reads up to count bytes from fid starting at offset. Returns the bytes actually read, which may be fewer than count (EOF or short read).
The returned slice is caller-owned — it is copied out of the pooled Rread struct (whose Data field aliases a bucket buffer from bufpool) before the struct is returned to the cache. Callers may retain the slice indefinitely.
Read does NOT clamp count to the negotiated msize or the file's iounit. Callers that need throughput-optimal chunking should consult the iounit returned by Lopen/Open and size their reads accordingly; passing an over-large count results in whatever the server chooses to return (many servers clamp silently).
func (*Conn) Remove ¶
Remove removes the file or directory at path. On 9P2000.L the wire op is Tunlinkat against the parent fid; auto-detects directories via a probe walk that reads QID.Type and sets the AT_REMOVEDIR flag accordingly (Pitfall 9). On 9P2000.u, Remove returns wrapped ErrNotSupported — .u lacks Tunlinkat and this library's server does not implement a Tremove handler (so a .u fallback cannot succeed anywhere).
The path must be non-root. All intermediate parent directories must exist; a missing parent surfaces the server's ENOENT as a *Error.
Fid lifecycle: Remove acquires up to two fids (parent + probe target), clunks and releases both on every exit path — no fid leaks on any failure mode (T-21-02-03). No separate Clunk is needed for the probe fid; Tunlinkat operates against the parent fid and the probe exists only to determine the AT_REMOVEDIR flag value.
func (*Conn) Rename ¶
Rename moves the entry at fromPath to toPath. On 9P2000.L the wire op is Trenameat(oldDirFid, oldName, newDirFid, newName) — both parent directories are walked, the rename is issued, and both parent fids are clunked regardless of outcome (Pitfall 10).
On 9P2000.u, Rename returns wrapped ErrNotSupported. Raw.Trename is gated to .L at the raw layer (p9l codec only), and this library's .u server does not implement a Trename-via-Twstat fallback. Callers needing .u rename semantics should use Raw primitives directly and speak Twstat with the server's expectations.
Both fromPath and toPath must be non-root. Intermediate directories on either path must exist (Rename does not create parents).
Rename-while-open preservation (21-RESEARCH.md Pitfall 5): a *File opened against fromPath before the rename remains valid — the fid stays bound to the same inode, not the path, and subsequent File.Read / File.Write continue against the renamed node. This matches Linux v9fs kernel semantics.
Fid lifecycle: on .L, Rename acquires two fids (source parent, dest parent). Every exit path — successful rename, source-walk failure, dest-walk failure, Trenameat error — clunks and releases both fids (T-21-02-03). No leaks on partial failure.
func (*Conn) Root ¶
Root returns the *File from the most recent successful Conn.Attach. Returns nil if no Attach has ever succeeded on this Conn. Does NOT issue a wire op.
func (*Conn) Shutdown ¶
Shutdown is Close with a caller-supplied context for custom timeout behavior (D-23). If ctx has a Deadline, the remaining time is used as the drain bound; otherwise the default 5s applies. Named after http.Server.Shutdown for idiom parity.
An already-cancelled ctx (ctx.Err() != nil) skips the drain but still waits for the read goroutine to exit, preserving the D-24 invariant that no goroutines leak past Shutdown's return.
func (*Conn) Symlink ¶
Symlink creates a symbolic link at linkPath with the given target string and returns a stat-only *File handle bound to the new symlink. Per D-01 (21-CONTEXT.md), symlink creation is path-rooted, so the method lives on *Conn rather than *File. The returned handle is NOT opened — 9P has no "open a symlink" op; the fid is useful for File.Readlink and File.Close only.
The linkPath must be non-root. The parent directories along linkPath must exist (this method does not recursively create parents); a missing parent surfaces the server's ENOENT as a *Error.
The target string is passed verbatim to the server; this library does not interpret it. Callers who later resolve the target via a local syscall/filesystem op are responsible for validating its shape (e.g. rejecting path components like ".." when a jailed resolution is required).
Requires a 9P2000.L-negotiated Conn; returns a wrapped ErrNotSupported on .u (9P2000.u has no Tsymlink wire op — .u callers must use Tcreate with the DMSYMLINK bit set and the target in the create extension field, which is out of scope for this high-level surface).
gid is passed as 0 ("server default"). Callers who need explicit gid control should use Raw.Tsymlink directly.
func (*Conn) Walk ¶
func (c *Conn) Walk(ctx context.Context, fid, newFid proto.Fid, names []string) ([]proto.QID, error)
Walk descends from fid along names, creating newFid at the final element. An empty names slice clones fid into newFid without navigating. Returns one QID per successfully walked element.
The returned []proto.QID is caller-owned — it is copied out of the pooled Rwalk struct before the struct is returned to the cache, so callers may retain the slice indefinitely.
type Error ¶
type Error struct {
// Errno is the 9P errno value carried on the wire. Always populated.
Errno proto.Errno
// Msg is the server-supplied human-readable string from Rerror.Ename
// on a .u-negotiated Conn. Empty for Rlerror responses.
Msg string
}
Error represents a 9P error response from the server. Rlerror (9P2000.L) populates only Errno; Rerror (9P2000.u) populates both Errno and Msg (the .u extension's human-readable ename). Callers treat the two dialects uniformly via this single type.
The Msg field is server-controlled on .u connections — callers that log errors from untrusted servers should redact or bound-check Msg before emitting it at info-level.
func (*Error) Error ¶
Error formats the error. When Msg is set (Rerror on .u), the output is "9p: <errno>: <msg>"; otherwise it is "9p: <errno>".
func (*Error) Is ¶
Is delegates to proto.Errno.Is, so the idiomatic Go pattern works:
if errors.Is(err, proto.EACCES) { ... }
Note: proto.Errno.Is matches only against other proto.Errno targets. It does NOT bridge to syscall.Errno — even though the numeric values match on Linux, errors.Is(&client.Error{Errno: proto.ENOENT}, syscall.ENOENT) returns false. Use proto.Errno constants for portable error discrimination. This is Assumption A1 from .planning/phases/19/19-RESEARCH.md §9, verified false during Plan 19-01 execution.
type File ¶
type File struct {
// contains filtered or unexported fields
}
File is a handle to a 9P file. Safe for sequential use; callers wanting parallel reads on the same server-side file should use File.Clone to obtain an independent handle with its own fid and offset.
Obtain a File from Conn.Attach (root of the filesystem), Conn.OpenFile (opened file at a path), Conn.Create (created + opened file), File.Walk (navigated-to node, NOT opened), or File.Clone (duplicate of an existing File at the same position with a fresh fid and zero offset).
Close idempotency ¶
Unlike os.File, which returns os.ErrClosed on a second Close, File.Close returns nil on every call after the first (D-06 in the Phase 20 CONTEXT.md). This simplifies defer-heavy code at the cost of masking double-close bugs. If a double-close diagnostic is needed in a future rev, flip the second-call return to os.ErrClosed; callers tracked via errors.Is continue to compile.
Concurrency ¶
The per-File mutex (f.mu) serializes File.Read, File.Write, File.ReadAt, and File.WriteAt so that offset mutation in the sequential path is race-free. File.ReadAt and File.WriteAt still conform to their io.ReaderAt/io.WriterAt contracts -- the contracts permit parallel CALLS and the implementation satisfies "does not panic or corrupt" by serializing. Callers that want actual parallel I/O spawn [File.Clone]s per goroutine; each clone has its own fid and mutex.
File.Close does NOT take the File mutex -- a Close issued while a Read is in flight unblocks via the Conn's closeCh path rather than deadlocking on the handle mutex (Pitfall 7 in 20-RESEARCH.md §9).
func (*File) Chmod ¶ added in v1.4.0
Chmod changes the File's permissions to mode.
Requires 9P2000.L; returns a wrapped ErrNotSupported on a .u Conn.
func (*File) Chown ¶ added in v1.4.0
Chown changes the File's owner and group to uid and gid.
Requires 9P2000.L; returns a wrapped ErrNotSupported on a .u Conn.
func (*File) Clone ¶
Clone returns a new independent *File at the same server-side node via Twalk(oldFid, newFid, nil). The clone has its own fid, its own zero offset, and its own mutex. Closing the clone does not affect this File; closing this File does not affect the clone (D-13).
On error, the reserved newFid is released to the allocator (Pitfall 2). A 0-step Walk binds newFid server-side only on success, so no Tclunk is needed on the error path.
func (*File) Close ¶
Close clunks the fid on the server, releases the fid to the allocator's reuse cache, and marks the File as closed. Subsequent Close calls return nil without touching the wire (D-06).
The error from the first Close (if Tclunk returned one) is returned to THAT caller and captured into f.closeErr for diagnostics; subsequent callers receive nil regardless of what the first call observed. Close does not take f.mu -- a concurrent in-flight Read/Write on this File unblocks via the Conn's shutdown path (Conn.Close / Conn.Shutdown), not via the handle mutex.
Context: Close does NOT use [Conn.opCtx] / WithRequestTimeout (D-24). Clunk is a cleanup op whose ceiling is governed by the Conn-wide drain deadline (5s per Phase 19 D-22), not a per-request user-configurable timeout. Using WithRequestTimeout here would let a caller with a pathological sub-millisecond value strand fids on the server; the fixed cleanupDeadline is the safer invariant.
func (*File) Fid ¶
Fid returns the proto.Fid the File holds on the server. The fid is stable for the File's lifetime (Close releases it back to the allocator). Exposed for Raw-surface interop and for tests that need to probe the post-Close fid state.
func (*File) GetLock ¶
GetLock queries the server for any lock currently held over the region that conflicts with a lock of type lt. Returns a non-nil *Lock describing the holder if a conflict exists, (nil, nil) if the region is free, or (nil, err) on protocol error.
The server signals "no conflict" by returning LockTypeUnlck in the Rgetlock reply; GetLock translates that into a nil pointer so callers can branch on `got == nil` without inspecting a sentinel field.
Requires 9P2000.L; returns (nil, wrapped ErrNotSupported) on a .u Conn.
func (*File) Getattr ¶
Getattr issues Tgetattr(fid, mask) and returns the full 9P2000.L proto.Attr struct. Exposed for callers that need fields attrToStat discards: NLink, Blocks, BTime, Gen, DataVersion.
Common masks: proto.AttrBasic (mode through blocks — the recommended default), proto.AttrAll (every defined attribute), or a narrower bitmask when the caller only needs one field (e.g. AttrSize for a size refresh).
Requires a 9P2000.L-negotiated Conn; returns a wrapped ErrNotSupported on a .u Conn. The gate fires before any wire op.
func (*File) Lock ¶
Lock acquires a POSIX-style advisory lock on this File's fid over the region [0, end-of-file). Blocks until the lock is acquired, ctx is cancelled, or the server returns an error.
The wire-level Tlock is poll-based: the server returns SUCCESS / BLOCKED / ERROR / GRACE per call. On BLOCKED or GRACE, Lock sleeps per the WithLockPollSchedule backoff curve (default 10/20/40/80/160/320/500ms cap) and re-issues Tlock until one of the terminal statuses fires.
On ctx cancellation, Lock unconditionally emits a Tlock(LockUnlock) cleanup using a fresh background context (with a short deadline) to release any lock the server may have granted between our Tlock send and ctx firing — Pitfall 6 in 21-RESEARCH.md. This belt-and-braces cleanup keeps server state aligned with client state even without Phase 22's ctx-driven Tflush wiring.
The fid MUST be opened (via Conn.OpenFile or Conn.Create); calling Lock on a walked-but-unopened fid returns a *Error with the server's errno (typically EBADF — server bridge enforces fidOpened state).
Concurrency constraint: Lock and File.Close are NOT safe to call concurrently on the same *File. Close deliberately bypasses f.mu to keep the shutdown path wait-free (Phase 20 D-12), which means a Close racing against a blocked Lock (parked in the backoff select) can release f.fid to the allocator before unlockCleanup fires. The allocator may then re-issue that numeric fid to an unrelated *File, and the cleanup Tlock(UNLCK) will land on the wrong fid — at best EBADF, at worst releasing a lock held by another caller. The same race exists on the in-flight-Tlock ctx-cancel branch. Callers who need to cancel a blocked Lock MUST cancel ctx and wait for Lock to return before invoking Close. If the caller's lifecycle cannot guarantee this, serialise Lock/Close externally with a handle-level mutex.
Requires 9P2000.L; returns a wrapped ErrNotSupported on a .u Conn.
Protocol Safety Warning: 9P2000.L locks are tied to the fid. Clunking the fid releases all locks held via that fid.
func (*File) Qid ¶
Qid returns the server's unique identifier for this file. Set at construction; does not issue a wire op.
func (*File) Read ¶
Read reads up to len(p) bytes from the File starting at the current local offset (see File.Seek). Returns io.EOF when the server responds with zero bytes on a non-empty p.
Short reads are permitted by io.Reader -- each call issues at most one Tread, and the server may return fewer bytes than requested. Callers wanting "fill or error" semantics should use File.ReadAt or wrap with bufio.Reader / io.ReadFull.
Context: Read does NOT take a ctx — the io.Reader contract has no ctx slot. Read derives its ctx from the Conn's WithRequestTimeout setting (default: infinite wait, matching Linux v9fs kernel parity per D-22 / Pitfall 9). Callers that need per-op cancellation use File.ReadCtx with a caller-supplied ctx.
Thread safety: serialized by f.mu with File.Write, File.ReadAt, and File.WriteAt. Use File.Clone for parallel I/O.
func (*File) ReadAt ¶
ReadAt reads len(p) bytes from the File starting at off. Satisfies the io.ReaderAt contract: returns a non-nil error whenever n < len(p), specifically io.EOF when the short return is at the end of file.
ReadAt does NOT advance the local offset -- it is independent of File.Read and File.Seek state. Concurrent callers on the same *File serialize via f.mu per D-12; callers wanting actual parallel I/O on the same server-side file should use File.Clone to obtain independent handles.
Internally loops issuing Treads against the offset until either p is filled or the server returns zero bytes (EOF). Each Tread is clamped to min(iounit, msize - ioFrameOverhead).
Context: ReadAt derives its ctx from the Conn's WithRequestTimeout setting (default: infinite wait per D-22). The same ctx is used for every chunk of the loop. Callers needing per-op cancellation use File.ReadAtCtx.
func (*File) ReadAtCtx ¶
ReadAtCtx is the ctx-taking variant of File.ReadAt. Loops issuing Treads at off, off+n1, off+n1+n2, ... until p is filled or the server returns zero bytes (io.EOF). Each Tread uses the caller's ctx verbatim, so a cancel mid-chunk returns a partial count with the ctx error.
Internally uses the zero-copy read path (24-03 / D-05): each chunk of length <= maxChunk() is decoded directly from the wire into the corresponding sub-slice of p, skipping both the intermediate Rread.Data allocation AND the Conn.Read result-copy. See 24-RESEARCH.md §Pattern B for the design.
Does NOT advance the local offset — the io.ReaderAt contract is preserved regardless of what the caller's ctx does. Serializes against other I/O methods on the same *File via f.mu per D-12.
func (*File) ReadCtx ¶
ReadCtx is the ctx-taking variant of File.Read. Satisfies the same byte-slice semantics as File.Read (advances the local offset, clamps count to min(iounit, msize-overhead), returns io.EOF on a zero-byte server response) but honors the caller-supplied ctx verbatim — WithRequestTimeout (if set on the Conn) is IGNORED in favor of the caller's ctx (D-23).
Serializes against other I/O methods on the same *File via f.mu. For parallel I/O, use File.Clone (which issues its own fid).
On ctx cancellation or deadline expiry, a Tflush(oldtag) is sent via the shared roundTrip pipeline (Plan 22-02); the returned error satisfies errors.Is against ctx.Err() (Canceled or DeadlineExceeded) and, on the Rflush-first path, also against ErrFlushed.
func (*File) ReadDir ¶
ReadDir reads directory entries from this File, which must have been opened on a directory fid (see Conn.OpenDir).
If n > 0, returns at most n entries; a subsequent call resumes at the next entry. If n <= 0, ReadDir loops issuing Treaddir round- trips until the server reports the directory is exhausted, and returns the full entry slice.
When the directory is exhausted, ReadDir returns (nil-or-empty slice, nil). Callers emulating the os.File.ReadDir n>0 contract (which returns io.EOF on exhaustion) should check len(entries) == 0 on nil err.
Only supported on 9P2000.L Conns. On a .u Conn returns (nil, ErrNotSupported) -- .u directory enumeration uses a different wire op (Tread on a directory fid returning packed .u Stat entries) and is deferred to a future phase.
The returned entries' os.DirEntry.Info method returns ErrNotSupported in v1.3.0 Phase 20. Phase 21 wires Tgetattr so Info() returns a populated fs.FileInfo.
Thread safety: takes f.mu and mutates the internal readdirOffset cursor. Concurrent ReadDir on the same *File serializes. Use File.Clone for parallel enumeration if that is ever needed (rare -- directory enumeration is typically sequential).
func (*File) Readlink ¶
Readlink returns the target path of a symbolic link. The file must have been opened or walked-to on a symlink node (QID.Type has QTSYMLINK set); Readlink on a non-symlink surfaces the server's error (typically EINVAL for .L servers) as a *Error.
Requires a 9P2000.L-negotiated Conn; returns a wrapped ErrNotSupported on .u (Treadlink is .L-only; .u servers use stat-extension fields to carry symlink targets, which is out of scope for this high-level surface).
Rename-while-open preservation (21-RESEARCH.md Pitfall 5) applies: if the underlying symlink is renamed concurrently, the fid continues to point at the same inode and Readlink returns the same target.
func (*File) Seek ¶
Seek sets the local offset for the next File.Read or File.Write on this File per D-09. Does NOT issue a wire op -- 9P is offset-addressed on every Tread/Twrite, so there is no server-side seek state to synchronize.
Whence:
- io.SeekStart: offset is the absolute position.
- io.SeekCurrent: offset is relative to the current position.
- io.SeekEnd: offset is relative to the file's size, read from f.cachedSize. cachedSize defaults to 0 and is populated by File.Sync once Phase 21 ships Tgetattr/Tstat; until then SeekEnd(0) returns 0 (correct for an empty file) and SeekEnd(-n) for n > 0 returns a "negative position" error with guidance to call File.Sync first.
Returns an error when the computed absolute position is negative. Seeking past the end of the file is allowed and does not error -- subsequent Reads return io.EOF, and subsequent Writes may extend the file (server-permitting).
Seek on a directory fid succeeds (pure arithmetic). A subsequent File.Read on a directory fid surfaces the server's EISDIR (or equivalent) error; this matches os.File behavior per D-11.
Thread safety: serialized with I/O methods via f.mu.
func (*File) Setattr ¶
Setattr mutates metadata fields on the server-side file referenced by this File. attr.Valid is the bitmask of fields to write — fields not named in Valid are ignored by the server.
Requires a 9P2000.L-negotiated Conn; returns a wrapped ErrNotSupported on a .u Conn. The gate fires before any wire op so the error message reads "Setattr requires 9P2000.L" rather than "Tsetattr requires 9P2000.L".
func (*File) Stat ¶
Stat returns a dialect-neutral snapshot of the File's metadata. On 9P2000.L connections, Stat issues Tgetattr(fid, AttrBasic) and converts the result via [attrToStat]. On 9P2000.u connections, Stat issues Tstat and returns the resulting stat directly.
The return type is p9u.Stat on both dialects — a dialect-neutral shape per D-16/D-18 in 21-CONTEXT.md (Pitfall 4 Option A). Fields present only in .L's richer proto.Attr (NLink, Blocks, BTime, Gen, DataVersion) are discarded; callers that need them call File.Getattr directly on a .L Conn.
Stat does NOT mutate f.cachedSize — that side effect lives in File.Sync so File.Seek with io.SeekEnd has a predictable refresh primitive.
UID and GID on .L are numeric and are stringified as decimal in the returned p9u.Stat; callers parsing those strings should use strconv.ParseUint. The numeric values also remain accessible via Stat.NUid and Stat.NGid.
func (*File) Statfs ¶
Statfs queries filesystem-level statistics for the file tree containing this File's fid. The return is BY VALUE — a successful Statfs always returns a populated proto.FSStat, so a pointer return shape (nil possible) would be misleading (Pitfall 8 in 21-RESEARCH.md).
Requires a 9P2000.L-negotiated Conn; returns a wrapped ErrNotSupported on a .u Conn. The gate fires before any wire op so the error message reads "Statfs requires 9P2000.L" rather than "Tstatfs requires 9P2000.L".
Statfs does not cache. Each call issues a fresh Tstatfs round-trip. Filesystem statistics can change between calls; callers that observe them over time should treat each result as a point-in-time snapshot.
func (*File) Sync ¶
Sync refreshes this File's cached size from the server by issuing Tgetattr (.L) or Tstat (.u). Callers that use File.Seek with io.SeekEnd on a file whose size may have changed server-side (e.g. concurrent writers, or a truncate via another fid) call Sync first so the subsequent SeekEnd returns a current value.
Sync uses a bounded background context; for caller-controlled cancellation use File.Stat directly (which takes a ctx and returns the size via the returned p9u.Stat without mutating f.cachedSize).
Sync is not a cache refresh in the "only do it if stale" sense: every call issues a fresh wire op. Callers that want a cheap SeekEnd after a known-static file was attached should cache the post-Sync size themselves.
On error, f.cachedSize is NOT modified — the previous successful value (or the zero-value initial state) is preserved. This keeps a file whose first Sync succeeded but whose second Sync fails transiently usable for SeekEnd against the earlier size.
Satisfies the common io.Syncer-like shape (no ctx) matching os.File.Sync. The internal syncImpl lives in client/sync.go; the body here is the one-line dispatch.
func (*File) Truncate ¶ added in v1.4.0
Truncate changes the File's size to size.
Requires 9P2000.L; returns a wrapped ErrNotSupported on a .u Conn.
func (*File) TryLock ¶
TryLock attempts to acquire a lock non-blocking. Returns (true, nil) if acquired, (false, nil) if the region is already held by a conflicting lock (server returned BLOCKED or GRACE), or (false, err) on protocol error.
Issues a single Tlock WITHOUT proto.LockFlagBlock — TryLock never retries. Callers wanting bounded-retry semantics should compose Lock with a ctx deadline instead.
Requires 9P2000.L; returns (false, wrapped ErrNotSupported) on a .u Conn.
func (*File) Unlock ¶
Unlock releases any lock held on this File's fid via Tlock(LockUnlock). Idempotent — the server accepts UNLCK on a region with no outstanding lock and returns OK.
Requires 9P2000.L; returns a wrapped ErrNotSupported on a .u Conn.
func (*File) Walk ¶
Walk returns a new *File for the node reached by following names from this File's position. Does NOT open the returned File -- useful for Stat (Phase 21) and ReadDir without opening.
Empty names is invalid here (use File.Clone for 0-step walks). Callers passing empty names receive an error.
On error, any reserved fid slot is released to the allocator before the error is returned (Pitfall 2).
func (*File) Write ¶
Write writes len(p) bytes to the File starting at the current local offset, advancing the offset by bytes-written. Chunks the payload over multiple Twrites when len(p) exceeds min(iounit, msize - ioFrameOverhead).
Returns io.ErrShortWrite if the server reports a Twrite count less than the chunk size sent -- per the io.Writer contract, a non-nil error must accompany any n < len(p) result.
Context: Write derives its ctx from the Conn's WithRequestTimeout setting (default: infinite wait per D-22). The same ctx is used for every chunk. Callers needing per-op cancellation use File.WriteCtx.
Thread safety: serialized with other I/O methods on the same *File via f.mu.
func (*File) WriteAt ¶
WriteAt writes len(p) bytes to the File starting at off. Satisfies the io.WriterAt contract: returns a non-nil error whenever n < len(p).
WriteAt does NOT advance the local offset -- it is independent of File.Write and File.Seek state. Concurrent callers on the same *File serialize via f.mu per D-12; use File.Clone for parallel writes.
Chunks the payload over multiple Twrites when len(p) exceeds min(iounit, msize - ioFrameOverhead). Returns io.ErrShortWrite if the server reports a Twrite count less than the chunk size sent.
Context: WriteAt derives its ctx from the Conn's WithRequestTimeout setting (default: infinite wait per D-22). Callers needing per-op cancellation use File.WriteAtCtx.
func (*File) WriteAtCtx ¶
WriteAtCtx is the ctx-taking variant of File.WriteAt. Chunks p over multiple Twrites at off, off+n1, off+n1+n2, ...; each chunk uses the caller's ctx verbatim. Does NOT advance the local offset. Serializes against other I/O methods on the same *File via f.mu.
func (*File) WriteCtx ¶
WriteCtx is the ctx-taking variant of File.Write. Chunks p over multiple Twrites when len(p) exceeds the per-op clamp; the caller's ctx is used on every chunk, so a cancel mid-write returns a partial count with the ctx error per the io.Writer contract.
Returns io.ErrShortWrite if the server reports a Twrite count less than the chunk size sent.
Serializes against other I/O methods on the same *File via f.mu. For parallel I/O, use File.Clone. On ctx cancellation or deadline expiry, a Tflush(oldtag) is sent for the in-flight Twrite.
func (*File) XattrGet ¶
XattrGet retrieves the value of the extended attribute name. Hides the 9P2000.L two-phase protocol (Txattrwalk → Tread loop → Tclunk).
Returns a non-nil, zero-length []byte when the attribute exists with an empty value; returns a *Error wrapping proto.ENODATA when the attribute does not exist on the file. Use errors.Is against proto.ENODATA for the missing-attribute case:
v, err := f.XattrGet(ctx, "user.comment")
if errors.Is(err, proto.ENODATA) {
// attribute not set
}
The server-declared size is clamped against proto.MaxDataSize (16 MiB) before the client allocates its receive buffer — a misbehaving server cannot OOM this call by declaring an oversized value.
Requires a 9P2000.L-negotiated Conn. Returns ErrNotSupported (wrapped with op context) on a .u Conn before touching the wire.
func (*File) XattrList ¶
XattrList returns the names of all extended attributes set on this file. Composes XattrGet with name="" per the 9P2000.L list-all convention — the server returns attribute names NUL-separated with a trailing NUL byte after the last name.
Returns a non-nil empty slice (not nil) when no attributes exist, so callers can treat the return value uniformly without a nil check before ranging.
The trailing NUL and any internal empty entries (a defensive guard against malformed server responses — no legitimate xattr name is the empty string) are filtered out.
Requires a 9P2000.L-negotiated Conn.
func (*File) XattrRemove ¶
XattrRemove deletes the extended attribute name. Composes XattrSet with a zero-length value and flags=0 per the 9P2000.L wire convention — the server's xattr-commit path (server/dispatch.go:254) treats size=0 at commit time as a request to invoke [server.NodeXattrRemover].
Requires a 9P2000.L-negotiated Conn. The server returns an error wrapping proto.ENODATA if name does not exist, matching removexattr(2) semantics; callers can discriminate with errors.Is(err, proto.ENODATA).
func (*File) XattrSet ¶
XattrSet writes data as the value of the extended attribute name. flags follows Linux setxattr(2): 0 = create-or-replace, XATTR_CREATE (1) fails if the attribute already exists, XATTR_REPLACE (2) fails if it does not. The exact flag semantics are enforced by the server implementation of [server.NodeXattrSetter].
Pitfall 1: the 9P2000.L Txattrcreate protocol MUTATES the supplied fid into xattr-write state; after the subsequent Tclunk commits the value, that fid is invalidated. To keep the caller's *File f valid for reads/writes after XattrSet returns, this method clones f first (File.Clone allocates a fresh fid at the same server-side node) and performs the mutation on the clone.
The total byte count of data must fit within the server's negotiated msize (server/bridge.go:897 clamps Txattrcreate.AttrSize to msize); callers with larger xattrs should either raise msize at Dial time via WithMsize or use Raw.Txattrcreate directly.
Requires a 9P2000.L-negotiated Conn.
Performance Note: XattrSet clones the current file internally to preserve the fid's open/offset state. This consumes one transient fid.
type Lock ¶
type Lock struct {
// Type is the conflicting holder's lock type (LockRead or LockWrite;
// LockUnlock is never surfaced — it is the server's "no conflict"
// signal translated into a nil return).
Type LockType
// Start is the byte offset of the conflicting region.
Start uint64
// Length is the byte length of the conflicting region; 0 means
// "to end of file" per POSIX semantics.
Length uint64
// ProcID is the holder's process identifier, as registered when the
// lock was acquired.
ProcID uint32
// ClientID is the holder's client identifier. Empty when the holder
// did not supply one.
ClientID string
}
Lock describes a conflicting POSIX byte-range lock holder as returned by File.GetLock. A nil *Lock return from GetLock means the queried region is free (no conflict); a non-nil *Lock describes the holder whose lock conflicts with the proposed request.
type LockType ¶
LockType is a thin public enum over proto.LockType so callers of the client package don't need to import proto/ for the common lock-type constants. Per-value equivalence: LockRead == LockType(LockTypeRdLck), LockWrite == LockType(LockTypeWrLck), LockUnlock == LockType(LockTypeUnlck).
const ( // LockRead requests a shared (read) lock. Multiple holders of a // read lock over overlapping regions may coexist; a write lock // blocks until all outstanding read locks release. LockRead LockType = LockType(proto.LockTypeRdLck) // LockWrite requests an exclusive (write) lock. At most one holder // per overlapping region, and the lock blocks against any concurrent // read or write lock. LockWrite LockType = LockType(proto.LockTypeWrLck) // LockUnlock releases a previously-held lock over a region. Typically // issued via [File.Unlock] rather than [File.Lock]. LockUnlock LockType = LockType(proto.LockTypeUnlck) )
Lock type constants. These are wire-compatible with proto.LockType; the conversion to the proto type is a single uint8 widening.
type Option ¶
type Option func(*config)
Option configures a Conn. Options are applied by the Conn constructor in the order they are supplied.
func WithLockPollSchedule ¶
WithLockPollSchedule overrides the default exponential backoff curve used by File.Lock when the server returns LockStatusBlocked or LockStatusGrace. Values are the sleep durations for iterations 0..N; iterations past N use the last entry as a cap.
Passing an empty slice is a programming error and silently falls back to the default schedule (DefaultLockBackoff).
Primarily used by tests to bound timing with a sub-millisecond cadence (deterministic timing for contention tests without a minute-long wall clock). Production callers should leave the default (10/20/40/80/160/320/500ms cap) in place.
func WithLogger ¶
WithLogger sets the structured logger used by the Conn for diagnostic output. A nil logger is ignored — the existing logger (by default slog.Default) is preserved.
func WithMaxInflight ¶
WithMaxInflight sets the maximum number of concurrent outstanding requests on the Conn. The free-list tag allocator uses this as its channel capacity, so back-pressure kicks in at this value — once saturated, new requests block until an in-flight tag is released.
Values less than 1 are clamped to 1. Values greater than 65534 are clamped to 65534: NoTag (0xFFFF) is reserved for Tversion and is excluded from the free-list. Default: 64.
func WithMeter ¶ added in v1.4.3
func WithMeter(mp metric.MeterProvider) Option
WithMeter sets the MeterProvider used by the Conn for instrumentation. If nil, metrics are disabled.
func WithMsize ¶
WithMsize sets the proposed maximum message size. The default is 1 MiB (1 << 20), chosen to match the Linux kernel v9fs client for interop parity (see package documentation). The server's Rversion msize caps the proposal; the negotiated msize is min(client proposal, server cap).
No clamping is performed on the input — callers that proposed 0 or a value smaller than proto.HeaderSize will surface ErrMsizeTooSmall at Dial time, not here.
func WithRequestTimeout ¶
WithRequestTimeout sets a per-request timeout applied to the non-ctx File.Read, File.Write, File.ReadAt, and File.WriteAt methods. When set to a positive duration d, each call builds a context via context.WithTimeout with that duration; timeout expiry triggers Tflush via the standard roundTrip cancellation pipeline (Plan 22-02) and returns an error chain where errors.Is matches context.DeadlineExceeded.
The default (zero) means infinite wait — matches the Linux kernel v9fs client for trans=tcp mounts (Pitfall 9 / D-22). Callers that need per-op deadlines without a Conn-wide default use File.ReadCtx, File.WriteCtx, File.ReadAtCtx, File.WriteAtCtx with a caller-supplied ctx instead of this option.
Values <= 0 (zero or negative) are treated as "no timeout"; this keeps the surface Linux-v9fs-parallel and prevents accidental pathological short timeouts from callers passing a time.Duration literal with a zero-value or a subtraction overflow.
Per-op precedence: if a caller passes a ctx WITH a deadline to a *Ctx variant (e.g. File.ReadCtx), that ctx is used verbatim — WithRequestTimeout is ignored on the *Ctx methods.
func WithTracer ¶ added in v1.4.3
func WithTracer(tp trace.TracerProvider) Option
WithTracer sets the TracerProvider used by the Conn for instrumentation. If nil, tracing is disabled.
func WithVersion ¶ added in v1.4.0
WithVersion sets the protocol version to negotiate during Dial. When set, the client proposes this version and returns an error if the server negotiates any other version (including lower versions). This is useful for deterministic testing of protocol-specific logic.
When not set, the client proposes the highest supported version (proto.VersionL) and accepts whatever the server negotiates.
type Raw ¶
type Raw struct {
// contains filtered or unexported fields
}
Raw exposes direct 9P wire operations against a Conn. Unlike the high-level *File surface, Raw takes explicit fid arguments and does NOT track offsets or auto-clunk -- callers own fid lifecycle.
Obtain via Conn.Raw. Every Raw method issues exactly one T-message and blocks for its R-message.
Concurrency ¶
Raw methods are safe for concurrent use by multiple goroutines -- they delegate to Conn, which is goroutine-safe (database/sql.DB model per Phase 19 D-07). The Conn serializes wire emission via its write mutex, and the read loop routes responses by tag. This means N concurrent Raw.Write calls on the same fid dispatch N Twrite frames sequentially on the wire; the wins over sequential round-trips come from overlapping server processing, not from wire parallelism.
Fid ownership ¶
Raw does not call the fid allocator. Callers that want to bypass the *File handle and manage fid lifecycle explicitly must supply fid values from their own pool (e.g. a port of an existing 9P client that tracks fids in a parallel structure). Plan 20-03 will expose AcquireFid/ReleaseFid to integrate with the Conn's allocator.
func (*Raw) AcquireFid ¶
AcquireFid hands out a fresh fid from the Conn's allocator. Callers that use Raw for explicit fid lifecycle pair each AcquireFid with either a successful Raw.Clunk + Raw.ReleaseFid sequence, or a Raw.ReleaseFid alone if the fid never became server-bound (e.g. Walk/Open failed before the server registered the new fid).
Returns ErrFidExhausted if the per-Conn counter has run past proto.NoFid.
func (*Raw) Attach ¶
Attach mirrors the low-level Conn.AttachFid. Caller supplies fid. Kept at the "Attach" name on Raw because Raw's contract is explicit fid lifecycle; the high-level *File-returning Attach lives on *Conn as of Phase 20.
func (*Raw) Clunk ¶
Clunk mirrors Conn.Clunk. Releases the server-side fid binding; callers that allocated the fid from the Conn's allocator remain responsible for returning it to the allocator (Plan 20-03 wires this).
func (*Raw) Create ¶
func (r *Raw) Create(ctx context.Context, fid proto.Fid, name string, perm proto.FileMode, mode uint8, extension string) (proto.QID, uint32, error)
Create mirrors the low-level Conn.CreateFid (9P2000.u create-and- open wire op). Requires a .u-negotiated Conn. The high-level path-taking Create lives on *Conn as of Phase 20.
func (*Raw) Flush ¶
Flush mirrors Conn.Flush. Sends Tflush(oldTag); per the 9P spec the server always replies with Rflush regardless of whether oldTag matched an outstanding request.
func (*Raw) Lcreate ¶
func (r *Raw) Lcreate(ctx context.Context, fid proto.Fid, name string, flags uint32, mode proto.FileMode, gid uint32) (proto.QID, uint32, error)
Lcreate mirrors Conn.Lcreate. Requires a 9P2000.L-negotiated Conn.
func (*Raw) Lopen ¶
Lopen mirrors Conn.Lopen. Requires a 9P2000.L-negotiated Conn; on a .u Conn returns ErrNotSupported.
func (*Raw) Open ¶
Open mirrors Conn.Open. Requires a 9P2000.u-negotiated Conn; on a .L Conn returns ErrNotSupported.
func (*Raw) ReleaseFid ¶
ReleaseFid returns fid to the Conn's allocator reuse cache. Does not touch the wire; pair with Raw.Clunk when the fid is server-bound.
Ordering: when fid IS server-bound, callers MUST wait for the corresponding Rclunk to be received BEFORE calling ReleaseFid. See 20-RESEARCH.md §9 Pitfall 6.
func (*Raw) Tgetattr ¶
Tgetattr requests the subset of file attributes selected by mask from fid. Callers typically pass proto.AttrBasic (0x7ff) for the common case; callers who need only Size or Mode can narrow the mask to amortize server work. The server is permitted to return MORE than requested.
Requires a 9P2000.L-negotiated Conn. High-level File.Stat sugar lives in plan 21-04.
func (*Raw) Tgetlock ¶
func (r *Raw) Tgetlock(ctx context.Context, fid proto.Fid, lt proto.LockType, start, length uint64, procID uint32, clientID string) (p9l.Rgetlock, error)
Tgetlock tests whether the described POSIX lock could be placed, returning the conflicting lock holder's parameters (or LockTypeUnlck when the region is free).
The return value is p9l.Rgetlock by value — Rgetlock carries only value- typed fields (LockType + uint64x2 + uint32 + string), which are naturally passed by value and keep the cache-hit contract consistent (Rgetlock is uncached per client/msgcache.go).
Requires a 9P2000.L-negotiated Conn. High-level File.GetLock sugar lives in plan 21-04.
func (*Raw) Tlink ¶
Tlink creates a hard link named name in directory dfid pointing at the file referenced by fid. Both fids must resolve within the same file tree; cross-mount links are rejected by the server.
Requires a 9P2000.L-negotiated Conn. Higher-level Conn.Link sugar lives in plan 21-02.
func (*Raw) Tlock ¶
func (r *Raw) Tlock(ctx context.Context, fid proto.Fid, lt proto.LockType, flags proto.LockFlags, start, length uint64, procID uint32, clientID string) (proto.LockStatus, error)
Tlock issues a POSIX byte-range lock operation against fid and returns the server's lock status (OK, Blocked, Error, Grace). proto.LockType is one of LockTypeRdLck / LockTypeWrLck / LockTypeUnlck. Flags may request blocking or reclaim semantics; start+length define the byte range; procID and clientID identify the lock holder.
Requires a 9P2000.L-negotiated Conn. High-level blocking File.Lock with ctx-driven poll/backoff lives in plan 21-04.
func (*Raw) Tmknod ¶
func (r *Raw) Tmknod(ctx context.Context, dfid proto.Fid, name string, mode, major, minor, gid uint32) (proto.QID, error)
Tmknod creates a device node named name in directory dfid. Mode carries POSIX mode + device-type bits; major/minor select the device; gid sets the owning group. Returns the new node's QID.
Requires a 9P2000.L-negotiated Conn. Higher-level Conn.Mknod sugar lives in plan 21-02.
func (*Raw) Treadlink ¶
Treadlink returns the target path of the symbolic link referenced by fid. Requires a 9P2000.L-negotiated Conn; returns a wrapped ErrNotSupported on .u.
Higher-level File.Readlink sugar lives in plan 21-02.
func (*Raw) Tremove ¶
Tremove removes the file associated with fid and invalidates the fid. The 9P spec states Tremove clunks fid regardless of whether the removal succeeded server-side; callers MUST NOT issue a subsequent Raw.Clunk on this fid, and should release the fid to the allocator via Raw.ReleaseFid once this call returns (error or not).
Dialect-neutral — Tremove ships on both 9P2000.L and 9P2000.u wire sets (see proto/messages.go). See 21-RESEARCH.md Pitfall 7.
func (*Raw) Trename ¶
Trename moves the entry referenced by fid into directory dfid under name. Semantically equivalent to the .L Trename wire op (single-fid rename): fid keeps pointing at the renamed object, dfid is the target directory.
Requires a 9P2000.L-negotiated Conn. High-level path-rooted Conn.Rename sugar lives in plan 21-02.
func (*Raw) Trenameat ¶
func (r *Raw) Trenameat(ctx context.Context, oldDirFid proto.Fid, oldName string, newDirFid proto.Fid, newName string) error
Trenameat moves oldName in oldDirFid to newName under newDirFid. Unlike Raw.Trename, both source and target dirs are addressed by fid and no fid is held against the object being renamed — no open-fid invalidation concerns on this path (Pitfall 5 applies only to long-lived open fids).
Requires a 9P2000.L-negotiated Conn. Higher-level sugar lives in plan 21-02.
func (*Raw) Tsetattr ¶
Tsetattr mutates the attributes selected by attr.Valid on the file referenced by fid. Unset-bit fields are ignored server-side.
Requires a 9P2000.L-negotiated Conn. High-level File.Chmod/File.Chown/ File.Truncate sugar lives in plan 21-04.
func (*Raw) Tstat ¶
Tstat returns the .u-format stat structure for the file referenced by fid. The 9P2000.u Stat carries the legacy 16-field layout (Name/UID/GID strings + numeric IDs in the .u extension fields).
Requires a 9P2000.u-negotiated Conn; returns a wrapped ErrNotSupported on .L where Raw.Tgetattr is the dialect equivalent. The unified File.Stat in plan 21-04 picks the correct wire op from c.Dialect().
func (*Raw) Tstatfs ¶
Tstatfs returns filesystem statistics for the file tree containing fid. The returned proto.FSStat is by value (not pointer) — Pitfall 8: FSStat is a small fixed-size struct, value return avoids an escape and keeps the cache-lifetime contract local to this method.
Requires a 9P2000.L-negotiated Conn. High-level File.Statfs sugar lives in plan 21-04.
func (*Raw) Tsymlink ¶
func (r *Raw) Tsymlink(ctx context.Context, dfid proto.Fid, name, target string, gid uint32) (proto.QID, error)
Tsymlink creates a symbolic link named name with the given target in the directory referenced by dfid. Returns the new symlink's QID. Requires a 9P2000.L-negotiated Conn; returns a wrapped ErrNotSupported on .u.
Higher-level path-rooted sugar lives at Conn.Symlink (plan 21-02).
func (*Raw) Tunlinkat ¶
Tunlinkat removes the entry name from directory dirFid. Flags may include AT_REMOVEDIR (0x200) to request directory removal; otherwise the entry must be a non-directory. See Pitfall 9 for flag encoding notes.
Requires a 9P2000.L-negotiated Conn. Higher-level Conn.Remove sugar lives in plan 21-02.
func (*Raw) Txattrcreate ¶
func (r *Raw) Txattrcreate(ctx context.Context, fid proto.Fid, name string, attrSize uint64, flags uint32) error
Txattrcreate prepares fid for an xattr-write sequence: the server mutates fid to the xattr-write state, and the caller MUST follow with Raw.Write calls that append the attribute value (total bytes = attrSize), then a final Raw.Clunk to commit.
The caller's fid is invalidated by the final Clunk — DO NOT reuse the same fid value afterwards (pair the Clunk with Raw.ReleaseFid). See 21-RESEARCH.md Pitfall 1.
Requires a 9P2000.L-negotiated Conn.
func (*Raw) Txattrwalk ¶
Txattrwalk opens an xattr-read fid (newFid) bound to fid + name, returning the server-declared size of the attribute value. Size is returned verbatim from Rxattrwalk — callers that allocate a buffer MUST bound size against a safe ceiling (high-level File.XattrGet in plan 21-03 clamps against proto.MaxDataSize). See 21-RESEARCH.md Pitfall 2.
Requires a 9P2000.L-negotiated Conn.
type Session ¶ added in v1.4.1
type Session struct {
// contains filtered or unexported fields
}
Session manages a stateful 9P connection that handles automatic reconnection with backoff.
func NewSession ¶ added in v1.4.1
NewSession returns a new Session that uses the provided dialer to establish connections.
func NewSessionWithOptions ¶ added in v1.4.1
func NewSessionWithOptions(dialer func(ctx context.Context) (net.Conn, error), opts []Option, sopts ...SessionOption) *Session
NewSessionWithOptions returns a new Session with the provided dialer and session options.
func (*Session) Conn ¶ added in v1.4.1
Conn returns a live *Conn. If the current connection is nil or closed, it re-establishes the connection using the Session's dialer.
Conn is safe for concurrent use. Multiple goroutines calling Conn simultaneously when a connection is needed will only trigger one dialer call; the others will block until the connection is ready.
On dialer or handshake failure, Conn retries with exponential backoff (default 10/20/40/80/160/320/500ms cap) until a connection is established or the provided context is cancelled.
type SessionOption ¶ added in v1.4.1
type SessionOption func(*Session)
SessionOption configures a Session.
func WithOnReconnect ¶ added in v1.4.1
func WithOnReconnect(fn func(context.Context, *Conn) error) SessionOption
WithOnReconnect sets a callback to be invoked every time a new connection is established (including the initial connection). If the callback returns an error, the connection is closed and reconnection is retried.
Source Files
¶
- accessors.go
- advanced_link.go
- advanced_lock.go
- advanced_mknod.go
- advanced_remove.go
- advanced_rename.go
- advanced_setattr.go
- advanced_stat.go
- advanced_statfs.go
- advanced_symlink.go
- advanced_xattr.go
- close.go
- conn.go
- dial.go
- dialect.go
- doc.go
- errors.go
- fidalloc.go
- file.go
- flush.go
- inflight.go
- msgcache.go
- ops.go
- options.go
- otel.go
- path.go
- raw.go
- raw_advanced.go
- read_loop.go
- readdir.go
- session.go
- session_manager.go
- sync.go
- tags.go
- write.go
Directories
¶
| Path | Synopsis |
|---|---|
|
Package clienttest provides test harness helpers that pair a ninep server and client over net.Pipe for integration tests.
|
Package clienttest provides test harness helpers that pair a ninep server and client over net.Pipe for integration tests. |