feat: add portable directory safeguards
This commit is contained in:
+33
-8
@@ -29,8 +29,8 @@ import (
|
||||
const version = "0.1.0-dev"
|
||||
|
||||
var (
|
||||
openDefaultConfig = config.OpenDefault
|
||||
defaultInstanceUserDataRoot = config.DefaultInstanceUserDataRoot
|
||||
openDefaultConfig = config.OpenDefault
|
||||
defaultPortableDirectories = config.DefaultPortableDirectories
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -105,11 +105,11 @@ func runWindow(logger *slog.Logger) {
|
||||
window.Option(app.Title("Chub"), app.Size(unit.Dp(1100), unit.Dp(720)))
|
||||
theme := material.NewTheme()
|
||||
shell := ui.NewShell(theme)
|
||||
if root, err := defaultInstanceUserDataRoot(); err != nil {
|
||||
logger.Error("resolve default instance user data root", "error", err)
|
||||
shell.ReportStartupIssue("无法确定程序目录中的默认 User Data Dir;新建实例时请选择其他绝对路径。")
|
||||
if directories, err := defaultPortableDirectories(); err != nil {
|
||||
logger.Error("resolve portable directories", "error", err)
|
||||
shell.ReportStartupIssue("无法确定程序目录中的默认 User Data Dir 和日志目录;请检查程序位置后重试。")
|
||||
} else {
|
||||
shell.SetDefaultInstanceUserDataRoot(root)
|
||||
shell.SetPortableDirectoryDefaults(directories.DefaultDir, directories.LogDir)
|
||||
}
|
||||
directoryPicker := files.NewDirectoryPicker()
|
||||
shell.OnChooseDirectory(directoryPicker.ChooseDirectory, window.Invalidate)
|
||||
@@ -131,6 +131,7 @@ func runWindow(logger *slog.Logger) {
|
||||
portAllocator: cdp,
|
||||
proxyResolver: proxies,
|
||||
monitor: exitMonitor,
|
||||
dataPreparer: files.NewInstanceDataPreparer(),
|
||||
}.Start, window.Invalidate)
|
||||
shell.OnStopInstance(instanceStopper{launcher: launcher, managedProfiles: launcher, monitor: exitMonitor}.Stop, window.Invalidate)
|
||||
dataRemover := files.NewInstanceDataRemover()
|
||||
@@ -179,7 +180,24 @@ func runWindow(logger *slog.Logger) {
|
||||
logger.Error("load default configuration", "error", err)
|
||||
shell.ReportStartupIssue("程序目录中的 config.json 无法读取;请恢复有效配置后重试。")
|
||||
} else {
|
||||
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})
|
||||
logPreparer := files.NewDirectoryPreparer()
|
||||
if err := logPreparer.PrepareDirectory(context.Background(), saved.Settings.LogDir); err != nil {
|
||||
logger.Error("prepare log directory", "error", err)
|
||||
shell.ReportStartupIssue("日志目录不可写;请在设置中选择可写目录后重试。")
|
||||
} else if fileLogger, closer, logErr := logging.OpenFile(saved.Settings.LogDir); logErr != nil {
|
||||
logger.Error("open diagnostic log", "error", logErr)
|
||||
shell.ReportStartupIssue("日志目录不可写;请在设置中选择可写目录后重试。")
|
||||
} else {
|
||||
logger = fileLogger
|
||||
defer closer.Close()
|
||||
logger.Info("diagnostic log opened", "version", version)
|
||||
}
|
||||
if directories, directoriesErr := store.PortableDirectories(); directoriesErr != nil {
|
||||
logger.Error("resolve portable settings directories", "error", directoriesErr)
|
||||
} else {
|
||||
shell.SetPortableDirectoryDefaults(directories.DefaultDir, directories.LogDir)
|
||||
}
|
||||
shell.SetSettings(ui.SettingsState{ChromePath: saved.Settings.ChromePath, EdgePath: saved.Settings.EdgePath, DefaultDir: saved.Settings.DefaultDir, DefaultDirMode: string(saved.Settings.DefaultDirSource), LogDir: saved.Settings.LogDir, LogDirMode: string(saved.Settings.LogDirSource), 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))
|
||||
@@ -188,7 +206,7 @@ func runWindow(logger *slog.Logger) {
|
||||
}
|
||||
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, RemoteDebugStartPort: value.RemoteDebugStartPort, CloseOnExit: value.CloseOnExit}
|
||||
saved.Settings = config.Settings{ChromePath: value.ChromePath, EdgePath: value.EdgePath, DefaultDir: value.DefaultDir, DefaultDirSource: config.DirectorySource(value.DefaultDirMode), LogDir: value.LogDir, LogDirSource: config.DirectorySource(value.LogDirMode), RemoteDebugStartPort: value.RemoteDebugStartPort, CloseOnExit: value.CloseOnExit}
|
||||
go saveSettings(logger, store, configSnapshot(saved))
|
||||
})
|
||||
shell.OnInstancesChanged(func(rows []ui.InstanceRow) {
|
||||
@@ -372,6 +390,7 @@ type instanceStarter struct {
|
||||
portAllocator browser.RemoteDebugPortAllocator
|
||||
proxyResolver proxyServerResolver
|
||||
monitor managedExitObserver
|
||||
dataPreparer files.InstanceDataPreparer
|
||||
}
|
||||
|
||||
type instanceStatusRefresher struct {
|
||||
@@ -561,6 +580,12 @@ 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}
|
||||
}
|
||||
}
|
||||
if s.dataPreparer == nil {
|
||||
return ui.InstanceStartOutcome{}, errors.New("User Data Dir 预检服务尚未准备好")
|
||||
}
|
||||
if err := s.dataPreparer.PrepareInstanceDataDir(ctx, row.UserDataDir); err != nil {
|
||||
return ui.InstanceStartOutcome{}, fmt.Errorf("User Data Dir 不可写,请选择其他目录:%w", err)
|
||||
}
|
||||
proxyServer := ""
|
||||
if row.ProxyID != "" {
|
||||
if s.proxyResolver == nil {
|
||||
|
||||
+43
-3
@@ -54,7 +54,7 @@ func TestInstanceStarterBuildsLaunchSpecFromInstanceAndSettings(t *testing.T) {
|
||||
launcher := &fakeProcessLauncher{handle: fakeProcessHandle{pid: 4242}}
|
||||
resolver := &fakeExecutableResolver{path: `C:\Browser\msedge.exe`}
|
||||
remote := &fakeRemoteDebugInspector{endpoint: browser.RemoteDebugEndpoint{Port: 9666}, inspectErr: browser.ErrRemoteDebugEndpointUnavailable}
|
||||
starter := instanceStarter{launcher: launcher, resolver: resolver, managedProfiles: fakeManagedProfileInspector{}, externalProfiles: fakeExternalProfileInspector{}, remoteDebug: remote, portAllocator: remote}
|
||||
starter := instanceStarter{launcher: launcher, resolver: resolver, managedProfiles: fakeManagedProfileInspector{}, externalProfiles: fakeExternalProfileInspector{}, remoteDebug: remote, portAllocator: remote, dataPreparer: &fakeInstanceDataPreparer{}}
|
||||
outcome, err := starter.Start(context.Background(), ui.InstanceRow{ID: "edge-a", Browser: "Edge", UserDataDir: `C:\profiles\edge-a`}, ui.SettingsState{EdgePath: `C:\Configured\msedge.exe`, RemoteDebugStartPort: 9666}, 1)
|
||||
if err != nil || outcome.PID != 4242 || outcome.RemoteDebugPort != 9666 {
|
||||
t.Fatalf("Start() = %#v, %v", outcome, err)
|
||||
@@ -67,6 +67,28 @@ func TestInstanceStarterBuildsLaunchSpecFromInstanceAndSettings(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstanceStarterRejectsUnwritableDataDirectoryBeforeLaunch(t *testing.T) {
|
||||
launcher := &fakeProcessLauncher{handle: fakeProcessHandle{pid: 4242}}
|
||||
preparer := &fakeInstanceDataPreparer{err: files.ErrInstanceDataDirectoryUnavailable}
|
||||
remote := &fakeRemoteDebugInspector{endpoint: browser.RemoteDebugEndpoint{Port: 9666}, inspectErr: browser.ErrRemoteDebugEndpointUnavailable}
|
||||
starter := instanceStarter{
|
||||
launcher: launcher,
|
||||
resolver: &fakeExecutableResolver{path: `C:\Browser\chrome.exe`},
|
||||
managedProfiles: fakeManagedProfileInspector{},
|
||||
externalProfiles: fakeExternalProfileInspector{},
|
||||
remoteDebug: remote,
|
||||
portAllocator: remote,
|
||||
dataPreparer: preparer,
|
||||
}
|
||||
err := func() error {
|
||||
_, err := starter.Start(context.Background(), ui.InstanceRow{ID: "chrome-a", Browser: "Chrome", UserDataDir: `C:\profiles\chrome-a`}, ui.SettingsState{RemoteDebugStartPort: 9666}, 1)
|
||||
return err
|
||||
}()
|
||||
if !errors.Is(err, files.ErrInstanceDataDirectoryUnavailable) || launcher.calls != 0 || preparer.calls != 1 {
|
||||
t.Fatalf("Start() error = %v, launcher calls = %d, preparer calls = %d", err, launcher.calls, preparer.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInstanceStarterUsesPreferredPortAndSkipsOtherInstancesReservations(t *testing.T) {
|
||||
launcher := &fakeProcessLauncher{handle: fakeProcessHandle{pid: 4242}}
|
||||
remote := &fakeRemoteDebugInspector{
|
||||
@@ -81,6 +103,7 @@ func TestInstanceStarterUsesPreferredPortAndSkipsOtherInstancesReservations(t *t
|
||||
externalProfiles: fakeExternalProfileInspector{},
|
||||
remoteDebug: remote,
|
||||
portAllocator: remote,
|
||||
dataPreparer: &fakeInstanceDataPreparer{},
|
||||
}
|
||||
outcome, err := starter.Start(context.Background(), ui.InstanceRow{ID: "chrome-a", Browser: "Chrome", UserDataDir: `C:\profiles\chrome-a`, PreferredRemoteDebugPort: 9666}, ui.SettingsState{RemoteDebugStartPort: 9777, ReservedRemoteDebugPorts: []int{9667}}, 1)
|
||||
if err != nil || outcome.RemoteDebugPort != 9668 || launcher.spec.RemoteDebugPort != 9668 {
|
||||
@@ -97,7 +120,7 @@ func TestInstanceStarterResolvesSelectedProxyAtLaunch(t *testing.T) {
|
||||
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}
|
||||
starter := instanceStarter{launcher: launcher, resolver: resolver, managedProfiles: fakeManagedProfileInspector{}, externalProfiles: fakeExternalProfileInspector{}, remoteDebug: remote, portAllocator: remote, proxyResolver: proxies, dataPreparer: &fakeInstanceDataPreparer{}}
|
||||
_, err := starter.Start(context.Background(), ui.InstanceRow{ID: "chrome-a", Browser: "Chrome", UserDataDir: `C:\profiles\chrome-a`, ProxyID: "proxy-sg"}, ui.SettingsState{RemoteDebugStartPort: 9666}, 1)
|
||||
if err != nil || launcher.spec.ProxyServer != "http://127.0.0.1:8080" {
|
||||
t.Fatalf("proxy launch spec = %#v, error = %v", launcher.spec, err)
|
||||
@@ -112,7 +135,7 @@ func TestInstanceStarterRegistersOnlyLaunchedProcessWithExitMonitor(t *testing.T
|
||||
launcher := &fakeProcessLauncher{handle: fakeProcessHandle{pid: 4242}}
|
||||
remote := &fakeRemoteDebugInspector{endpoint: browser.RemoteDebugEndpoint{Port: 9666}, inspectErr: browser.ErrRemoteDebugEndpointUnavailable}
|
||||
monitor := &fakeManagedExitObserver{}
|
||||
starter := instanceStarter{launcher: launcher, resolver: &fakeExecutableResolver{path: `C:\Browser\chrome.exe`}, managedProfiles: fakeManagedProfileInspector{}, externalProfiles: fakeExternalProfileInspector{}, remoteDebug: remote, portAllocator: remote, monitor: monitor}
|
||||
starter := instanceStarter{launcher: launcher, resolver: &fakeExecutableResolver{path: `C:\Browser\chrome.exe`}, managedProfiles: fakeManagedProfileInspector{}, externalProfiles: fakeExternalProfileInspector{}, remoteDebug: remote, portAllocator: remote, monitor: monitor, dataPreparer: &fakeInstanceDataPreparer{}}
|
||||
_, err := starter.Start(context.Background(), ui.InstanceRow{ID: "chrome-a", Browser: "Chrome", UserDataDir: `C:\profiles\chrome-a`}, ui.SettingsState{RemoteDebugStartPort: 9666}, 7)
|
||||
if err != nil || monitor.watchID != "chrome-a" || monitor.watchGeneration != 7 || monitor.watchPID != 4242 {
|
||||
t.Fatalf("monitor watch = %#v, error = %v", monitor, err)
|
||||
@@ -190,6 +213,7 @@ func TestInstanceStarterAssociatesVerifiedExternalProfileWithoutLaunching(t *tes
|
||||
externalProfiles: fakeExternalProfileInspector{use: browser.ProfileUse{Occupied: true, PID: 16108, Source: browser.ProfileSourceMessageWindow}},
|
||||
remoteDebug: remote,
|
||||
portAllocator: remote,
|
||||
dataPreparer: &fakeInstanceDataPreparer{},
|
||||
}
|
||||
outcome, err := starter.Start(context.Background(), ui.InstanceRow{ID: "chrome-a", Browser: "Chrome", UserDataDir: `C:\profiles\chrome-a`}, ui.SettingsState{RemoteDebugStartPort: 9666}, 1)
|
||||
if err != nil || !outcome.External || outcome.PID != 16108 || outcome.RemoteDebugPort != 9668 || outcome.Source != browser.ProfileSourceMessageWindow {
|
||||
@@ -210,6 +234,7 @@ func TestInstanceStarterRejectsUnverifiedExternalProfile(t *testing.T) {
|
||||
externalProfiles: fakeExternalProfileInspector{use: browser.ProfileUse{Occupied: true, PID: 16108, Source: browser.ProfileSourceLock}},
|
||||
remoteDebug: remote,
|
||||
portAllocator: remote,
|
||||
dataPreparer: &fakeInstanceDataPreparer{},
|
||||
}
|
||||
_, err := starter.Start(context.Background(), ui.InstanceRow{ID: "chrome-a", Browser: "Chrome", UserDataDir: `C:\profiles\chrome-a`}, ui.SettingsState{RemoteDebugStartPort: 9666}, 1)
|
||||
if !errors.Is(err, domain.ErrProfileOccupied) {
|
||||
@@ -482,6 +507,21 @@ func (r *fakeInstanceDataRemover) RemoveInstanceData(ctx context.Context, userDa
|
||||
return r.err
|
||||
}
|
||||
|
||||
type fakeInstanceDataPreparer struct {
|
||||
calls int
|
||||
userDataDir string
|
||||
err error
|
||||
}
|
||||
|
||||
func (p *fakeInstanceDataPreparer) PrepareInstanceDataDir(ctx context.Context, userDataDir string) error {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
p.calls++
|
||||
p.userDataDir = userDataDir
|
||||
return p.err
|
||||
}
|
||||
|
||||
type fakeManagedStopper struct {
|
||||
use browser.ProfileUse
|
||||
stopCalls int
|
||||
|
||||
Reference in New Issue
Block a user