feat: add reusable proxy configuration
This commit is contained in:
+94
-1
@@ -10,6 +10,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"chub/internal/domain"
|
||||
@@ -110,6 +111,7 @@ func runWindow(logger *slog.Logger) {
|
||||
launcher := browser.NewOSLauncher()
|
||||
cdp := browser.NewCDPInspector()
|
||||
externalProfiles := browser.NewWindowsProfileInspector()
|
||||
proxies := newProxyDirectory()
|
||||
shell.OnStartInstance(instanceStarter{
|
||||
launcher: launcher,
|
||||
resolver: discoverer,
|
||||
@@ -117,6 +119,7 @@ func runWindow(logger *slog.Logger) {
|
||||
externalProfiles: externalProfiles,
|
||||
remoteDebug: cdp,
|
||||
portAllocator: cdp,
|
||||
proxyResolver: proxies,
|
||||
}.Start, window.Invalidate)
|
||||
shell.OnRefreshInstances(instanceStatusRefresher{
|
||||
managedProfiles: launcher,
|
||||
@@ -153,9 +156,11 @@ func runWindow(logger *slog.Logger) {
|
||||
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, RemoteDebugStartPort: saved.Settings.RemoteDebugStartPort, CloseOnExit: saved.Settings.CloseOnExit})
|
||||
proxies.Set(saved.Proxies)
|
||||
shell.SetProxies(proxyOptions(saved.Proxies))
|
||||
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: "已退出"})
|
||||
rows = append(rows, ui.InstanceRow{ID: item.ID, Name: item.Name, Browser: browserLabel(item.Launch.Kind), UserDataDir: item.Launch.UserDataDir, TargetURL: item.Launch.TargetURL, ProxyID: item.ProxyID, Status: "已退出"})
|
||||
}
|
||||
shell.SetInstances(rows)
|
||||
shell.OnSave(func(value ui.SettingsState) {
|
||||
@@ -166,6 +171,11 @@ func runWindow(logger *slog.Logger) {
|
||||
saved.Instances = mergeInstanceConfig(saved.Instances, rows)
|
||||
go saveSettings(logger, store, configSnapshot(saved))
|
||||
})
|
||||
shell.OnProxiesChanged(func(options []ui.ProxyOption) {
|
||||
saved.Proxies = mergeProxyConfig(saved.Proxies, options)
|
||||
proxies.Set(saved.Proxies)
|
||||
go saveSettings(logger, store, configSnapshot(saved))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -192,6 +202,55 @@ func saveSettings(logger *slog.Logger, store *config.Store, value config.File) {
|
||||
}
|
||||
}
|
||||
|
||||
type proxyServerResolver interface {
|
||||
Resolve(string) (string, error)
|
||||
}
|
||||
|
||||
// proxyDirectory is the single in-memory authority used by UI launch requests.
|
||||
// It stores only endpoints already validated by config and never exposes secrets.
|
||||
type proxyDirectory struct {
|
||||
mu sync.RWMutex
|
||||
servers map[string]string
|
||||
}
|
||||
|
||||
func newProxyDirectory() *proxyDirectory {
|
||||
return &proxyDirectory{servers: make(map[string]string)}
|
||||
}
|
||||
|
||||
func (d *proxyDirectory) Set(profiles []config.ProxyProfile) {
|
||||
if d == nil {
|
||||
return
|
||||
}
|
||||
servers := make(map[string]string, len(profiles))
|
||||
for _, profile := range profiles {
|
||||
servers[profile.ID] = profile.Server
|
||||
}
|
||||
d.mu.Lock()
|
||||
d.servers = servers
|
||||
d.mu.Unlock()
|
||||
}
|
||||
|
||||
func (d *proxyDirectory) Resolve(id string) (string, error) {
|
||||
if d == nil || strings.TrimSpace(id) == "" {
|
||||
return "", errors.New("所选代理不可用")
|
||||
}
|
||||
d.mu.RLock()
|
||||
server, exists := d.servers[id]
|
||||
d.mu.RUnlock()
|
||||
if !exists {
|
||||
return "", errors.New("所选代理不可用")
|
||||
}
|
||||
return server, nil
|
||||
}
|
||||
|
||||
func proxyOptions(profiles []config.ProxyProfile) []ui.ProxyOption {
|
||||
options := make([]ui.ProxyOption, 0, len(profiles))
|
||||
for _, profile := range profiles {
|
||||
options = append(options, ui.ProxyOption{ID: profile.ID, Name: profile.Name, Server: profile.Server})
|
||||
}
|
||||
return options
|
||||
}
|
||||
|
||||
type executableResolver interface {
|
||||
Resolve(context.Context, domain.BrowserKind, string) (string, error)
|
||||
}
|
||||
@@ -211,6 +270,7 @@ type instanceStarter struct {
|
||||
externalProfiles browser.ExternalProfileInspector
|
||||
remoteDebug browser.RemoteDebugEndpointInspector
|
||||
portAllocator browser.RemoteDebugPortAllocator
|
||||
proxyResolver proxyServerResolver
|
||||
}
|
||||
|
||||
type instanceStatusRefresher struct {
|
||||
@@ -332,6 +392,16 @@ func (s instanceStarter) Start(ctx context.Context, row ui.InstanceRow, settings
|
||||
return ui.InstanceStartOutcome{PID: use.PID, Source: use.Source}, &browser.ProfileOccupiedError{UserDataDir: row.UserDataDir, PID: use.PID}
|
||||
}
|
||||
}
|
||||
proxyServer := ""
|
||||
if row.ProxyID != "" {
|
||||
if s.proxyResolver == nil {
|
||||
return ui.InstanceStartOutcome{}, errors.New("所选代理不可用")
|
||||
}
|
||||
proxyServer, err = s.proxyResolver.Resolve(row.ProxyID)
|
||||
if err != nil {
|
||||
return ui.InstanceStartOutcome{}, err
|
||||
}
|
||||
}
|
||||
configuredExecutable := settings.ChromePath
|
||||
if kind == domain.BrowserEdge {
|
||||
configuredExecutable = settings.EdgePath
|
||||
@@ -350,6 +420,7 @@ func (s instanceStarter) Start(ctx context.Context, row ui.InstanceRow, settings
|
||||
UserDataDir: row.UserDataDir,
|
||||
RemoteDebugPort: port,
|
||||
TargetURL: row.TargetURL,
|
||||
ProxyServer: proxyServer,
|
||||
})
|
||||
if err != nil {
|
||||
return ui.InstanceStartOutcome{}, err
|
||||
@@ -398,15 +469,37 @@ func mergeInstanceConfig(existing []config.Instance, rows []ui.InstanceRow) []co
|
||||
item.Launch.Kind = kind
|
||||
item.Launch.UserDataDir = row.UserDataDir
|
||||
item.Launch.TargetURL = row.TargetURL
|
||||
item.ProxyID = row.ProxyID
|
||||
if item.ProxyID != "" {
|
||||
item.Launch.ProxyServer = ""
|
||||
}
|
||||
item.UpdatedAt = time.Now()
|
||||
updated = append(updated, item)
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
func mergeProxyConfig(existing []config.ProxyProfile, options []ui.ProxyOption) []config.ProxyProfile {
|
||||
byID := make(map[string]config.ProxyProfile, len(existing))
|
||||
for _, profile := range existing {
|
||||
byID[profile.ID] = profile
|
||||
}
|
||||
updated := make([]config.ProxyProfile, 0, len(options))
|
||||
for _, option := range options {
|
||||
profile := byID[option.ID]
|
||||
profile.ID = option.ID
|
||||
profile.Name = option.Name
|
||||
profile.Server = option.Server
|
||||
profile.UpdatedAt = time.Now()
|
||||
updated = append(updated, profile)
|
||||
}
|
||||
return updated
|
||||
}
|
||||
|
||||
func configSnapshot(value config.File) config.File {
|
||||
clone := value
|
||||
clone.Instances = append([]config.Instance(nil), value.Instances...)
|
||||
clone.Proxies = append([]config.ProxyProfile(nil), value.Proxies...)
|
||||
for i := range clone.Instances {
|
||||
clone.Instances[i].Launch.ExtraArgs = append([]string(nil), clone.Instances[i].Launch.ExtraArgs...)
|
||||
}
|
||||
|
||||
@@ -37,6 +37,23 @@ func TestInstanceStarterBuildsLaunchSpecFromInstanceAndSettings(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstanceStarterResolvesSelectedProxyAtLaunch(t *testing.T) {
|
||||
launcher := &fakeProcessLauncher{handle: fakeProcessHandle{pid: 4242}}
|
||||
resolver := &fakeExecutableResolver{path: `C:\Browser\chrome.exe`}
|
||||
remote := &fakeRemoteDebugInspector{endpoint: browser.RemoteDebugEndpoint{Port: 9666}, inspectErr: browser.ErrRemoteDebugEndpointUnavailable}
|
||||
proxies := newProxyDirectory()
|
||||
proxies.Set([]config.ProxyProfile{{ID: "proxy-sg", Name: "SG", Server: "http://127.0.0.1:8080"}})
|
||||
starter := instanceStarter{launcher: launcher, resolver: resolver, managedProfiles: fakeManagedProfileInspector{}, externalProfiles: fakeExternalProfileInspector{}, remoteDebug: remote, portAllocator: remote, proxyResolver: proxies}
|
||||
_, err := starter.Start(context.Background(), ui.InstanceRow{ID: "chrome-a", Browser: "Chrome", UserDataDir: `C:\profiles\chrome-a`, ProxyID: "proxy-sg"}, ui.SettingsState{RemoteDebugStartPort: 9666})
|
||||
if err != nil || launcher.spec.ProxyServer != "http://127.0.0.1:8080" {
|
||||
t.Fatalf("proxy launch spec = %#v, error = %v", launcher.spec, err)
|
||||
}
|
||||
_, err = starter.Start(context.Background(), ui.InstanceRow{ID: "missing", Browser: "Chrome", UserDataDir: `C:\profiles\missing`, ProxyID: "gone"}, ui.SettingsState{RemoteDebugStartPort: 9666})
|
||||
if err == nil || launcher.calls != 1 {
|
||||
t.Fatalf("missing proxy error = %v, launch calls = %d", err, launcher.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstanceStarterAssociatesVerifiedExternalProfileWithoutLaunching(t *testing.T) {
|
||||
launcher := &fakeProcessLauncher{handle: fakeProcessHandle{pid: 4242}}
|
||||
remote := &fakeRemoteDebugInspector{endpoint: browser.RemoteDebugEndpoint{Port: 9668}}
|
||||
@@ -92,6 +109,13 @@ func TestMergeInstanceConfigPreservesExistingLaunchOptions(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeInstanceConfigPersistsProxySelection(t *testing.T) {
|
||||
updated := mergeInstanceConfig(nil, []ui.InstanceRow{{ID: "one", Name: "实例", Browser: "Chrome", UserDataDir: `C:\profiles\one`, ProxyID: "proxy-sg"}})
|
||||
if len(updated) != 1 || updated[0].ProxyID != "proxy-sg" || updated[0].Launch.ProxyServer != "" {
|
||||
t.Fatalf("merged proxy = %#v", updated)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstanceStatusRefresherChecksOnlyConfiguredRows(t *testing.T) {
|
||||
remote := &fakeRemoteDebugInspector{endpoint: browser.RemoteDebugEndpoint{Port: 9777}}
|
||||
refresher := instanceStatusRefresher{
|
||||
|
||||
Reference in New Issue
Block a user