From d0ce8a108ec684064fc420fdd583ab7d13c85066 Mon Sep 17 00:00:00 2001 From: Levi Neely <141506390+lneely@users.noreply.github.com> Date: Tue, 19 May 2026 18:10:36 +0200 Subject: [PATCH] session/session.go: session logic in its own package - session.Manager (was SessionManager) - session.ManagerConfig (was SessionManagerConfig) - session.Session, session.NewSession - session.FileStore, session.NewFileStore - session.FileMode, session.FileList - session.FormatEvent, session.FormatParams, session.ParseParams - server.go imports olliesrv/session - Clean package boundary: session/ = lifecycle, store/ = abstraction --- main_test.go | 121 ++++++++++++----------- server.go | 19 ++-- session.go => session/session.go | 66 ++++++------- sessionfile.go => session/sessionfile.go | 16 +-- 4 files changed, 112 insertions(+), 110 deletions(-) rename session.go => session/session.go (91%) rename sessionfile.go => session/sessionfile.go (97%) diff --git a/main_test.go b/main_test.go index fa8967b..4a40782 100644 --- a/main_test.go +++ b/main_test.go @@ -13,6 +13,7 @@ import ( "ollie/pkg/agent" "ollie/pkg/backend" olog "ollie/pkg/log" + "olliesrv/session" "olliesrv/store" ) @@ -106,15 +107,15 @@ func (c *blockingCore) Submit(ctx context.Context, input string, handler agent.E func testSink() *olog.Sink { return olog.NewSink(io.Discard, io.Discard, olog.LevelError) } -func testSession(id string) *Session { +func testSession(id string) *session.Session { ctx, cancel := context.WithCancel(context.Background()) - return NewSession(id, &stubCore{state: "idle", backend_: "stub", model: "m", agentName: "default", cwd: "/tmp"}, ctx, cancel) + return session.NewSession(id, &stubCore{state: "idle", backend_: "stub", model: "m", agentName: "default", cwd: "/tmp"}, ctx, cancel) } -func newTestSessionManager(t *testing.T) *SessionManager { +func newTestSessionManager(t *testing.T) *session.Manager { t.Helper() sink := testSink() - return NewSessionManager(SessionManagerConfig{ + return session.NewManager(session.ManagerConfig{ Log: sink.NewLogger("test"), Sink: sink, ReadFile: func(string) ([]byte, error) { return []byte("#!/bin/sh\n"), nil }, @@ -122,10 +123,10 @@ func newTestSessionManager(t *testing.T) *SessionManager { }) } -func newTestSessionManagerWithCore(t *testing.T) *SessionManager { +func newTestSessionManagerWithCore(t *testing.T) *session.Manager { t.Helper() sink := testSink() - return NewSessionManager(SessionManagerConfig{ + return session.NewManager(session.ManagerConfig{ Log: sink.NewLogger("test"), Sink: sink, ReadFile: func(string) ([]byte, error) { return []byte("#!/bin/sh\n"), nil }, @@ -157,11 +158,11 @@ var _ = strings.Contains // suppress unused import var _ = fmt.Sprintf // suppress unused import var _ = os.Remove // suppress unused import -func newTestSessionFileStore(t *testing.T, sess *Session) (*SessionFileStore, *stubCore) { +func newTestSessionFileStore(t *testing.T, sess *session.Session) (*session.FileStore, *stubCore) { t.Helper() sink := testSink() core := sess.Core.(*stubCore) - sf := NewSessionFileStore(sess, sink.NewLogger("test"), + sf := session.NewFileStore(sess, sink.NewLogger("test"), func() {}, func(id string) error { return nil }, func(data []byte) error { return nil }, @@ -169,12 +170,12 @@ func newTestSessionFileStore(t *testing.T, sess *Session) (*SessionFileStore, *s return sf, core } -func newTestSessionFileStoreWith(t *testing.T, sess *Session, kill func(), rename func(string) error, save func([]byte) error) *SessionFileStore { +func newTestSessionFileStoreWith(t *testing.T, sess *session.Session, kill func(), rename func(string) error, save func([]byte) error) *session.FileStore { t.Helper() sink := testSink() - return NewSessionFileStore(sess, sink.NewLogger("test"), kill, rename, save) + return session.NewFileStore(sess, sink.NewLogger("test"), kill, rename, save) } -// ===== Session ===== +// ===== session.Session ===== func TestSessionAppendLog(t *testing.T) { sess := testSession("s1") @@ -194,15 +195,15 @@ func TestSessionAppendLog(t *testing.T) { } func TestSessionManagerFileMode(t *testing.T) { - if m, ok := SessionManagerFileMode("new"); !ok || m != 0666 { - t.Errorf("SessionManagerFileMode(new) = %o, %v", m, ok) + if m, ok := session.FileMode("new"); !ok || m != 0666 { + t.Errorf("session.FileMode(new) = %o, %v", m, ok) } - if _, ok := SessionManagerFileMode("bogus"); ok { - t.Error("SessionManagerFileMode(bogus) should be false") + if _, ok := session.FileMode("bogus"); ok { + t.Error("session.FileMode(bogus) should be false") } } -// ===== SessionManager ===== +// ===== session.Manager ===== func TestSessionManagerReadableContract(t *testing.T) { } @@ -301,13 +302,13 @@ func TestSessionManagerDeleteAndKill(t *testing.T) { func TestSessionManagerSession(t *testing.T) { s := newTestSessionManager(t) if s.Session("nope") != nil { - t.Error("Session(nonexistent) should be nil") + t.Error("session.Session(nonexistent) should be nil") } sess := testSession("s1") defer sess.Cancel() s.AddSession(sess) if s.Session("s1") == nil { - t.Error("Session(s1) should not be nil") + t.Error("session.Session(s1) should not be nil") } } @@ -335,7 +336,7 @@ func TestSessionManagerShutdown(t *testing.T) { func TestSessionManagerRename(t *testing.T) { sink := testSink() var renamed [2]string - s := NewSessionManager(SessionManagerConfig{ + s := session.NewManager(session.ManagerConfig{ Log: sink.NewLogger("test"), Sink: sink, ReadFile: func(string) ([]byte, error) { return nil, nil }, @@ -388,13 +389,13 @@ func TestSessionManagerRenameErrors(t *testing.T) { } } -// ===== SessionFileStore ===== +// ===== session.FileStore ===== func TestSessionFileStoreReadableContract(t *testing.T) { sess := testSession("s1") defer sess.Cancel() sink := testSink() - _ = NewSessionFileStore(sess, sink.NewLogger("test"), + _ = session.NewFileStore(sess, sink.NewLogger("test"), func() {}, func(string) error { return nil }, func([]byte) error { return nil }) } @@ -402,15 +403,15 @@ func TestSessionFileStoreList(t *testing.T) { sess := testSession("s1") defer sess.Cancel() sink := testSink() - sf := NewSessionFileStore(sess, sink.NewLogger("test"), + sf := session.NewFileStore(sess, sink.NewLogger("test"), func() {}, func(string) error { return nil }, func([]byte) error { return nil }) entries, err := sf.List() if err != nil { t.Fatalf("List: %v", err) } - if len(entries) != len(SessionFileList) { - t.Errorf("List() returned %d entries; want %d", len(entries), len(SessionFileList)) + if len(entries) != len(session.FileList) { + t.Errorf("List() returned %d entries; want %d", len(entries), len(session.FileList)) } } @@ -419,7 +420,7 @@ func TestSessionFileStoreStatChat(t *testing.T) { defer sess.Cancel() sess.AppendLog([]byte("hello")) sink := testSink() - sf := NewSessionFileStore(sess, sink.NewLogger("test"), + sf := session.NewFileStore(sess, sink.NewLogger("test"), func() {}, func(string) error { return nil }, func([]byte) error { return nil }) fi, err := sf.Stat("chat") @@ -436,7 +437,7 @@ func TestSessionFileStoreGetChat(t *testing.T) { defer sess.Cancel() sess.AppendLog([]byte("hello")) sink := testSink() - sf := NewSessionFileStore(sess, sink.NewLogger("test"), + sf := session.NewFileStore(sess, sink.NewLogger("test"), func() {}, func(string) error { return nil }, func([]byte) error { return nil }) data := testStoreRead(t, sf, "chat") @@ -449,7 +450,7 @@ func TestSessionFileStoreGetContent(t *testing.T) { sess := testSession("s1") defer sess.Cancel() sink := testSink() - sf := NewSessionFileStore(sess, sink.NewLogger("test"), + sf := session.NewFileStore(sess, sink.NewLogger("test"), func() {}, func(string) error { return nil }, func([]byte) error { return nil }) for _, name := range []string{"cfg", "offset", "usage", "ctxsz", "models", "systemprompt"} { @@ -463,7 +464,7 @@ func TestSessionFileStorePutCwd(t *testing.T) { sess := testSession("s1") defer sess.Cancel() sink := testSink() - sf := NewSessionFileStore(sess, sink.NewLogger("test"), + sf := session.NewFileStore(sess, sink.NewLogger("test"), func() {}, func(string) error { return nil }, func([]byte) error { return nil }) testStoreWrite(t, sf, "cfg", []byte("cwd=/new/path")) @@ -477,7 +478,7 @@ func TestSessionFileStorePutEmpty(t *testing.T) { sess := testSession("s1") defer sess.Cancel() sink := testSink() - sf := NewSessionFileStore(sess, sink.NewLogger("test"), + sf := session.NewFileStore(sess, sink.NewLogger("test"), func() {}, func(string) error { return nil }, func([]byte) error { return nil }) // Empty write is a no-op @@ -490,7 +491,7 @@ func TestSessionFileStorePutEmpty(t *testing.T) { } } -// ===== SessionFileStore: content, writeFile, handleCtl, blockingRead, makePublish ===== +// ===== session.FileStore: content, writeFile, handleCtl, blockingRead, makePublish ===== func TestSessionFileStoreContentAllFields(t *testing.T) { sess := testSession("s1") @@ -712,7 +713,7 @@ func TestSessionFileStoreHandleCtl(t *testing.T) { core5 := &stubCore{state: "idle", backend_: "stub", model: "m", agentName: "default", cwd: "/tmp"} ctx5, cancel5 := context.WithCancel(context.Background()) defer cancel5() - sess5 := NewSession("s5", core5, ctx5, cancel5) + sess5 := session.NewSession("s5", core5, ctx5, cancel5) sf5, _ := newTestSessionFileStore(t, sess5) for _, cmd := range []string{"compact", "clear", "help", "history", "tools", "skills"} { testStoreWrite(t, sf5, "ctl", []byte(cmd)) @@ -740,7 +741,7 @@ func TestSessionFileStoreHandleCtlErrors(t *testing.T) { func TestSessionFileStoreBlockingRead(t *testing.T) { core := &stubCore{state: "idle", backend_: "stub", model: "m", agentName: "default", cwd: "/tmp", waitCh: make(chan string, 1)} ctx, cancel := context.WithCancel(context.Background()) - sess := NewSession("s1", core, ctx, cancel) + sess := session.NewSession("s1", core, ctx, cancel) defer cancel() sf, _ := newTestSessionFileStore(t, sess) @@ -795,7 +796,7 @@ func TestSessionFileStoreBlockingReadAllWaitFiles(t *testing.T) { // to avoid blocking forever in wait loops. core := &stubCore{state: "thinking", backend_: "stub", model: "m", agentName: "default", cwd: "/tmp", usage: "0", ctxsz: "0", waitCh: make(chan string, 1)} ctx, cancel := context.WithCancel(context.Background()) - sess := NewSession("s1", core, ctx, cancel) + sess := session.NewSession("s1", core, ctx, cancel) defer cancel() sf, _ := newTestSessionFileStore(t, sess) @@ -820,7 +821,7 @@ func TestSessionFileStoreBlockingReadIdleTimeout(t *testing.T) { core := &stubCore{state: "idle", backend_: "stub", model: "m", agentName: "default", cwd: "/tmp"} ctx, cancel := context.WithTimeout(context.Background(), 10*time.Millisecond) defer cancel() - sess := NewSession("s1", core, ctx, cancel) + sess := session.NewSession("s1", core, ctx, cancel) sf, _ := newTestSessionFileStore(t, sess) e, err := sf.Open("statewait") @@ -865,7 +866,7 @@ func TestSessionFileStoreMakePublishMultipleEvents(t *testing.T) { core2 := &publishCore{stubCore: core} ctx, cancel := context.WithCancel(context.Background()) defer cancel() - sess := NewSession("s1", core2, ctx, cancel) + sess := session.NewSession("s1", core2, ctx, cancel) sf := newTestSessionFileStoreWith(t, sess, func() {}, func(string) error { return nil }, func([]byte) error { return nil }) @@ -940,24 +941,24 @@ func TestSessionManagerOpenStore(t *testing.T) { } } -// ===== LoadAgentConfig ===== +// ===== session.LoadAgentConfig ===== func TestLoadAgentConfig(t *testing.T) { dir := t.TempDir() os.WriteFile(filepath.Join(dir, "test.json"), []byte(`{}`), 0644) - cfg := LoadAgentConfig(dir, "test", nil) + cfg := session.LoadAgentConfig(dir, "test", nil) if cfg == nil { - t.Error("LoadAgentConfig should return non-nil for existing file") + t.Error("session.LoadAgentConfig should return non-nil for existing file") } - cfg = LoadAgentConfig(dir, "nonexistent", nil) + cfg = session.LoadAgentConfig(dir, "nonexistent", nil) if cfg != nil { - t.Error("LoadAgentConfig should return nil for missing file") + t.Error("session.LoadAgentConfig should return nil for missing file") } } -// ===== FormatEvent ===== +// ===== session.FormatEvent ===== func TestFormatEvent(t *testing.T) { for _, tc := range []struct { @@ -974,9 +975,9 @@ func TestFormatEvent(t *testing.T) { {agent.Event{Role: "info", Content: "note"}, ":: note"}, {agent.Event{Role: "unknown"}, ""}, } { - got := string(FormatEvent(tc.ev)) + got := string(session.FormatEvent(tc.ev)) if got != tc.want { - t.Errorf("FormatEvent(%q) = %q; want %q", tc.ev.Role, got, tc.want) + t.Errorf("session.FormatEvent(%q) = %q; want %q", tc.ev.Role, got, tc.want) } } } @@ -1009,7 +1010,7 @@ func TestFileEntryDirEntry(t *testing.T) { } } -// ===== FormatParams / ParseParams ===== +// ===== session.FormatParams / session.ParseParams ===== func TestFormatParamsRoundTrip(t *testing.T) { temp := 0.7 @@ -1021,10 +1022,10 @@ func TestFormatParamsRoundTrip(t *testing.T) { FrequencyPenalty: &freq, PresencePenalty: &pres, } - out := FormatParams(p) - got, err := ParseParams(out, backend.GenerationParams{}) + out := session.FormatParams(p) + got, err := session.ParseParams(out, backend.GenerationParams{}) if err != nil { - t.Fatalf("ParseParams: %v", err) + t.Fatalf("session.ParseParams: %v", err) } if got.MaxTokens != 1024 { t.Errorf("MaxTokens = %d; want 1024", got.MaxTokens) @@ -1042,10 +1043,10 @@ func TestFormatParamsRoundTrip(t *testing.T) { func TestFormatParamsNilOptionals(t *testing.T) { p := backend.GenerationParams{MaxTokens: 512} - out := FormatParams(p) - got, err := ParseParams(out, backend.GenerationParams{}) + out := session.FormatParams(p) + got, err := session.ParseParams(out, backend.GenerationParams{}) if err != nil { - t.Fatalf("ParseParams: %v", err) + t.Fatalf("session.ParseParams: %v", err) } if got.MaxTokens != 512 { t.Errorf("MaxTokens = %d; want 512", got.MaxTokens) @@ -1058,9 +1059,9 @@ func TestFormatParamsNilOptionals(t *testing.T) { func TestParseParamsClearWithEmpty(t *testing.T) { temp := 1.0 p := backend.GenerationParams{MaxTokens: 100, Temperature: &temp} - got, err := ParseParams("maxTokens=\ntemperature=\nfrequencyPenalty=\npresencePenalty=\n", p) + got, err := session.ParseParams("maxTokens=\ntemperature=\nfrequencyPenalty=\npresencePenalty=\n", p) if err != nil { - t.Fatalf("ParseParams: %v", err) + t.Fatalf("session.ParseParams: %v", err) } if got.MaxTokens != 0 { t.Errorf("MaxTokens = %d; want 0", got.MaxTokens) @@ -1071,21 +1072,21 @@ func TestParseParamsClearWithEmpty(t *testing.T) { } func TestParseParamsErrors(t *testing.T) { - if _, err := ParseParams("maxTokens=bad", backend.GenerationParams{}); err == nil { + if _, err := session.ParseParams("maxTokens=bad", backend.GenerationParams{}); err == nil { t.Error("expected error for invalid maxTokens") } - if _, err := ParseParams("temperature=bad", backend.GenerationParams{}); err == nil { + if _, err := session.ParseParams("temperature=bad", backend.GenerationParams{}); err == nil { t.Error("expected error for invalid temperature") } - if _, err := ParseParams("frequencyPenalty=bad", backend.GenerationParams{}); err == nil { + if _, err := session.ParseParams("frequencyPenalty=bad", backend.GenerationParams{}); err == nil { t.Error("expected error for invalid frequencyPenalty") } - if _, err := ParseParams("presencePenalty=bad", backend.GenerationParams{}); err == nil { + if _, err := session.ParseParams("presencePenalty=bad", backend.GenerationParams{}); err == nil { t.Error("expected error for invalid presencePenalty") } } -// ===== Session.Interrupt ===== +// ===== session.Session.Interrupt ===== func TestSessionInterrupt(t *testing.T) { sess := testSession("i1") @@ -1094,7 +1095,7 @@ func TestSessionInterrupt(t *testing.T) { // stubCore.Interrupt is a no-op; just ensure it doesn't panic } -// ===== SessionManager.createSession ===== +// ===== session.Manager.createSession ===== func TestSessionManagerCreateSessionViaWrite(t *testing.T) { s := newTestSessionManagerWithCore(t) @@ -1153,7 +1154,7 @@ func TestSessionManagerCreateSessionEnvExpansion(t *testing.T) { t.Setenv("HOME", home) var gotCwd string sink := testSink() - s := NewSessionManager(SessionManagerConfig{ + s := session.NewManager(session.ManagerConfig{ Log: sink.NewLogger("test"), Sink: sink, ReadFile: func(string) ([]byte, error) { return []byte("#!/bin/sh\n"), nil }, @@ -1180,7 +1181,7 @@ func TestSessionManagerCreateSessionTildeExpansion(t *testing.T) { } var gotCwd string sink := testSink() - s := NewSessionManager(SessionManagerConfig{ + s := session.NewManager(session.ManagerConfig{ Log: sink.NewLogger("test"), Sink: sink, ReadFile: func(string) ([]byte, error) { return []byte("#!/bin/sh\n"), nil }, diff --git a/server.go b/server.go index cc184f0..b071df6 100644 --- a/server.go +++ b/server.go @@ -19,6 +19,7 @@ import ( olog "ollie/pkg/log" "ollie/pkg/paths" "ollie/pkg/tools/execute" + "olliesrv/session" "olliesrv/store" "9fans.net/go/plan9" @@ -70,7 +71,7 @@ type Server struct { utilStore Store pluginStore Store skillStore Store - sessionMgr *SessionManager + sessionMgr *session.Manager transcriptStore Store tmpStore Store rootStore *store.DirStore @@ -115,7 +116,7 @@ func New(sink *olog.Sink, opts ...ServerOption) *Server { for _, o := range opts { o(s) } - s.sessionMgr = NewSessionManager(SessionManagerConfig{ + s.sessionMgr = session.NewManager(session.ManagerConfig{ AgentsDir: agentsDir, SessionsDir: sessionsDir, Log: s.log, @@ -242,7 +243,7 @@ func (s *Server) InGroup(group, user string) bool { func (s *Server) fileOwnerGroup(path string) (uid, gid string) { if strings.HasPrefix(path, "/s/") { parts := strings.SplitN(strings.TrimPrefix(path, "/s/"), "/", 2) - if len(parts) >= 1 && parts[0] != "new" && !isSessionManagerFile(path) { + if len(parts) >= 1 && parts[0] != "new" && !isSessionFile(path) { if sess := s.sessionMgr.Session(parts[0]); sess != nil { return sess.Uname(), "agent" } @@ -457,14 +458,14 @@ func (s *Server) handle(cs *connState, fc *plan9.Fcall, ctx context.Context) *pl } } -// isSessionManagerFile reports whether path is a fixed file directly under /s/ +// isSessionFile reports whether path is a fixed file directly under /s/ // (i.e. /s/ where name is in sessionStoreFiles). -func isSessionManagerFile(path string) bool { +func isSessionFile(path string) bool { name, ok := strings.CutPrefix(path, "/s/") if !ok || strings.Contains(name, "/") { return false } - _, ok = SessionManagerFileMode(name) + _, ok = session.FileMode(name) return ok } @@ -1076,8 +1077,8 @@ func (s *Server) makeStat(path string) plan9.Dir { default: if path == "/backends" || path == "/help" { mode = 0444 - } else if isSessionManagerFile(path) { - mode_, _ := SessionManagerFileMode(base) + } else if isSessionFile(path) { + mode_, _ := session.FileMode(base) mode = plan9.Perm(mode_) } else if strings.HasPrefix(path, "/a/") { mode = 0666 @@ -1147,7 +1148,7 @@ func (s *Server) makeStat(path string) plan9.Dir { // that check stat before reading (cat, 9pfuse, etc.) see non-zero size. if dir.Length == 0 && !isDir { switch { - case isSessionManagerFile(path): + case isSessionFile(path): if content, err := storeRead(s.sessionMgr.Store(), base); err == nil { dir.Length = uint64(len(content)) } diff --git a/session.go b/session/session.go similarity index 91% rename from session.go rename to session/session.go index fedeff0..de7adf9 100644 --- a/session.go +++ b/session/session.go @@ -1,4 +1,4 @@ -package main +package session import ( "context" @@ -93,15 +93,15 @@ var sessionStoreFiles = map[string]os.FileMode{ var sessionStoreOrder = []string{"new", "idx", "ls", "kill", "sh", "b", "bfg", "bbg", "cleanup"} -// SessionManagerFileMode returns the mode for a fixed session store file, +// FileMode returns the mode for a fixed session store file, // or 0 and false if the name is not a fixed file. -func SessionManagerFileMode(name string) (os.FileMode, bool) { +func FileMode(name string) (os.FileMode, bool) { m, ok := sessionStoreFiles[name] return m, ok } -// SessionManagerConfig holds the dependencies for a SessionManager. -type SessionManagerConfig struct { +// ManagerConfig holds the dependencies for a Manager. +type ManagerConfig struct { AgentsDir string SessionsDir string Log *olog.Logger @@ -123,31 +123,31 @@ type SessionManagerConfig struct { Yolo bool } -// SessionManager manages session lifecycle and exposes sessions as a DirStore. -type SessionManager struct { +// Manager manages session lifecycle and exposes sessions as a DirStore. +type Manager struct { store *store.DirStore - cfg SessionManagerConfig + cfg ManagerConfig mu sync.RWMutex sessions map[string]*Session nextUID atomic.Uint32 // incrementing principal counter } // Store returns the DirStore view of the session namespace. -func (s *SessionManager) Store() *store.DirStore { return s.store } +func (s *Manager) Store() *store.DirStore { return s.store } // nextUname generates the next uname atomically. -func (s *SessionManager) nextUname() string { +func (s *Manager) nextUname() string { return fmt.Sprintf("%d", s.nextUID.Add(1)) } -func NewSessionManager(cfg SessionManagerConfig) *SessionManager { +func NewManager(cfg ManagerConfig) *Manager { if cfg.ReadFile == nil { cfg.ReadFile = os.ReadFile } if cfg.MkdirAll == nil { cfg.MkdirAll = os.MkdirAll } - ss := &SessionManager{ + ss := &Manager{ cfg: cfg, sessions: make(map[string]*Session), } @@ -165,13 +165,13 @@ func NewSessionManager(cfg SessionManagerConfig) *SessionManager { } // AddSession inserts a pre-built session into the store. -func (s *SessionManager) AddSession(sess *Session) { +func (s *Manager) AddSession(sess *Session) { s.mu.Lock() s.sessions[sess.RunnableID()] = sess s.mu.Unlock() } -func (s *SessionManager) list() ([]os.DirEntry, error) { +func (s *Manager) list() ([]os.DirEntry, error) { entries := make([]os.DirEntry, 0, len(sessionStoreOrder)) for _, name := range sessionStoreOrder { entries = append(entries, store.FileEntry(name, sessionStoreFiles[name])) @@ -185,7 +185,7 @@ func (s *SessionManager) list() ([]os.DirEntry, error) { } // ListDir lists entries in a subdirectory (e.g. "{id}", "{id}/t", "{id}/t/{subdir}"). -func (s *SessionManager) ListDir(name string) ([]os.DirEntry, error) { +func (s *Manager) ListDir(name string) ([]os.DirEntry, error) { parts := strings.SplitN(name, "/", 3) sessID := parts[0] sess := s.Session(sessID) @@ -221,7 +221,7 @@ func (s *SessionManager) ListDir(name string) ([]os.DirEntry, error) { return nil, fmt.Errorf("%s: not a directory", name) } -func (s *SessionManager) stat(name string) (os.FileInfo, error) { +func (s *Manager) stat(name string) (os.FileInfo, error) { // Top-level fixed files (new, idx, sh, etc.) if mode, ok := sessionStoreFiles[name]; ok { return &store.SyntheticFileInfo{Name_: name, Mode_: mode}, nil @@ -257,7 +257,7 @@ func (s *SessionManager) stat(name string) (os.FileInfo, error) { return sfs.Stat(parts[1]) } -func (s *SessionManager) openEntry(name string) (store.StoreEntry, error) { +func (s *Manager) openEntry(name string) (store.StoreEntry, error) { notBlocking := func(context.Context, string) ([]byte, string, error) { return nil, "", fmt.Errorf("blocking read not supported") } @@ -332,7 +332,7 @@ func (s *SessionManager) openEntry(name string) (store.StoreEntry, error) { } -func (s *SessionManager) openFilteredToolIdx(allowed map[string]bool) (store.StoreEntry, error) { +func (s *Manager) openFilteredToolIdx(allowed map[string]bool) (store.StoreEntry, error) { base, err := s.cfg.ToolStore.Open("idx") if err != nil { return nil, err @@ -366,7 +366,7 @@ func filterToolIdx(data []byte, allowed map[string]bool) []byte { return out } -func (s *SessionManager) create(name string) error { +func (s *Manager) create(name string) error { // Tool create: {id}/t/{rel} parts := strings.SplitN(name, "/", 3) if len(parts) == 3 && parts[1] == "t" && s.cfg.ToolStore != nil { @@ -376,7 +376,7 @@ func (s *SessionManager) create(name string) error { } // MkdirAll creates a directory within the session's tool store. -func (s *SessionManager) MkdirAll(name string) error { +func (s *Manager) MkdirAll(name string) error { parts := strings.SplitN(name, "/", 3) if len(parts) == 3 && parts[1] == "t" { type mkdirAller interface { @@ -389,7 +389,7 @@ func (s *SessionManager) MkdirAll(name string) error { return fmt.Errorf("mkdir not supported: %s", name) } -func (s *SessionManager) del(name string) error { +func (s *Manager) del(name string) error { parts := strings.SplitN(name, "/", 3) sessID := parts[0] // Delete session directory itself. @@ -415,14 +415,14 @@ func (s *SessionManager) del(name string) error { } // Session returns the session for the given ID, or nil. -func (s *SessionManager) Session(id string) *Session { +func (s *Manager) Session(id string) *Session { s.mu.RLock() defer s.mu.RUnlock() return s.sessions[id] } // SessionByUname returns the session with the given uname (principal), or nil. -func (s *SessionManager) SessionByUname(uname string) *Session { +func (s *Manager) SessionByUname(uname string) *Session { s.mu.RLock() defer s.mu.RUnlock() for _, sess := range s.sessions { @@ -434,16 +434,16 @@ func (s *SessionManager) SessionByUname(uname string) *Session { } // OpenStore returns a DirStore for the given session ID. -func (s *SessionManager) OpenStore(id string) (*store.DirStore, error) { +func (s *Manager) OpenStore(id string) (*store.DirStore, error) { if sess := s.Session(id); sess != nil { return s.openStore(sess) } return nil, fmt.Errorf("session not found: %s", id) } -func (s *SessionManager) openStore(sess *Session) (*store.DirStore, error) { +func (s *Manager) openStore(sess *Session) (*store.DirStore, error) { id := sess.RunnableID() - return NewSessionFileStore( + return NewFileStore( sess, s.cfg.Log, func() { s.KillSession(id) }, @@ -453,7 +453,7 @@ func (s *SessionManager) openStore(sess *Session) (*store.DirStore, error) { } // InterruptAll interrupts every active session. -func (s *SessionManager) InterruptAll() { +func (s *Manager) InterruptAll() { s.mu.RLock() defer s.mu.RUnlock() for _, sess := range s.sessions { @@ -462,7 +462,7 @@ func (s *SessionManager) InterruptAll() { } // Shutdown kills all active sessions. -func (s *SessionManager) Shutdown() { +func (s *Manager) Shutdown() { s.mu.Lock() ids := make([]string, 0, len(s.sessions)) for id := range s.sessions { @@ -474,7 +474,7 @@ func (s *SessionManager) Shutdown() { } } -func (s *SessionManager) KillSession(id string) { +func (s *Manager) KillSession(id string) { s.mu.Lock() sess := s.sessions[id] delete(s.sessions, id) @@ -486,7 +486,7 @@ func (s *SessionManager) KillSession(id string) { } } -func (s *SessionManager) index() []byte { +func (s *Manager) index() []byte { var sb strings.Builder s.mu.RLock() ids := make([]string, 0, len(s.sessions)) @@ -508,7 +508,7 @@ func (s *SessionManager) index() []byte { return []byte(sb.String()) } -func (s *SessionManager) createSession(args []string) error { +func (s *Manager) createSession(args []string) error { name := "" backendOverride := "" modelOverride := "" @@ -643,7 +643,7 @@ func (s *SessionManager) createSession(args []string) error { return nil } -func (s *SessionManager) renameSession(old, new string) error { +func (s *Manager) renameSession(old, new string) error { // Tool rename: {id}/t/{oldRel} -> newName if strings.Contains(old, "/t/") { parts := strings.SplitN(old, "/", 3) @@ -691,7 +691,7 @@ func (s *SessionManager) renameSession(old, new string) error { // OnRename must be called without holding s.mu to avoid deadlock // with connection handlers that hold connState.mu and call into - // SessionManager (e.g. SessionByUname during Tattach). + // Manager (e.g. SessionByUname during Tattach). if s.cfg.OnRename != nil { s.cfg.OnRename(oldID, newID) } diff --git a/sessionfile.go b/session/sessionfile.go similarity index 97% rename from sessionfile.go rename to session/sessionfile.go index dd69ade..ba2c12e 100644 --- a/sessionfile.go +++ b/session/sessionfile.go @@ -1,4 +1,4 @@ -package main +package session import ( "context" @@ -14,8 +14,8 @@ import ( "olliesrv/store" ) -// SessionFileList defines the fixed set of files in a session directory. -var SessionFileList = []struct { +// FileList defines the fixed set of files in a session directory. +var FileList = []struct { Name string Mode os.FileMode OneShot bool @@ -42,13 +42,13 @@ var SessionFileList = []struct { {"context", 0444, false, false}, } -// SessionFileStore is a DirStore for a session directory. -type SessionFileStore = store.DirStore +// FileStore is a DirStore for a session directory. +type FileStore = store.DirStore -func NewSessionFileStore(sess *Session, log *olog.Logger, kill func(), rename func(newID string) error, saveTranscript func([]byte) error) *SessionFileStore { +func NewFileStore(sess *Session, log *olog.Logger, kill func(), rename func(newID string) error, saveTranscript func([]byte) error) *FileStore { h := &sessionHelper{sess: sess, log: log, kill: kill, rename: rename, saveTranscript: saveTranscript} - specs := make([]store.FileSpec, len(SessionFileList)) - for i, f := range SessionFileList { + specs := make([]store.FileSpec, len(FileList)) + for i, f := range FileList { specs[i] = h.fileSpec(f.Name, f.Mode) specs[i].OneShot = f.OneShot specs[i].Async = f.Async