diff --git a/cmd/olliesrv/main.go b/cmd/olliesrv/main.go index 3f8074a..4f41779 100644 --- a/cmd/olliesrv/main.go +++ b/cmd/olliesrv/main.go @@ -21,6 +21,7 @@ import ( "ollie/env" "ollie/fs/session" olog "ollie/log" + "ollie/mount" "ollie/paths" "ollie/skills" "ollie/toolsrv" @@ -89,6 +90,11 @@ func (c *ModelCache) Invalidate() { func main() { flag.CommandLine.Parse(os.Args[1:]) //nolint:errcheck + // Handle mount subcommand (kept as a convenience) + if flag.NArg() > 0 && flag.Arg(0) == "mount" { + cmdMount() + return + } ns := client.Namespace() if ns == "" { @@ -100,7 +106,35 @@ func main() { runServer(sockPath) } - +func cmdMount() { + if flag.NArg() < 1 { + fmt.Fprintln(os.Stderr, "usage: olliesrv mount
[mountpoint]") + os.Exit(1) + } + addr := flag.Arg(0) + mnt := flag.Arg(1) + if mnt == "" { + home, _ := os.UserHomeDir() + mnt = filepath.Join(home, "mnt", addr) + } + if err := os.MkdirAll(mnt, fs.PermMkdir); err != nil { + fmt.Fprintf(os.Stderr, "cannot create mount dir: %v\n", err) + os.Exit(1) + } + network := "unix" + if strings.Contains(addr, ":") { + network = "tcp" + } + fmt.Printf("mounting %s at %s\n", addr, mnt) + if err := mount.Run(mount.Config{ + Network: network, + Address: addr, + Mountpoint: mnt, + }); err != nil { + fmt.Fprintf(os.Stderr, "mount: %v\n", err) + os.Exit(1) + } +} func runServer(sockPath string) { env.EnsureDefaults() diff --git a/go.mod b/go.mod index 24744f3..0c4e430 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,10 @@ go 1.25.6 require ( 9fans.net/go v0.0.7 github.com/godbus/dbus/v5 v5.1.0 + github.com/hanwen/go-fuse/v2 v2.10.1 github.com/simonfxr/pubsub v0.0.5 golang.org/x/time v0.15.0 gopkg.in/yaml.v3 v3.0.1 ) + +require golang.org/x/sys v0.47.0 // indirect diff --git a/go.sum b/go.sum index e7854e9..fb92932 100644 --- a/go.sum +++ b/go.sum @@ -7,6 +7,12 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/hanwen/go-fuse/v2 v2.10.1 h1:QAqZuc9+aBtTou+OPruU/hkYQYCkgPtQd2QaepHkTTs= +github.com/hanwen/go-fuse/v2 v2.10.1/go.mod h1:aU7NkGYZUmuJrZapoI3mEcNve7PZTySUOLBuch/vR6U= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/moby/sys/mountinfo v0.7.2 h1:1shs6aH5s4o5H2zQLn796ADW1wMrIwHsyJ2v9KouLrg= +github.com/moby/sys/mountinfo v0.7.2/go.mod h1:1YOa8w8Ih7uW0wALDUgT1dTTSBrZ+HiBLGws92L2RU4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/simonfxr/pubsub v0.0.5 h1:DJfvFoglqGvwJriIOC5NI5um34n2YX8KAU5+7jv768w= @@ -36,6 +42,8 @@ golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210415045647-66c3f260301c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno= diff --git a/mount/mount.go b/mount/mount.go new file mode 100644 index 0000000..669959f --- /dev/null +++ b/mount/mount.go @@ -0,0 +1,65 @@ +// Package mount provides FUSE-based 9P mount functionality. +// Used by `olliesrv mount` for network-transparent access to remote servers. +package mount + +import ( + "fmt" + "os" + "os/signal" + "syscall" + + "9fans.net/go/plan9/client" + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" +) + +// Config configures a FUSE mount. +type Config struct { + Network string // "unix" or "tcp" + Address string // server address + Mountpoint string // local mountpoint + Uname string // uname for Tattach (default: $USER) +} + +// Run mounts the remote 9P server and blocks until unmounted or signaled. +func Run(cfg Config) error { + if cfg.Uname == "" { + cfg.Uname = os.Getenv("USER") + } + if cfg.Uname == "" { + cfg.Uname = "none" + } + + conn, err := client.Dial(cfg.Network, cfg.Address) + if err != nil { + return fmt.Errorf("dial %s!%s: %w", cfg.Network, cfg.Address, err) + } + fsys, err := conn.Attach(nil, cfg.Uname, "") + if err != nil { + conn.Close() + return fmt.Errorf("attach: %w", err) + } + + root := &p9Dir{fsys: fsys, path: "/"} + server, err := fs.Mount(cfg.Mountpoint, root, &fs.Options{ + MountOptions: fuse.MountOptions{ + FsName: "ollie", + Name: "ollie9p", + DisableXAttrs: true, + MaxBackground: 32, + }, + }) + if err != nil { + return fmt.Errorf("mount %s: %w", cfg.Mountpoint, err) + } + + sigCh := make(chan os.Signal, 1) + signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM) + go func() { + <-sigCh + server.Unmount() + }() + + server.Wait() + return nil +} diff --git a/mount/node.go b/mount/node.go new file mode 100644 index 0000000..33a0ebf --- /dev/null +++ b/mount/node.go @@ -0,0 +1,271 @@ +package mount + +import ( + "context" + "os/user" + "strconv" + "sync" + "syscall" + + "9fans.net/go/plan9" + "9fans.net/go/plan9/client" + "github.com/hanwen/go-fuse/v2/fs" + "github.com/hanwen/go-fuse/v2/fuse" +) + +// p9Dir is a FUSE directory node backed by a 9P path. +type p9Dir struct { + fs.Inode + fsys *client.Fsys + path string // 9P path (empty string = root) +} + +// p9File is a FUSE file node backed by a 9P path. +type p9File struct { + fs.Inode + fsys *client.Fsys + path string + mu sync.Mutex +} + +var _ = (fs.NodeGetattrer)((*p9Dir)(nil)) +var _ = (fs.NodeReaddirer)((*p9Dir)(nil)) +var _ = (fs.NodeLookuper)((*p9Dir)(nil)) +var _ = (fs.NodeCreater)((*p9Dir)(nil)) +var _ = (fs.NodeUnlinker)((*p9Dir)(nil)) +var _ = (fs.NodeMkdirer)((*p9Dir)(nil)) +var _ = (fs.NodeRmdirer)((*p9Dir)(nil)) +var _ = (fs.NodeRenamer)((*p9Dir)(nil)) + +var _ = (fs.NodeGetattrer)((*p9File)(nil)) +var _ = (fs.NodeOpener)((*p9File)(nil)) +var _ = (fs.NodeReader)((*p9File)(nil)) +var _ = (fs.NodeWriter)((*p9File)(nil)) +var _ = (fs.NodeSetattrer)((*p9File)(nil)) + +func p9path(base, name string) string { + if base == "" { + return name + } + return base + "/" + name +} + +// --- p9Dir --- + +func (d *p9Dir) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno { + path := d.path + if path == "" { + path = "/" + } + dir, err := d.fsys.Stat(path) + if err != nil { + return syscall.ENOENT + } + out.Mode = uint32(dir.Mode)&0777 | syscall.S_IFDIR + out.Uid = lookupUid(dir.Uid) + out.Gid = lookupGid(dir.Gid) + return 0 +} + +func (d *p9Dir) Readdir(ctx context.Context) (fs.DirStream, syscall.Errno) { + path := d.path + if path == "" { + path = "/" + } + fid, err := d.fsys.Open(path, plan9.OREAD) + if err != nil { + return nil, syscall.EIO + } + defer fid.Close() + dirs, err := fid.Dirreadall() + if err != nil { + return nil, syscall.EIO + } + entries := make([]fuse.DirEntry, len(dirs)) + for i, dir := range dirs { + m := uint32(dir.Mode) & 0777 + if dir.Mode&plan9.DMDIR != 0 { + m |= syscall.S_IFDIR + } + entries[i] = fuse.DirEntry{Name: dir.Name, Mode: m} + } + return fs.NewListDirStream(entries), 0 +} + +func (d *p9Dir) Lookup(ctx context.Context, name string, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { + child := p9path(d.path, name) + dir, err := d.fsys.Stat(child) + if err != nil { + return nil, syscall.ENOENT + } + out.Uid = lookupUid(dir.Uid) + out.Gid = lookupGid(dir.Gid) + if dir.Mode&plan9.DMDIR != 0 { + out.Mode = uint32(dir.Mode)&0777 | syscall.S_IFDIR + node := &p9Dir{fsys: d.fsys, path: child} + return d.NewInode(ctx, node, fs.StableAttr{Mode: syscall.S_IFDIR}), 0 + } + out.Mode = uint32(dir.Mode) & 0777 + out.Size = dir.Length + node := &p9File{fsys: d.fsys, path: child} + return d.NewInode(ctx, node, fs.StableAttr{}), 0 +} + +func (d *p9Dir) Create(ctx context.Context, name string, flags uint32, mode uint32, out *fuse.EntryOut) (*fs.Inode, fs.FileHandle, uint32, syscall.Errno) { + child := p9path(d.path, name) + fid, err := d.fsys.Create(child, plan9.OWRITE, plan9.Perm(mode&0777)) + if err != nil { + return nil, nil, 0, syscall.EIO + } + fid.Close() + out.Mode = mode & 0777 + node := &p9File{fsys: d.fsys, path: child} + inode := d.NewInode(ctx, node, fs.StableAttr{}) + return inode, nil, fuse.FOPEN_DIRECT_IO, 0 +} + +func (d *p9Dir) Unlink(ctx context.Context, name string) syscall.Errno { + child := p9path(d.path, name) + if err := d.fsys.Remove(child); err != nil { + return syscall.EIO + } + return 0 +} + +func (d *p9Dir) Mkdir(ctx context.Context, name string, mode uint32, out *fuse.EntryOut) (*fs.Inode, syscall.Errno) { + child := p9path(d.path, name) + fid, err := d.fsys.Create(child, plan9.OREAD, plan9.DMDIR|plan9.Perm(mode&0777)) + if err != nil { + return nil, syscall.EIO + } + fid.Close() + out.Mode = mode&0777 | syscall.S_IFDIR + node := &p9Dir{fsys: d.fsys, path: child} + return d.NewInode(ctx, node, fs.StableAttr{Mode: syscall.S_IFDIR}), 0 +} + +func (d *p9Dir) Rmdir(ctx context.Context, name string) syscall.Errno { + child := p9path(d.path, name) + if err := d.fsys.Remove(child); err != nil { + return syscall.EIO + } + return 0 +} + +func (d *p9Dir) Rename(ctx context.Context, name string, newParent fs.InodeEmbedder, newName string, flags uint32) syscall.Errno { + child := p9path(d.path, name) + var dir plan9.Dir + dir.Null() + dir.Name = newName + if err := d.fsys.Wstat(child, &dir); err != nil { + return syscall.EIO + } + return 0 +} + +// --- p9File --- + +func (f *p9File) Getattr(ctx context.Context, fh fs.FileHandle, out *fuse.AttrOut) syscall.Errno { + dir, err := f.fsys.Stat(f.path) + if err != nil { + return syscall.ENOENT + } + out.Mode = uint32(dir.Mode) & 0777 + out.Size = dir.Length + out.Uid = lookupUid(dir.Uid) + out.Gid = lookupGid(dir.Gid) + return 0 +} + +func (f *p9File) Setattr(ctx context.Context, fh fs.FileHandle, in *fuse.SetAttrIn, out *fuse.AttrOut) syscall.Errno { + // Accept truncate silently — 9P files are append/overwrite on write. + dir, err := f.fsys.Stat(f.path) + if err != nil { + return syscall.ENOENT + } + out.Mode = uint32(dir.Mode) & 0777 + return 0 +} + +func (f *p9File) Open(ctx context.Context, flags uint32) (fs.FileHandle, uint32, syscall.Errno) { + return nil, fuse.FOPEN_DIRECT_IO, 0 +} + +func (f *p9File) Read(ctx context.Context, fh fs.FileHandle, dest []byte, off int64) (fuse.ReadResult, syscall.Errno) { + f.mu.Lock() + defer f.mu.Unlock() + fid, err := f.fsys.Open(f.path, plan9.OREAD) + if err != nil { + return nil, syscall.EIO + } + defer fid.Close() + if off > 0 { + buf := make([]byte, min(off, 8192)) + remaining := off + for remaining > 0 { + n := int64(len(buf)) + if n > remaining { + n = remaining + } + nr, err := fid.Read(buf[:n]) + if err != nil || nr == 0 { + break + } + remaining -= int64(nr) + } + } + n, err := fid.Read(dest) + if n == 0 { + return fuse.ReadResultData(nil), 0 + } + return fuse.ReadResultData(dest[:n]), 0 +} + +func (f *p9File) Write(ctx context.Context, fh fs.FileHandle, data []byte, off int64) (uint32, syscall.Errno) { + f.mu.Lock() + defer f.mu.Unlock() + fid, err := f.fsys.Open(f.path, plan9.OWRITE) + if err != nil { + return 0, syscall.EACCES + } + n, err := fid.Write(data) + if err != nil { + fid.Close() + return 0, syscall.EIO + } + if err := fid.Close(); err != nil { + return 0, syscall.EACCES + } + return uint32(n), 0 +} + +func min(a, b int64) int64 { + if a < b { + return a + } + return b +} + +func lookupUid(name string) uint32 { + if id, err := strconv.ParseUint(name, 10, 32); err == nil { + return uint32(id) + } + if u, err := user.Lookup(name); err == nil { + if id, err := strconv.ParseUint(u.Uid, 10, 32); err == nil { + return uint32(id) + } + } + return 0 +} + +func lookupGid(name string) uint32 { + if id, err := strconv.ParseUint(name, 10, 32); err == nil { + return uint32(id) + } + if g, err := user.LookupGroup(name); err == nil { + if id, err := strconv.ParseUint(g.Gid, 10, 32); err == nil { + return uint32(id) + } + } + return 0 +}