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 } }