From 7dfd769c250130cc8b3f9a87ce871c04ec3b5cb3 Mon Sep 17 00:00:00 2001 From: Ollie Agent Date: Tue, 11 Aug 2026 11:45:47 +0200 Subject: [PATCH] server: fix BlockingReadMode truncation, separate stream/blocking paths BlockingReadMode had the same 512-byte truncation bug: it returned EOF on offset>0 before the full content was delivered. Now caches content on the fid like normal reads. StreamMode is separated into its own path since it has fundamentally different semantics (each Tread blocks for the next chunk, no caching). --- cmd/olliesrv/server.go | 58 ++++++++++++++++++++++++++++-------------- 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/cmd/olliesrv/server.go b/cmd/olliesrv/server.go index e11fd9c..14a99d8 100644 --- a/cmd/olliesrv/server.go +++ b/cmd/olliesrv/server.go @@ -468,11 +468,15 @@ func read(root *fs.Tree, cs *connState, fc *plan9.Fcall, ctx context.Context) *p return errFcall(fc, "file not opened") } - if entry.BlockingReadMode() && fc.Offset > 0 { - return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: 0} - } - - if entry.BlockingReadMode() || entry.StreamMode() { + if entry.BlockingReadMode() { + // Serve from cache if we already have content (multi-Tread paging). + if f.readCache != nil { + if int(fc.Offset) >= len(f.readCache) { + return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: 0} + } + return readSlice(fc, f.readCache) + } + // First Tread: block for content. cs.mu.RLock() f, fidOK := cs.fids[fc.Fid] var base string @@ -480,13 +484,7 @@ func read(root *fs.Tree, cs *connState, fc *plan9.Fcall, ctx context.Context) *p base = f.waitBase } cs.mu.RUnlock() - var waitCtx context.Context - var waitCancel context.CancelFunc - if entry.StreamMode() { - waitCtx, waitCancel = context.WithCancel(ctx) - } else { - waitCtx, waitCancel = context.WithTimeout(ctx, 5*time.Second) - } + waitCtx, waitCancel := context.WithTimeout(ctx, 5*time.Second) defer waitCancel() content, nextBase, err := entry.BlockingRead(waitCtx, base) if err != nil { @@ -499,16 +497,38 @@ func read(root *fs.Tree, cs *connState, fc *plan9.Fcall, ctx context.Context) *p } cs.mu.Unlock() } - if entry.StreamMode() { - count := int(fc.Count) - if count > len(content) { - count = len(content) - } - return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: uint32(count), Data: content[:count]} - } + f.readCache = content return readSlice(fc, content) } + if entry.StreamMode() { + cs.mu.RLock() + f, fidOK := cs.fids[fc.Fid] + var base string + if fidOK { + base = f.waitBase + } + cs.mu.RUnlock() + waitCtx, waitCancel := context.WithCancel(ctx) + defer waitCancel() + content, nextBase, err := entry.BlockingRead(waitCtx, base) + if err != nil { + return errFcall(fc, err.Error()) + } + if nextBase != "" { + cs.mu.Lock() + if f, ok := cs.fids[fc.Fid]; ok { + f.waitBase = nextBase + } + cs.mu.Unlock() + } + count := int(fc.Count) + if count > len(content) { + count = len(content) + } + return &plan9.Fcall{Type: plan9.Rread, Tag: fc.Tag, Count: uint32(count), Data: content[:count]} + } + // Normal file read: cache content on first Tread, serve via readSlice. // This handles the case where io.ReadAll issues multiple Treads at // increasing offsets (buffer starts at 512 bytes and grows).