feat: add local cdp startup and external association

This commit is contained in:
QiuSW
2026-07-25 15:53:31 +08:00
parent 972c637716
commit 4063b851eb
17 changed files with 845 additions and 158 deletions
+72 -17
View File
@@ -107,7 +107,16 @@ func runWindow(logger *slog.Logger) {
directoryPicker := files.NewDirectoryPicker()
shell.OnChooseDirectory(directoryPicker.ChooseDirectory, window.Invalidate)
discoverer := browser.NewDiscoverer()
shell.OnStartInstance(instanceStarter{launcher: browser.NewOSLauncher(), resolver: discoverer}.Start, window.Invalidate)
launcher := browser.NewOSLauncher()
cdp := browser.NewCDPInspector()
shell.OnStartInstance(instanceStarter{
launcher: launcher,
resolver: discoverer,
managedProfiles: launcher,
externalProfiles: browser.NewWindowsProfileInspector(),
remoteDebug: cdp,
portAllocator: cdp,
}.Start, window.Invalidate)
shell.OnPathSearch(func(ctx context.Context, field ui.PathField, current string) (string, error) {
switch field {
case ui.PathChromeExecutable:
@@ -137,14 +146,14 @@ func runWindow(logger *slog.Logger) {
if path, err := config.DefaultPath(); err == nil {
if store, err := config.New(path); err == nil {
if saved, err := store.Load(); err == nil {
shell.SetSettings(ui.SettingsState{ChromePath: saved.Settings.ChromePath, EdgePath: saved.Settings.EdgePath, DefaultDir: saved.Settings.DefaultDir, LogDir: saved.Settings.LogDir, CloseOnExit: saved.Settings.CloseOnExit})
shell.SetSettings(ui.SettingsState{ChromePath: saved.Settings.ChromePath, EdgePath: saved.Settings.EdgePath, DefaultDir: saved.Settings.DefaultDir, LogDir: saved.Settings.LogDir, RemoteDebugStartPort: saved.Settings.RemoteDebugStartPort, CloseOnExit: saved.Settings.CloseOnExit})
rows := make([]ui.InstanceRow, 0, len(saved.Instances))
for _, item := range saved.Instances {
rows = append(rows, ui.InstanceRow{ID: item.ID, Name: item.Name, Browser: browserLabel(item.Launch.Kind), UserDataDir: item.Launch.UserDataDir, TargetURL: item.Launch.TargetURL, Status: "已退出"})
}
shell.SetInstances(rows)
shell.OnSave(func(value ui.SettingsState) {
saved.Settings = config.Settings{ChromePath: value.ChromePath, EdgePath: value.EdgePath, DefaultDir: value.DefaultDir, LogDir: value.LogDir, CloseOnExit: value.CloseOnExit}
saved.Settings = config.Settings{ChromePath: value.ChromePath, EdgePath: value.EdgePath, DefaultDir: value.DefaultDir, LogDir: value.LogDir, RemoteDebugStartPort: value.RemoteDebugStartPort, CloseOnExit: value.CloseOnExit}
go saveSettings(logger, store, configSnapshot(saved))
})
shell.OnInstancesChanged(func(rows []ui.InstanceRow) {
@@ -185,18 +194,53 @@ type processLauncher interface {
Start(context.Context, domain.LaunchSpec) (browser.ProcessHandle, error)
}
type instanceStarter struct {
launcher processLauncher
resolver executableResolver
type managedProfileInspector interface {
InspectProfile(context.Context, string) (browser.ProfileUse, error)
}
func (s instanceStarter) Start(ctx context.Context, row ui.InstanceRow, settings ui.SettingsState) (int, error) {
type instanceStarter struct {
launcher processLauncher
resolver executableResolver
managedProfiles managedProfileInspector
externalProfiles browser.ExternalProfileInspector
remoteDebug browser.RemoteDebugEndpointInspector
portAllocator browser.RemoteDebugPortAllocator
}
func (s instanceStarter) Start(ctx context.Context, row ui.InstanceRow, settings ui.SettingsState) (ui.InstanceStartOutcome, error) {
kind, err := browserKind(row.Browser)
if err != nil {
return 0, err
return ui.InstanceStartOutcome{}, err
}
if s.launcher == nil || s.resolver == nil {
return 0, errors.New("浏览器启动服务尚未准备好")
if s.launcher == nil || s.resolver == nil || s.remoteDebug == nil || s.portAllocator == nil {
return ui.InstanceStartOutcome{}, errors.New("浏览器启动服务尚未准备好")
}
if s.managedProfiles != nil {
use, inspectErr := s.managedProfiles.InspectProfile(ctx, row.UserDataDir)
if inspectErr != nil {
return ui.InstanceStartOutcome{}, fmt.Errorf("检查 Chub 实例状态失败:%w", inspectErr)
}
if use.Occupied {
endpoint, _ := s.remoteDebug.InspectRemoteDebugEndpoint(ctx, kind, row.UserDataDir)
return ui.InstanceStartOutcome{PID: use.PID, RemoteDebugPort: endpoint.Port, Source: use.Source}, &browser.ProfileOccupiedError{UserDataDir: row.UserDataDir, PID: use.PID}
}
}
if s.externalProfiles != nil {
use, inspectErr := s.externalProfiles.InspectProfile(ctx, kind, row.UserDataDir, 0)
if inspectErr != nil {
return ui.InstanceStartOutcome{Source: "unknown"}, fmt.Errorf("检查外部浏览器占用失败:%w", inspectErr)
}
endpoint, endpointErr := s.remoteDebug.InspectRemoteDebugEndpoint(ctx, kind, row.UserDataDir)
if endpointErr == nil {
source := use.Source
if source == "" {
source = "devtools_active_port"
}
return ui.InstanceStartOutcome{PID: use.PID, RemoteDebugPort: endpoint.Port, External: true, Source: source}, nil
}
if use.Occupied {
return ui.InstanceStartOutcome{PID: use.PID, Source: use.Source}, &browser.ProfileOccupiedError{UserDataDir: row.UserDataDir, PID: use.PID}
}
}
configuredExecutable := settings.ChromePath
if kind == domain.BrowserEdge {
@@ -204,18 +248,29 @@ func (s instanceStarter) Start(ctx context.Context, row ui.InstanceRow, settings
}
executable, err := s.resolver.Resolve(ctx, kind, configuredExecutable)
if err != nil {
return 0, fmt.Errorf("无法找到%s可执行文件:%w", browserLabel(kind), err)
return ui.InstanceStartOutcome{}, fmt.Errorf("无法找到%s可执行文件:%w", browserLabel(kind), err)
}
port, err := s.portAllocator.FindAvailableRemoteDebugPort(ctx, settings.RemoteDebugStartPort)
if err != nil {
return ui.InstanceStartOutcome{}, fmt.Errorf("无法分配本地调试端口:%w", err)
}
handle, err := s.launcher.Start(ctx, domain.LaunchSpec{
Kind: kind,
Executable: executable,
UserDataDir: row.UserDataDir,
TargetURL: row.TargetURL,
Kind: kind,
Executable: executable,
UserDataDir: row.UserDataDir,
RemoteDebugPort: port,
TargetURL: row.TargetURL,
})
if err != nil {
return 0, err
return ui.InstanceStartOutcome{}, err
}
return handle.PID(), nil
endpointCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
endpoint, endpointErr := s.remoteDebug.WaitForRemoteDebugPort(endpointCtx, kind, port)
if endpointErr != nil {
return ui.InstanceStartOutcome{PID: handle.PID(), Warning: "本地调试端点不可用"}, nil
}
return ui.InstanceStartOutcome{PID: handle.PID(), RemoteDebugPort: endpoint.Port, Source: "chub_registry"}, nil
}
func browserKind(value string) (domain.BrowserKind, error) {