mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-15 16:14:43 +00:00
feat(browser): add timeout, idle auto-close, and max-pages safety mechanisms
Prevent resource leaks and hanging actions in the browser tool: - Per-action context timeout (default 30s, configurable via timeoutMs param or config) - Idle page reaper goroutine closes pages unused for 10min (configurable) - Max pages per tenant (default 5) with LRU eviction - RefStore cleanup on page close/evict/reap to prevent memory leaks
This commit is contained in:
1 parent
afbecd3e4c
commit
ee4d34ae28
8 files changed
+226
-12
No files matched your search
+62
-9
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/go-rod/rod"
|
||||
"github.com/go-rod/rod/lib/launcher"
|
||||
@@ -19,9 +20,14 @@ type Manager struct {
|
||||
console map[string][]ConsoleMessage // targetID → console messages
|
||||
tenantCtxs map[string]*rod.Browser // tenantID → incognito browser context
|
||||
pageTenants map[string]string // targetID → tenantID (for filtering)
|
||||
headless bool
|
||||
remoteURL string // CDP endpoint for remote Chrome (sidecar); skips local launcher
|
||||
logger *slog.Logger
|
||||
pageLastUsed map[string]time.Time // targetID → last access time
|
||||
headless bool
|
||||
remoteURL string // CDP endpoint for remote Chrome (sidecar); skips local launcher
|
||||
actionTimeout time.Duration // per-action context timeout (default 30s)
|
||||
idleTimeout time.Duration // auto-close pages idle longer than this (default 10m, 0=disabled)
|
||||
maxPages int // max open pages per tenant (default 5)
|
||||
stopReaper chan struct{} // signal to stop the reaper goroutine
|
||||
logger *slog.Logger
|
||||
}
|
||||
|
||||
// Option configures a Manager.
|
||||
@@ -43,15 +49,34 @@ func WithLogger(l *slog.Logger) Option {
|
||||
return func(m *Manager) { m.logger = l }
|
||||
}
|
||||
|
||||
// WithActionTimeout sets the per-action context timeout.
|
||||
func WithActionTimeout(d time.Duration) Option {
|
||||
return func(m *Manager) { m.actionTimeout = d }
|
||||
}
|
||||
|
||||
// WithIdleTimeout sets the idle page auto-close timeout. 0 disables the reaper.
|
||||
func WithIdleTimeout(d time.Duration) Option {
|
||||
return func(m *Manager) { m.idleTimeout = d }
|
||||
}
|
||||
|
||||
// WithMaxPages sets the max open pages per tenant.
|
||||
func WithMaxPages(n int) Option {
|
||||
return func(m *Manager) { m.maxPages = n }
|
||||
}
|
||||
|
||||
// New creates a Manager with options.
|
||||
func New(opts ...Option) *Manager {
|
||||
m := &Manager{
|
||||
refs: NewRefStore(),
|
||||
pages: make(map[string]*rod.Page),
|
||||
console: make(map[string][]ConsoleMessage),
|
||||
tenantCtxs: make(map[string]*rod.Browser),
|
||||
pageTenants: make(map[string]string),
|
||||
logger: slog.Default(),
|
||||
refs: NewRefStore(),
|
||||
pages: make(map[string]*rod.Page),
|
||||
console: make(map[string][]ConsoleMessage),
|
||||
tenantCtxs: make(map[string]*rod.Browser),
|
||||
pageTenants: make(map[string]string),
|
||||
pageLastUsed: make(map[string]time.Time),
|
||||
actionTimeout: 30 * time.Second,
|
||||
idleTimeout: 10 * time.Minute,
|
||||
maxPages: 5,
|
||||
logger: slog.Default(),
|
||||
}
|
||||
for _, o := range opts {
|
||||
o(m)
|
||||
@@ -59,6 +84,16 @@ func New(opts ...Option) *Manager {
|
||||
return m
|
||||
}
|
||||
|
||||
// ActionTimeout returns the configured per-action timeout.
|
||||
func (m *Manager) ActionTimeout() time.Duration {
|
||||
return m.actionTimeout
|
||||
}
|
||||
|
||||
// touchPageLocked updates the last-used timestamp for a page. Must be called with mu held.
|
||||
func (m *Manager) touchPageLocked(targetID string) {
|
||||
m.pageLastUsed[targetID] = time.Now()
|
||||
}
|
||||
|
||||
// Start launches a local Chrome browser or connects to a remote one.
|
||||
// If already connected but the connection is dead, it reconnects automatically.
|
||||
func (m *Manager) Start(ctx context.Context) error {
|
||||
@@ -112,11 +147,28 @@ func (m *Manager) Start(ctx context.Context) error {
|
||||
}
|
||||
|
||||
m.browser = b
|
||||
|
||||
// Start idle-page reaper if configured
|
||||
if m.idleTimeout > 0 && m.stopReaper == nil {
|
||||
m.stopReaper = make(chan struct{})
|
||||
go m.runReaper()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop closes the Chrome browser (local) or disconnects (remote sidecar).
|
||||
func (m *Manager) Stop(ctx context.Context) error {
|
||||
// Grab and nil-out stopReaper under the lock, then close outside to avoid
|
||||
// deadlock (reaper goroutine also acquires mu).
|
||||
m.mu.Lock()
|
||||
ch := m.stopReaper
|
||||
m.stopReaper = nil
|
||||
m.mu.Unlock()
|
||||
if ch != nil {
|
||||
close(ch)
|
||||
}
|
||||
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
|
||||
@@ -137,6 +189,7 @@ func (m *Manager) Stop(ctx context.Context) error {
|
||||
m.pages = make(map[string]*rod.Page)
|
||||
m.console = make(map[string][]ConsoleMessage)
|
||||
m.pageTenants = make(map[string]string)
|
||||
m.pageLastUsed = make(map[string]time.Time)
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user