115 lines
3.3 KiB
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()
|
|
}
|