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() }