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).
This commit is contained in:
Ollie Agent 2026-08-11 11:45:47 +02:00
parent cfc526b0d7
commit 7dfd769c25
1 changed files with 39 additions and 19 deletions

View File

@ -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).