From 8ae8a6ead4096e1674c02865e6a0635150726741 Mon Sep 17 00:00:00 2001 From: Ollie Agent Date: Sat, 1 Aug 2026 16:14:49 +0200 Subject: [PATCH] session rename: eventwait file, broadcast on create/delete/rename, ollie-9p mv --- cmd/ollie-9p/main.go | 7 +++++ fs/session/create.go | 1 + fs/session/perm.go | 5 ++-- fs/session/persist.go | 2 ++ fs/session/root.go | 62 ++++++++++++++++++++++++++++++++++++++++++- kde | 2 +- 6 files changed, 75 insertions(+), 4 deletions(-) diff --git a/cmd/ollie-9p/main.go b/cmd/ollie-9p/main.go index 15abd89..28b5b5f 100644 --- a/cmd/ollie-9p/main.go +++ b/cmd/ollie-9p/main.go @@ -116,6 +116,8 @@ func dial() *client.Fsys { } address = ns + "/ollie" } + // Strip plan9port-style unix! prefix if present + address = strings.TrimPrefix(address, "unix!") aname := os.Getenv("OLLIE_UNAME") if aname == "" { @@ -128,6 +130,11 @@ func dial() *client.Fsys { aname = "none" } } + // If OLLIE_SESSION_ID is set, we're inside a session and must use OLLIE_UNAME. + // If it's not set, we're outside (e.g. GUI) and use $USER. + // The GUI calls renameSession which spawns ollie-9p without OLLIE_SESSION_ID, + // so it falls through to $USER. That's correct — the server accepts $USER + // as a valid attach identity for non-session operations. fsys, err := mountWithAname(address, aname) if err != nil { diff --git a/fs/session/create.go b/fs/session/create.go index 5be927d..6ddf9a0 100644 --- a/fs/session/create.go +++ b/fs/session/create.go @@ -260,6 +260,7 @@ func Create(rs *rootState, args []string) (string, error) { rs.cfg.Log.Info("new session %s (backend=%s model=%s agent=%s)", sessID, core.Agent().BackendName(), core.Agent().ModelName(), core.Agent().Name()) + broadcastEvents() return sessID, nil } diff --git a/fs/session/perm.go b/fs/session/perm.go index 038965f..c64c8a1 100644 --- a/fs/session/perm.go +++ b/fs/session/perm.go @@ -57,8 +57,9 @@ var Perms = map[string]Perm{ DirMode: 0755, FileMode: 0444, Files: map[string]os.FileMode{ - "new": 0666, - "idx": 0444, + "new": 0666, + "idx": 0444, + "eventwait": 0444, }, }, PathSessionDir: {DirMode: 0755}, diff --git a/fs/session/persist.go b/fs/session/persist.go index f048e16..be8a33c 100644 --- a/fs/session/persist.go +++ b/fs/session/persist.go @@ -31,6 +31,7 @@ func Kill(rs *rootState, id string) { removePersistedAgent(rs, id) rs.cfg.Log.Info("killed session %s", id) } + broadcastEvents() } // KillFromRoot is the public API for callers with *Tree. @@ -66,6 +67,7 @@ func Rename(rs *rootState, old, new string) error { rs.mu.Unlock() rs.cfg.Log.Info("renamed session %s -> %s", old, new) + broadcastEvents() return nil } diff --git a/fs/session/root.go b/fs/session/root.go index 56b8843..efaac96 100644 --- a/fs/session/root.go +++ b/fs/session/root.go @@ -17,7 +17,45 @@ import ( "ollie/toolsrv" ) -var rootFileOrder = []string{"new", "idx"} +// eventBroadcaster manages notification of session index changes. +// The GUI can tail session/eventwait to detect creates, deletes, and renames. +var ( + evMu sync.Mutex + evChans []chan struct{} +) + +func broadcastEvents() { + evMu.Lock() + for _, ch := range evChans { + select { + case ch <- struct{}{}: + default: + } + } + evMu.Unlock() +} + +// eventRegister returns a channel that fires on the next session index change. +func eventRegister() chan struct{} { + ch := make(chan struct{}, 1) + evMu.Lock() + evChans = append(evChans, ch) + evMu.Unlock() + return ch +} + +func eventRelease(ch chan struct{}) { + evMu.Lock() + for i, c := range evChans { + if c == ch { + evChans = append(evChans[:i], evChans[i+1:]...) + break + } + } + evMu.Unlock() +} + +var rootFileOrder = []string{"new", "idx", "eventwait"} // FileMode returns the mode for a fixed session file, // or 0 and false if the name is not a fixed file. @@ -295,6 +333,7 @@ func rootOpen(rs *rootState, name string) (File, error) { notBlocking := func(context.Context, string) ([]byte, string, error) { return nil, "", fmt.Errorf("blocking read not supported") } + parts := strings.SplitN(name, "/", 3) // Proc file: {id}/proc/{pid} @@ -328,6 +367,27 @@ func rootOpen(rs *rootState, name string) (File, error) { } switch name { +case "eventwait": + return &FileConfig{ + StatFn: func() (os.FileInfo, error) { + return &SyntheticFileInfo{Name_: "eventwait", Mode_: Perms[PathSessions].Files["eventwait"]}, nil + }, + ReadFn: func() ([]byte, error) { + return []byte(buildIndex(rs)), nil + }, + WriteFn: func([]byte) error { return fmt.Errorf("eventwait: read-only") }, + BlockingReadFn: func(ctx context.Context, _ string) ([]byte, string, error) { + ch := eventRegister() + defer eventRelease(ch) + select { + case <-ch: + return []byte(buildIndex(rs)), "", nil + case <-ctx.Done(): + return nil, "", ctx.Err() + } + }, + IsBlocking_: true, + }, nil case "new": return &FileConfig{ StatFn: func() (os.FileInfo, error) { diff --git a/kde b/kde index cd0f92e..76f97c4 160000 --- a/kde +++ b/kde @@ -1 +1 @@ -Subproject commit cd0f92e16de666f23885614c23d7fa33dab8ad9f +Subproject commit 76f97c42ebbba47e96d99554da88d4625db2d378