diff --git a/cmd/ollie-9p-mount/main.go b/cmd/ollie-9p-mount/main.go deleted file mode 100644 index d95f2b6..0000000 --- a/cmd/ollie-9p-mount/main.go +++ /dev/null @@ -1,98 +0,0 @@ -// ollie-9p-mount — per-session FUSE proxy that attaches to olliesrv with a custom uname. -// -// Each agent session gets its own mount. Tools running inside the session see -// the full ollie namespace through $OLLIE, but all 9P operations carry the -// session's identity (Uname). The server can then enforce access control -// (e.g. rejecting self-prompts). -// -// Usage: -// -// ollie-9p-mount [-u uname] [-s service] [-addr address] -// -// If -u is not given, $OLLIE_UNAME is used. -package main - -import ( - "flag" - "fmt" - "log" - "os" - "os/signal" - "syscall" - - "9fans.net/go/plan9/client" - "github.com/hanwen/go-fuse/v2/fs" - "github.com/hanwen/go-fuse/v2/fuse" - perms "olliesrv/fs" -) - -var ( - userFlag = flag.String("u", "", "uname for 9P attach (default: $OLLIE_SESSION_ID)") - serviceFlag = flag.String("s", "ollie", "9P service name") - netFlag = flag.String("net", "", "network type (unix, tcp); empty = namespace") - addrFlag = flag.String("addr", "", "9P address; empty = namespace service") -) - -func main() { - flag.Parse() - if flag.NArg() != 1 { - fmt.Fprintf(os.Stderr, "usage: ollie-9p-mount [-u uname] [-s service] \n") - os.Exit(1) - } - mnt := flag.Arg(0) - - uname := *userFlag - if uname == "" { - uname = os.Getenv("OLLIE_UNAME") - } - if uname == "" { - fmt.Fprintf(os.Stderr, "ollie-9p-mount: no uname (-u or $OLLIE_UNAME)\n") - os.Exit(1) - } - - fsys, err := dial(uname) - if err != nil { - log.Fatalf("dial: %v", err) - } - - os.MkdirAll(mnt, perms.PermMkdir) - root := &p9Dir{fsys: fsys, path: ""} - server, err := fs.Mount(mnt, root, &fs.Options{ - MountOptions: fuse.MountOptions{ - FsName: "ollie-9p", - Name: "ollie", - DisableXAttrs: true, - }, - }) - if err != nil { - log.Fatalf("mount: %v", err) - } - - // Unmount on signal. - sig := make(chan os.Signal, 1) - signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) - go func() { - <-sig - server.Unmount() - }() - - server.Wait() -} - -func dial(uname string) (*client.Fsys, error) { - var conn *client.Conn - var err error - if *addrFlag != "" { - n := *netFlag - if n == "" { - n = "tcp" - } - conn, err = client.Dial(n, *addrFlag) - } else { - conn, err = client.DialService(*serviceFlag) - } - if err != nil { - return nil, err - } - return conn.Attach(nil, uname, "") -} diff --git a/main.go b/main.go index f3c9010..55b6ef1 100644 --- a/main.go +++ b/main.go @@ -7,7 +7,6 @@ import ( "fmt" "net" "os" - "os/exec" "os/signal" "path/filepath" "strings" @@ -23,6 +22,7 @@ import ( "ollie/pkg/paths" "ollie/pkg/tools/execute" fs "olliesrv/fs" + "olliesrv/mount" "olliesrv/server" "olliesrv/session" ) @@ -125,15 +125,15 @@ func cmdMount() { if strings.Contains(addr, ":") { network = "tcp" } - exe, _ := os.Executable() - cmd := exec.Command(filepath.Join(filepath.Dir(exe), "ollie-9p-mount"), - "-u", os.Getenv("USER"), "-net", network, "-addr", addr, mnt) - cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true} - if err := cmd.Start(); err != nil { - fmt.Fprintf(os.Stderr, "ollie-9p-mount: %v\n", err) + 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) } - fmt.Printf("mounted %s at %s (pid %d)\n", addr, mnt, cmd.Process.Pid) } func runServer(sockPath string) { 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/cmd/ollie-9p-mount/node.go b/mount/node.go similarity index 99% rename from cmd/ollie-9p-mount/node.go rename to mount/node.go index fb58f62..33a0ebf 100644 --- a/cmd/ollie-9p-mount/node.go +++ b/mount/node.go @@ -1,4 +1,4 @@ -package main +package mount import ( "context" diff --git a/session/mount.go b/session/mount.go deleted file mode 100644 index ff3d5c8..0000000 --- a/session/mount.go +++ /dev/null @@ -1,122 +0,0 @@ -package session - -import ( - "context" - "os" - "os/exec" - "path/filepath" - "sync" - "syscall" - "time" - - "ollie/pkg/tools/execute" -) - -// mountState holds the per-session mount process state. -type mountState struct { - mu sync.RWMutex - proc *os.Process - path string - envExtra map[string]string // reference to Server's envExtra for OLLIE_UNAME lookup -} - -// MountPathFor returns the per-session FUSE mount path for a given uname. -func MountPathFor(uname string) string { - return filepath.Join(os.TempDir(), "ollie-mount-"+uname) -} - -// WithMount9P returns an execute.Option that enables per-session 9P FUSE mounts. -// It wires up hooks to start, check, and tear down the mount lifecycle. -func WithMount9P() execute.Option { - state := &mountState{envExtra: make(map[string]string)} - - return func(s *execute.Server) { - // Hook: OnEnvSet - start mount when OLLIE_UNAME is set - s.OnEnvSet = func(key, value string) { - state.mu.Lock() - state.envExtra[key] = value - if key == "OLLIE_UNAME" && value != "" { - startMount(state, value) - } - state.mu.Unlock() - } - - // Hook: OnPreDispatch - check mount health before each dispatch - s.OnPreDispatch = func() { - remountIfStale(state) - } - - // Hook: OnClose - tear down mount on session end - s.OnClose = func() { - teardownMount(state) - } - } -} - -// startMount launches ollie-9p-mount for this session. -func startMount(state *mountState, uname string) { - bin, err := exec.LookPath("ollie-9p-mount") - if err != nil { - return - } - mnt := MountPathFor(uname) - os.MkdirAll(mnt, 0755) //nolint:errcheck - cmd := exec.Command(bin, "-u", uname, mnt) - cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} - if err := cmd.Start(); err != nil { - return - } - // Reap in background so Wait works later. - go cmd.Wait() //nolint:errcheck - // Wait for mount to be ready. - for i := 0; i < 20; i++ { - time.Sleep(50 * time.Millisecond) - if _, err := os.Stat(filepath.Join(mnt, "s")); err == nil { - break - } - } - state.proc = cmd.Process - state.path = mnt - state.envExtra["OLLIE"] = mnt -} - -// remountIfStale checks the per-session FUSE mount health and re-establishes it if broken. -func remountIfStale(state *mountState) { - state.mu.RLock() - mnt := state.path - proc := state.proc - state.mu.RUnlock() - if mnt == "" || proc == nil { - return - } - if _, err := os.Stat(filepath.Join(mnt, "s")); err == nil { - return - } - // Mount is stale — tear down and restart. - state.mu.Lock() - defer state.mu.Unlock() - exec.Command("fusermount", "-u", state.path).Run() //nolint:errcheck - syscall.Kill(state.proc.Pid, syscall.SIGKILL) //nolint:errcheck - state.proc = nil - state.path = "" - if uname := state.envExtra["OLLIE_UNAME"]; uname != "" { - startMount(state, uname) - } -} - -// teardownMount tears down the per-session FUSE mount. -func teardownMount(state *mountState) { - state.mu.Lock() - proc := state.proc - mnt := state.path - state.proc = nil - state.path = "" - state.mu.Unlock() - if proc != nil { - ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) - exec.CommandContext(ctx, "fusermount", "-u", mnt).Run() //nolint:errcheck - cancel() - syscall.Kill(proc.Pid, syscall.SIGKILL) //nolint:errcheck - os.Remove(mnt) //nolint:errcheck - } -} diff --git a/session/session.go b/session/session.go index 05aae4e..202ad5e 100644 --- a/session/session.go +++ b/session/session.go @@ -40,7 +40,7 @@ type Session struct { prevPrompt []byte // last submitted prompt; overwritten on each new submission peers map[string]bool // peer session IDs (bidirectional links) remote string // SSH target for remote execution (empty = local) - mount *mountState // non-nil for remote sessions that need a local mount + // Cached model list (expensive API call; refreshed every 24h). modelsMu sync.Mutex @@ -853,7 +853,6 @@ func (s *Manager) restoreSession(ps *agent.PersistedSession) error { } else { var execOpts []execute.Option if !s.cfg.NoMount { - execOpts = append(execOpts, WithMount9P()) } if s.cfg.Strict { execOpts = append(execOpts, execute.WithStrict()) @@ -972,9 +971,7 @@ func (s *Manager) Shutdown() { if sess != nil { sess.Cancel() sess.Core.Close() - if sess.mount != nil { - teardownMount(sess.mount) - } + s.cfg.Log.Info("shutdown session %s", id) } } @@ -1009,9 +1006,6 @@ func (s *Manager) KillSession(id string) { if sess != nil { sess.Cancel() sess.Core.Close() - if sess.mount != nil { - teardownMount(sess.mount) - } s.removePersistedSession(id) s.cfg.Log.Info("killed session %s", id) if s.cfg.OnSessionKilled != nil { @@ -1141,7 +1135,6 @@ func (s *Manager) CreateSession(args []string) (string, error) { var execOpts []execute.Option if !s.cfg.NoMount { - execOpts = append(execOpts, WithMount9P()) } if s.cfg.Strict { execOpts = append(execOpts, execute.WithStrict())