Close memory holes, adjust tests

This commit is contained in:
2026-10-19 00:42:19 +00:00
parent 466290d316
commit a81044322b
4 changed files with 380 additions and 19 deletions
+80 -2
View File
@@ -546,6 +546,10 @@ func ensureSlotConnected(s *UserMasterSession, slotIdx int, background bool, cli
IsBound: isBound, IsBound: isBound,
MaxBufferSize: maxBuf, MaxBufferSize: maxBuf,
} }
if GlobalConfig != nil {
slot.AutoHoldDiscardLimit = GlobalConfig.Server.AutoHoldDiscardLimit
slot.AutoHoldBurstLimit = GlobalConfig.Server.AutoHoldBurstLimit
}
s.Slots[slotIdx] = slot s.Slots[slotIdx] = slot
go upstreamReader(s, slot, slotIdx) go upstreamReader(s, slot, slotIdx)
return slot return slot
@@ -575,8 +579,7 @@ func connectOrResumeSlot(s *UserMasterSession, slotIdx int, clientConn net.Conn)
}() }()
slot.Mu.Lock() slot.Mu.Lock()
slot.ScreenChanged = false slot.resetHoldAndLoopCountersLocked()
slot.IsHeld = false
if len(slot.LastScreen) > 0 { if len(slot.LastScreen) > 0 {
_, _ = clientConn.Write(slot.LastScreen) _, _ = clientConn.Write(slot.LastScreen)
} }
@@ -781,6 +784,21 @@ func upstreamReader(s *UserMasterSession, slot *SessionSlot, slotIdx int) {
// Cap buffer at 64KB to avoid unbounded memory use from constant ticks // Cap buffer at 64KB to avoid unbounded memory use from constant ticks
if len(recBuf) > 65536 { if len(recBuf) > 65536 {
recBuf = recBuf[len(recBuf)-16384:] recBuf = recBuf[len(recBuf)-16384:]
if !isActive {
slot.Mu.Lock()
slot.DiscardCount++
discardLimit := slot.AutoHoldDiscardLimit
if discardLimit == 0 {
discardLimit = defaultAutoHoldDiscardLimit
}
if discardLimit > 0 && slot.DiscardCount >= discardLimit {
slot.IsHeld = true
slot.ScreenChanged = false
slot.DiscardCount = 0
log.Printf("[%s] '%s' System [%d] %s placed on automatic HOLD (raw receive buffer discard loop detected)", displayTag, username, slotIdx+1, slot.System.Name)
}
slot.Mu.Unlock()
}
} }
recBuf = append(recBuf, data...) recBuf = append(recBuf, data...)
for { for {
@@ -870,8 +888,41 @@ func upstreamReader(s *UserMasterSession, slot *SessionSlot, slotIdx int) {
continue continue
} }
now := time.Now()
// Auto-hold loop detection for background sessions:
// 1. High frequency write updates (excessive update rate loop)
if !isActive {
burstLimit := slot.AutoHoldBurstLimit
if burstLimit == 0 {
burstLimit = defaultAutoHoldBurstLimit
}
if burstLimit > 0 {
if slot.WriteBurstWindow.IsZero() || now.Sub(slot.WriteBurstWindow) > autoHoldBurstWindow {
slot.WriteBurstCount = 1
slot.WriteBurstWindow = now
} else {
slot.WriteBurstCount++
}
if slot.WriteBurstCount >= burstLimit {
slot.IsHeld = true
slot.ScreenChanged = false
slot.WriteBurstCount = 0
slot.DiscardCount = 0
log.Printf("[%s] '%s' System [%d] %s placed on automatic HOLD (excessive update rate detected)", displayTag, username, slotIdx+1, slot.System.Name)
if isEraseWrite || len(slot.LastScreen) == 0 {
slot.LastScreen = bytes.Clone(screenData)
}
slot.Mu.Unlock()
continue
}
}
}
if isEraseWrite || len(slot.LastScreen) == 0 { if isEraseWrite || len(slot.LastScreen) == 0 {
slot.LastScreen = bytes.Clone(screenData) slot.LastScreen = bytes.Clone(screenData)
slot.DiscardCount = 0
} else if len(slot.LastScreen)+len(screenData) <= maxBuf { } else if len(slot.LastScreen)+len(screenData) <= maxBuf {
slot.LastScreen = append(slot.LastScreen, screenData...) slot.LastScreen = append(slot.LastScreen, screenData...)
} else { } else {
@@ -886,6 +937,33 @@ func upstreamReader(s *UserMasterSession, slot *SessionSlot, slotIdx int) {
newBuf = append(newBuf, slot.LastScreen[:baseLen]...) newBuf = append(newBuf, slot.LastScreen[:baseLen]...)
newBuf = append(newBuf, screenData...) newBuf = append(newBuf, screenData...)
slot.LastScreen = newBuf slot.LastScreen = newBuf
// Auto-hold loop detection for background sessions:
// 2. Buffer overflow discard loop (repeated discards of older intermediate writes)
if !isActive {
discardLimit := slot.AutoHoldDiscardLimit
if discardLimit == 0 {
discardLimit = defaultAutoHoldDiscardLimit
}
if discardLimit > 0 {
if slot.DiscardWindow.IsZero() || now.Sub(slot.DiscardWindow) > autoHoldDiscardWindow {
slot.DiscardCount = 1
slot.DiscardWindow = now
} else {
slot.DiscardCount++
}
if slot.DiscardCount >= discardLimit {
slot.IsHeld = true
slot.ScreenChanged = false
slot.DiscardCount = 0
slot.WriteBurstCount = 0
log.Printf("[%s] '%s' System [%d] %s placed on automatic HOLD (discard loop detected)", displayTag, username, slotIdx+1, slot.System.Name)
slot.Mu.Unlock()
continue
}
}
}
} }
if !isActive { if !isActive {
slot.ScreenChanged = true slot.ScreenChanged = true
+256
View File
@@ -3635,3 +3635,259 @@ func TestCleanupSessionIfEmpty(t *testing.T) {
} }
} }
func TestAutoHoldOnDiscardLoopBufferOverflow(t *testing.T) {
origSystems := ConfiguredSystem
defer func() {
ConfiguredSystem = origSystems
}()
ConfiguredSystem = []TargetSystem{
{Name: "DISCARD_SYS", Description: "Discard loop system"},
}
clientServer, clientUser := net.Pipe()
defer clientServer.Close()
defer clientUser.Close()
upServer, upClient := net.Pipe()
defer upServer.Close()
defer upClient.Close()
initialScreen := []byte{CMD_ERASE_WRITE, 0xC3, 0x11, 0x40, 0x40, 'B', 'A', 'S', 'E', IAC, EOR}
slot := &SessionSlot{
System: ConfiguredSystem[0],
Upstream: upClient,
LastScreen: initialScreen,
MaxBufferSize: 200, // Small buffer so it easily overflows
AutoHoldBurstLimit: -1, // Disable burst rate check to test discard check specifically
AutoHoldDiscardLimit: 3, // 3 discards triggers auto-hold
}
s := &UserMasterSession{
Username: "DISCARDUSER",
DisplayTag: "disc1234",
TerminalType: "IBM-3278-2",
ClientConn: clientServer,
ActiveSlotIdx: -1, // Background
Slots: []*SessionSlot{slot},
IsDisconnected: false,
}
go upstreamReader(s, slot, 0)
// Send 10 writes of 60 bytes each (each > 200/2, quickly overflowing 200-byte MaxBufferSize)
writePayload := []byte{CMD_WRITE, 0xC3, 0x11, 0x40, 0x60}
for i := 0; i < 50; i++ {
writePayload = append(writePayload, 'X')
}
writePayload = append(writePayload, IAC, EOR)
for i := 0; i < 10; i++ {
_, err := upServer.Write(writePayload)
if err != nil {
t.Fatalf("Failed to write payload %d: %v", i, err)
}
time.Sleep(10 * time.Millisecond)
}
time.Sleep(150 * time.Millisecond)
slot.Mu.Lock()
isHeld := slot.IsHeld
screenChanged := slot.ScreenChanged
slot.Mu.Unlock()
if !isHeld {
t.Fatal("Expected slot.IsHeld == true after buffer overflow discard loop")
}
if screenChanged {
t.Fatal("Expected slot.ScreenChanged == false once placed on automatic HOLD")
}
// Verify status in main menu is rendered as "HELD" with color Red
screen := buildMainMenuScreen(s)
var rowHeld *go3270.Field
for i := range screen {
f := &screen[i]
if f.Row == 7 && strings.Contains(f.Content, "DISCARD_SYS") {
rowHeld = f
break
}
}
if rowHeld == nil {
t.Fatal("Expected row for DISCARD_SYS on row 7")
}
if !strings.Contains(rowHeld.Content, "HELD") {
t.Fatalf("Expected row content to contain 'HELD', got: %s", rowHeld.Content)
}
if rowHeld.Color != go3270.Red {
t.Fatalf("Expected HELD row color to be Red, got: %v", rowHeld.Color)
}
// Releasing the hold resets counters
slot.Mu.Lock()
slot.resetHoldAndLoopCountersLocked()
isHeldAfterReset := slot.IsHeld
discardCountAfterReset := slot.DiscardCount
slot.Mu.Unlock()
if isHeldAfterReset {
t.Fatal("Expected IsHeld == false after resetHoldAndLoopCountersLocked")
}
if discardCountAfterReset != 0 {
t.Fatalf("Expected DiscardCount == 0, got %d", discardCountAfterReset)
}
}
func TestAutoHoldOnExcessiveUpdateRate(t *testing.T) {
origSystems := ConfiguredSystem
defer func() {
ConfiguredSystem = origSystems
}()
ConfiguredSystem = []TargetSystem{
{Name: "BURST_SYS", Description: "Burst loop system"},
}
clientServer, clientUser := net.Pipe()
defer clientServer.Close()
defer clientUser.Close()
upServer, upClient := net.Pipe()
defer upServer.Close()
defer upClient.Close()
initialScreen := []byte{CMD_ERASE_WRITE, 0xC3, 0x11, 0x40, 0x40, 'B', 'A', 'S', 'E', IAC, EOR}
slot := &SessionSlot{
System: ConfiguredSystem[0],
Upstream: upClient,
LastScreen: initialScreen,
MaxBufferSize: 32768, // Huge buffer, will NOT overflow
AutoHoldBurstLimit: 5, // 5 rapid updates triggers auto-hold
AutoHoldDiscardLimit: -1, // Disable discard check
}
s := &UserMasterSession{
Username: "BURSTUSER",
DisplayTag: "brst1234",
TerminalType: "IBM-3278-2",
ClientConn: clientServer,
ActiveSlotIdx: -1, // Background
Slots: []*SessionSlot{slot},
IsDisconnected: false,
}
go upstreamReader(s, slot, 0)
// Send 6 small updates in rapid succession
smallUpdate := []byte{CMD_WRITE, 0xC3, 0x11, 0x40, 0x50, '1', IAC, EOR}
for i := 0; i < 6; i++ {
_, err := upServer.Write(smallUpdate)
if err != nil {
t.Fatalf("Failed to write small update %d: %v", i, err)
}
}
time.Sleep(100 * time.Millisecond)
slot.Mu.Lock()
isHeld := slot.IsHeld
screenChanged := slot.ScreenChanged
slot.Mu.Unlock()
if !isHeld {
t.Fatal("Expected slot.IsHeld == true after excessive update rate loop")
}
if screenChanged {
t.Fatal("Expected slot.ScreenChanged == false once placed on automatic HOLD")
}
// Menu renders HELD
screen := buildMainMenuScreen(s)
var rowHeld *go3270.Field
for i := range screen {
f := &screen[i]
if f.Row == 7 && strings.Contains(f.Content, "BURST_SYS") {
rowHeld = f
break
}
}
if rowHeld == nil {
t.Fatal("Expected row for BURST_SYS on row 7")
}
if !strings.Contains(rowHeld.Content, "HELD") {
t.Fatalf("Expected row content to contain 'HELD', got: %s", rowHeld.Content)
}
}
func TestActiveSlotDoesNotAutoHold(t *testing.T) {
origSystems := ConfiguredSystem
defer func() {
ConfiguredSystem = origSystems
}()
ConfiguredSystem = []TargetSystem{
{Name: "ACTIVE_SYS", Description: "Active interactive system"},
}
clientServer, clientUser := net.Pipe()
defer clientServer.Close()
defer clientUser.Close()
upServer, upClient := net.Pipe()
defer upServer.Close()
defer upClient.Close()
// Drain clientUser in background
go func() {
buf := make([]byte, 4096)
for {
_, err := clientUser.Read(buf)
if err != nil {
return
}
}
}()
initialScreen := []byte{CMD_ERASE_WRITE, 0xC3, 0x11, 0x40, 0x40, 'B', 'A', 'S', 'E', IAC, EOR}
slot := &SessionSlot{
System: ConfiguredSystem[0],
Upstream: upClient,
LastScreen: initialScreen,
MaxBufferSize: 200,
AutoHoldBurstLimit: 5,
}
s := &UserMasterSession{
Username: "ACTIVEUSER",
DisplayTag: "actv1234",
TerminalType: "IBM-3278-2",
ClientConn: clientServer,
ActiveSlotIdx: 0, // ACTIVE (user is viewing slot 0)
Slots: []*SessionSlot{slot},
IsDisconnected: false,
}
go upstreamReader(s, slot, 0)
// Send 10 writes rapidly
smallUpdate := []byte{CMD_WRITE, 0xC3, 0x11, 0x40, 0x50, 'Z', IAC, EOR}
for i := 0; i < 10; i++ {
_, err := upServer.Write(smallUpdate)
if err != nil {
t.Fatalf("Failed to write update %d: %v", i, err)
}
}
time.Sleep(100 * time.Millisecond)
slot.Mu.Lock()
isHeld := slot.IsHeld
slot.Mu.Unlock()
if isHeld {
t.Fatal("Expected active slot (user watching) to NOT be placed on automatic HOLD")
}
}
+3 -1
View File
@@ -309,9 +309,11 @@ func runMenuLoop(s *UserMasterSession, clientConn net.Conn) {
if idx < len(s.Slots) && s.Slots[idx] != nil { if idx < len(s.Slots) && s.Slots[idx] != nil {
slot := s.Slots[idx] slot := s.Slots[idx]
slot.Mu.Lock() slot.Mu.Lock()
slot.IsHeld = hold
if hold { if hold {
slot.IsHeld = true
slot.ScreenChanged = false slot.ScreenChanged = false
} else {
slot.resetHoldAndLoopCountersLocked()
} }
slot.Mu.Unlock() slot.Mu.Unlock()
if hold { if hold {
+41 -16
View File
@@ -3,6 +3,7 @@ package main
import ( import (
"net" "net"
"sync" "sync"
"time"
"github.com/racingmars/go3270" "github.com/racingmars/go3270"
) )
@@ -108,12 +109,14 @@ type TargetSystem struct {
} }
type ServerConfig struct { type ServerConfig struct {
BindAddress string `toml:"bind_address"` BindAddress string `toml:"bind_address"`
Codepage string `toml:"codepage"` Codepage string `toml:"codepage"`
TLS bool `toml:"tls"` TLS bool `toml:"tls"`
CertFile string `toml:"cert_file"` CertFile string `toml:"cert_file"`
KeyFile string `toml:"key_file"` KeyFile string `toml:"key_file"`
Public bool `toml:"public"` Public bool `toml:"public"`
AutoHoldDiscardLimit int `toml:"auto_hold_discard_limit"`
AutoHoldBurstLimit int `toml:"auto_hold_burst_limit"`
} }
type LoginConfig struct { type LoginConfig struct {
@@ -146,17 +149,39 @@ type Config struct {
Users []UserConfig `toml:"users"` Users []UserConfig `toml:"users"`
} }
const (
defaultAutoHoldDiscardLimit = 5
autoHoldDiscardWindow = 5 * time.Second
defaultAutoHoldBurstLimit = 20
autoHoldBurstWindow = 1 * time.Second
)
type SessionSlot struct { type SessionSlot struct {
System TargetSystem System TargetSystem
Upstream net.Conn Upstream net.Conn
LastScreen []byte LastScreen []byte
ScreenChanged bool ScreenChanged bool
IsTN3270E bool IsTN3270E bool
IsBound bool IsBound bool
SeqNumber uint16 SeqNumber uint16
IsHeld bool IsHeld bool
MaxBufferSize int MaxBufferSize int
Mu sync.Mutex DiscardCount int
DiscardWindow time.Time
WriteBurstCount int
WriteBurstWindow time.Time
AutoHoldDiscardLimit int
AutoHoldBurstLimit int
Mu sync.Mutex
}
func (s *SessionSlot) resetHoldAndLoopCountersLocked() {
s.IsHeld = false
s.ScreenChanged = false
s.DiscardCount = 0
s.DiscardWindow = time.Time{}
s.WriteBurstCount = 0
s.WriteBurstWindow = time.Time{}
} }
type slotRenderInfo struct { type slotRenderInfo struct {