feat: toggle managed instance start and stop

This commit is contained in:
QiuSW
2026-07-25 17:10:20 +08:00
parent 3a3c92c875
commit b4e16fa708
7 changed files with 390 additions and 16 deletions
+71
View File
@@ -121,6 +121,7 @@ func runWindow(logger *slog.Logger) {
portAllocator: cdp,
proxyResolver: proxies,
}.Start, window.Invalidate)
shell.OnStopInstance(instanceStopper{launcher: launcher, managedProfiles: launcher}.Stop, window.Invalidate)
shell.OnRefreshInstances(instanceStatusRefresher{
managedProfiles: launcher,
externalProfiles: externalProfiles,
@@ -263,6 +264,76 @@ type managedProfileInspector interface {
InspectProfile(context.Context, string) (browser.ProfileUse, error)
}
type profileStopper interface {
StopProfile(context.Context, string, bool) error
}
// instanceStopper is deliberately scoped to Chub's in-memory registry. It
// requests graceful exit only, then waits for that registry entry to disappear;
// it never enumerates or terminates browsers by process name.
type instanceStopper struct {
launcher profileStopper
managedProfiles managedProfileInspector
waitTimeout time.Duration
waitInterval time.Duration
}
func (s instanceStopper) Stop(ctx context.Context, row ui.InstanceRow) error {
if row.Status != "运行中" && row.Status != "运行中(调试不可用)" {
return errors.New("实例当前不是可停止的 Chub 托管状态")
}
if row.OccupancySource != "" && row.OccupancySource != "chub_registry" {
return domain.ErrIdentityMismatch
}
if s.launcher == nil || s.managedProfiles == nil {
return errors.New("浏览器停止服务尚未准备好")
}
use, err := s.managedProfiles.InspectProfile(ctx, row.UserDataDir)
if err != nil {
return fmt.Errorf("检查 Chub 实例身份失败:%w", err)
}
if !use.Occupied {
return domain.ErrInstanceNotFound
}
if use.Source != "chub_registry" || (row.PID > 0 && use.PID > 0 && row.PID != use.PID) {
return domain.ErrIdentityMismatch
}
if err := s.launcher.StopProfile(ctx, row.UserDataDir, false); err != nil {
return err
}
timeout := s.waitTimeout
if timeout <= 0 {
timeout = 8 * time.Second
}
interval := s.waitInterval
if interval <= 0 {
interval = 100 * time.Millisecond
}
waitCtx, cancel := context.WithTimeout(ctx, timeout)
defer cancel()
for {
use, err = s.managedProfiles.InspectProfile(waitCtx, row.UserDataDir)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) {
return errors.New("浏览器未在等待时间内退出;可稍后刷新状态")
}
return fmt.Errorf("检查浏览器退出状态失败:%w", err)
}
if !use.Occupied {
return nil
}
timer := time.NewTimer(interval)
select {
case <-waitCtx.Done():
if !timer.Stop() {
<-timer.C
}
return errors.New("浏览器未在等待时间内退出;可稍后刷新状态")
case <-timer.C:
}
}
}
type instanceStarter struct {
launcher processLauncher
resolver executableResolver