Files
cdp_hub/cmd/chub/managed_exit_monitor.go

115 lines
3.3 KiB
Go

package main
import (
"context"
"sync"
"chub/internal/platform/browser"
"chub/internal/ui"
)
// managedExitObserver keeps lifecycle observation scoped to a browser process
// Chub started in the current application session. It never looks up a process
// by name or attaches to an external browser.
type managedExitObserver interface {
Watch(instanceID string, launchGeneration uint64, handle browser.ProcessHandle)
ExpectStop(instanceID string, launchGeneration uint64, pid int) bool
ClearExpectedStop(instanceID string, launchGeneration uint64, pid int)
}
type managedExitProcessKey struct {
instanceID string
launchGeneration uint64
pid int
}
type managedExitMonitor struct {
ctx context.Context
cancel context.CancelFunc
report func(ui.ManagedInstanceExit)
invalidate func()
mu sync.Mutex
active map[string]managedExitProcessKey
expected map[managedExitProcessKey]bool
}
func newManagedExitMonitor(report func(ui.ManagedInstanceExit), invalidate func()) *managedExitMonitor {
ctx, cancel := context.WithCancel(context.Background())
return &managedExitMonitor{
ctx: ctx,
cancel: cancel,
report: report,
invalidate: invalidate,
active: make(map[string]managedExitProcessKey),
expected: make(map[managedExitProcessKey]bool),
}
}
// Close stops notification delivery without stopping browsers. Process waits
// observe this context and return, while the launcher's own registry cleanup
// continues independently.
func (m *managedExitMonitor) Close() {
if m != nil && m.cancel != nil {
m.cancel()
}
}
func (m *managedExitMonitor) Watch(instanceID string, launchGeneration uint64, handle browser.ProcessHandle) {
if m == nil || handle == nil || instanceID == "" || launchGeneration == 0 || handle.PID() <= 0 {
return
}
key := managedExitProcessKey{instanceID: instanceID, launchGeneration: launchGeneration, pid: handle.PID()}
m.mu.Lock()
if _, alreadyWatching := m.active[instanceID]; alreadyWatching {
m.mu.Unlock()
return
}
m.active[instanceID] = key
m.mu.Unlock()
go func() {
exitCode, _ := handle.Wait(m.ctx)
if m.ctx.Err() != nil {
return
}
m.mu.Lock()
expected := m.expected[key]
delete(m.expected, key)
if current, ok := m.active[instanceID]; ok && current == key {
delete(m.active, instanceID)
}
m.mu.Unlock()
if m.report != nil {
m.report(ui.ManagedInstanceExit{InstanceID: instanceID, LaunchGeneration: launchGeneration, PID: key.pid, ExitCode: exitCode, ExpectedStop: expected})
}
if m.invalidate != nil {
m.invalidate()
}
}()
}
func (m *managedExitMonitor) ExpectStop(instanceID string, launchGeneration uint64, pid int) bool {
if m == nil || instanceID == "" || launchGeneration == 0 || pid <= 0 {
return false
}
key := managedExitProcessKey{instanceID: instanceID, launchGeneration: launchGeneration, pid: pid}
m.mu.Lock()
defer m.mu.Unlock()
if current, ok := m.active[instanceID]; !ok || current != key {
return false
}
m.expected[key] = true
return true
}
func (m *managedExitMonitor) ClearExpectedStop(instanceID string, launchGeneration uint64, pid int) {
if m == nil {
return
}
key := managedExitProcessKey{instanceID: instanceID, launchGeneration: launchGeneration, pid: pid}
m.mu.Lock()
delete(m.expected, key)
m.mu.Unlock()
}