feat: refresh and edit browser instances

This commit is contained in:
QiuSW
2026-07-25 16:36:26 +08:00
parent 1a453cf8f0
commit be9292c376
7 changed files with 946 additions and 51 deletions
+91 -1
View File
@@ -109,14 +109,20 @@ func runWindow(logger *slog.Logger) {
discoverer := browser.NewDiscoverer()
launcher := browser.NewOSLauncher()
cdp := browser.NewCDPInspector()
externalProfiles := browser.NewWindowsProfileInspector()
shell.OnStartInstance(instanceStarter{
launcher: launcher,
resolver: discoverer,
managedProfiles: launcher,
externalProfiles: browser.NewWindowsProfileInspector(),
externalProfiles: externalProfiles,
remoteDebug: cdp,
portAllocator: cdp,
}.Start, window.Invalidate)
shell.OnRefreshInstances(instanceStatusRefresher{
managedProfiles: launcher,
externalProfiles: externalProfiles,
remoteDebug: cdp,
}.Refresh, window.Invalidate)
shell.OnPathSearch(func(ctx context.Context, field ui.PathField, current string) (string, error) {
switch field {
case ui.PathChromeExecutable:
@@ -207,6 +213,90 @@ type instanceStarter struct {
portAllocator browser.RemoteDebugPortAllocator
}
type instanceStatusRefresher struct {
managedProfiles managedProfileInspector
externalProfiles browser.ExternalProfileInspector
remoteDebug browser.RemoteDebugEndpointInspector
}
func (s instanceStatusRefresher) Refresh(ctx context.Context, rows []ui.InstanceRow) ([]ui.InstanceRefreshResult, error) {
results := make([]ui.InstanceRefreshResult, 0, len(rows))
for _, row := range rows {
if err := ctx.Err(); err != nil {
return results, err
}
results = append(results, s.refreshOne(ctx, row))
}
return results, nil
}
func (s instanceStatusRefresher) refreshOne(ctx context.Context, row ui.InstanceRow) ui.InstanceRefreshResult {
result := ui.InstanceRefreshResult{ID: row.ID, Status: "已退出"}
kind, err := browserKind(row.Browser)
if err != nil {
result.Status = "未知占用"
result.Message = "浏览器类型无效,无法刷新实例状态。"
return result
}
if s.managedProfiles != nil {
use, inspectErr := s.managedProfiles.InspectProfile(ctx, row.UserDataDir)
if inspectErr != nil {
result.Status = "未知占用"
result.Message = "无法检查 Chub 托管实例状态。"
return result
}
if use.Occupied {
result.Status = "运行中"
result.PID = use.PID
result.OccupancySource = use.Source
if s.remoteDebug == nil || row.RemoteDebugPort <= 0 {
result.Status = "运行中(调试不可用)"
result.Message = "未记录可验证的本地调试端口。"
return result
}
endpoint, endpointErr := s.remoteDebug.InspectRemoteDebugPort(ctx, kind, row.RemoteDebugPort)
if endpointErr != nil {
result.Status = "运行中(调试不可用)"
result.Message = "本地调试端点当前不可用。"
return result
}
result.RemoteDebugPort = endpoint.Port
return result
}
}
var externalUse browser.ProfileUse
if s.externalProfiles != nil {
use, inspectErr := s.externalProfiles.InspectProfile(ctx, kind, row.UserDataDir, row.PID)
if inspectErr != nil {
result.Status = "未知占用"
result.Message = "无法检查外部浏览器占用状态。"
return result
}
externalUse = use
}
if s.remoteDebug != nil {
endpoint, endpointErr := s.remoteDebug.InspectRemoteDebugEndpoint(ctx, kind, row.UserDataDir)
if endpointErr == nil {
result.Status = "外部已关联"
result.PID = externalUse.PID
result.RemoteDebugPort = endpoint.Port
result.OccupancySource = externalUse.Source
if result.OccupancySource == "" {
result.OccupancySource = "devtools_active_port"
}
return result
}
}
if externalUse.Occupied {
result.Status = "外部占用"
result.PID = externalUse.PID
result.OccupancySource = externalUse.Source
return result
}
return result
}
func (s instanceStarter) Start(ctx context.Context, row ui.InstanceRow, settings ui.SettingsState) (ui.InstanceStartOutcome, error) {
kind, err := browserKind(row.Browser)
if err != nil {
+35
View File
@@ -92,6 +92,41 @@ func TestMergeInstanceConfigPreservesExistingLaunchOptions(t *testing.T) {
}
}
func TestInstanceStatusRefresherChecksOnlyConfiguredRows(t *testing.T) {
remote := &fakeRemoteDebugInspector{endpoint: browser.RemoteDebugEndpoint{Port: 9777}}
refresher := instanceStatusRefresher{
managedProfiles: fakeManagedProfileInspector{use: browser.ProfileUse{Occupied: true, PID: 5432, Source: "chub_registry"}},
externalProfiles: fakeExternalProfileInspector{},
remoteDebug: remote,
}
rows := []ui.InstanceRow{{ID: "managed", Browser: "Chrome", UserDataDir: `C:\profiles\managed`, RemoteDebugPort: 9777}}
results, err := refresher.Refresh(context.Background(), rows)
if err != nil || len(results) != 1 {
t.Fatalf("Refresh() = %#v, %v", results, err)
}
got := results[0]
if got.ID != "managed" || got.Status != "运行中" || got.PID != 5432 || got.RemoteDebugPort != 9777 || got.OccupancySource != "chub_registry" {
t.Fatalf("refresh result = %#v", got)
}
}
func TestInstanceStatusRefresherAssociatesExternalWithoutLifecycleControl(t *testing.T) {
remote := &fakeRemoteDebugInspector{endpoint: browser.RemoteDebugEndpoint{Port: 9668}}
refresher := instanceStatusRefresher{
managedProfiles: fakeManagedProfileInspector{},
externalProfiles: fakeExternalProfileInspector{use: browser.ProfileUse{Occupied: true, PID: 16108, Source: browser.ProfileSourceMessageWindow}},
remoteDebug: remote,
}
results, err := refresher.Refresh(context.Background(), []ui.InstanceRow{{ID: "external", Browser: "Edge", UserDataDir: `C:\profiles\external`}})
if err != nil || len(results) != 1 {
t.Fatalf("Refresh() = %#v, %v", results, err)
}
got := results[0]
if got.Status != "外部已关联" || got.PID != 16108 || got.RemoteDebugPort != 9668 || got.OccupancySource != browser.ProfileSourceMessageWindow {
t.Fatalf("external refresh result = %#v", got)
}
}
type fakeExecutableResolver struct {
path string
kind domain.BrowserKind