feat: support explicit instance data deletion

This commit is contained in:
QiuSW
2026-07-27 23:16:11 +08:00
parent e400172d04
commit cab7e6ac7b
12 changed files with 670 additions and 28 deletions
+43
View File
@@ -133,6 +133,12 @@ func runWindow(logger *slog.Logger) {
monitor: exitMonitor,
}.Start, window.Invalidate)
shell.OnStopInstance(instanceStopper{launcher: launcher, managedProfiles: launcher, monitor: exitMonitor}.Stop, window.Invalidate)
dataRemover := files.NewInstanceDataRemover()
shell.OnDeleteInstanceData(instanceDataDeleter{
managedProfiles: launcher,
externalProfiles: externalProfiles,
dataRemover: dataRemover,
}.Delete, window.Invalidate)
shell.OnRefreshInstances(instanceStatusRefresher{
managedProfiles: launcher,
externalProfiles: externalProfiles,
@@ -483,6 +489,43 @@ func (s instanceStatusRefresher) refreshOne(ctx context.Context, row ui.Instance
return result
}
type instanceDataDeleter struct {
managedProfiles managedProfileInspector
externalProfiles browser.ExternalProfileInspector
dataRemover files.InstanceDataRemover
}
func (d instanceDataDeleter) Delete(ctx context.Context, row ui.InstanceRow) error {
if err := ctx.Err(); err != nil {
return err
}
if d.managedProfiles == nil || d.externalProfiles == nil || d.dataRemover == nil {
return errors.New("实例数据删除服务尚未准备好")
}
if strings.TrimSpace(row.UserDataDir) == "" || !filepath.IsAbs(row.UserDataDir) {
return files.ErrUnsafeInstanceDataDirectory
}
kind, err := browserKind(row.Browser)
if err != nil {
return err
}
managed, err := d.managedProfiles.InspectProfile(ctx, row.UserDataDir)
if err != nil {
return fmt.Errorf("检查 Chub 实例占用失败:%w", err)
}
if managed.Occupied {
return domain.ErrProfileOccupied
}
external, err := d.externalProfiles.InspectProfile(ctx, kind, row.UserDataDir, row.PID)
if err != nil {
return fmt.Errorf("检查外部浏览器占用失败:%w", err)
}
if external.Occupied {
return domain.ErrProfileOccupied
}
return d.dataRemover.RemoveInstanceData(ctx, row.UserDataDir)
}
func (s instanceStarter) Start(ctx context.Context, row ui.InstanceRow, settings ui.SettingsState, launchGeneration uint64) (ui.InstanceStartOutcome, error) {
kind, err := browserKind(row.Browser)
if err != nil {
+105 -2
View File
@@ -12,6 +12,7 @@ import (
"chub/internal/domain"
"chub/internal/platform/browser"
"chub/internal/platform/config"
"chub/internal/platform/files"
"chub/internal/ui"
)
@@ -276,6 +277,90 @@ func TestInstanceStatusRefresherAssociatesExternalWithoutLifecycleControl(t *tes
}
}
func TestInstanceDataDeleterDeletesOnlyUnoccupiedConfiguredProfile(t *testing.T) {
remover := &fakeInstanceDataRemover{}
deleter := instanceDataDeleter{
managedProfiles: fakeManagedProfileInspector{},
externalProfiles: fakeExternalProfileInspector{},
dataRemover: remover,
}
row := ui.InstanceRow{ID: "chrome-a", Browser: "Chrome", UserDataDir: `C:\profiles\chrome-a`}
if err := deleter.Delete(context.Background(), row); err != nil {
t.Fatalf("Delete() error = %v", err)
}
if remover.calls != 1 || remover.userDataDir != row.UserDataDir {
t.Fatalf("remover calls=%d userDataDir=%q, want 1 %q", remover.calls, remover.userDataDir, row.UserDataDir)
}
}
func TestInstanceDataDeleterRejectsOccupiedProfileBeforeRemoval(t *testing.T) {
tests := []struct {
name string
managed fakeManagedProfileInspector
external fakeExternalProfileInspector
}{
{
name: "chub managed instance",
managed: fakeManagedProfileInspector{use: browser.ProfileUse{Occupied: true, PID: 4242}},
},
{
name: "external browser instance",
external: fakeExternalProfileInspector{use: browser.ProfileUse{Occupied: true, PID: 4243, Source: browser.ProfileSourceLock}},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
remover := &fakeInstanceDataRemover{}
deleter := instanceDataDeleter{
managedProfiles: tt.managed,
externalProfiles: tt.external,
dataRemover: remover,
}
err := deleter.Delete(context.Background(), ui.InstanceRow{ID: "chrome-a", Browser: "Chrome", UserDataDir: `C:\profiles\chrome-a`})
if !errors.Is(err, domain.ErrProfileOccupied) {
t.Fatalf("Delete() error = %v, want profile occupied", err)
}
if remover.calls != 0 {
t.Fatalf("remover calls = %d, want 0", remover.calls)
}
})
}
}
func TestInstanceDataDeleterPreservesDataWhenSafetyCheckFails(t *testing.T) {
remover := &fakeInstanceDataRemover{}
deleter := instanceDataDeleter{
managedProfiles: fakeManagedProfileInspector{err: errors.New("registry unavailable")},
externalProfiles: fakeExternalProfileInspector{},
dataRemover: remover,
}
err := deleter.Delete(context.Background(), ui.InstanceRow{ID: "chrome-a", Browser: "Chrome", UserDataDir: `C:\profiles\chrome-a`})
if err == nil || remover.calls != 0 {
t.Fatalf("Delete() error = %v, remover calls = %d; want safety failure before removal", err, remover.calls)
}
}
func TestInstanceDataDeleterRejectsUnsafeDirectoryBeforeInspection(t *testing.T) {
remover := &fakeInstanceDataRemover{}
deleter := instanceDataDeleter{
managedProfiles: fakeManagedProfileInspector{},
externalProfiles: fakeExternalProfileInspector{},
dataRemover: remover,
}
err := deleter.Delete(context.Background(), ui.InstanceRow{ID: "chrome-a", Browser: "Chrome", UserDataDir: `profiles\chrome-a`})
if !errors.Is(err, files.ErrUnsafeInstanceDataDirectory) {
t.Fatalf("Delete() error = %v, want unsafe directory", err)
}
if remover.calls != 0 {
t.Fatalf("remover calls = %d, want 0", remover.calls)
}
}
func TestInstanceTabsInspectorReadsOnlyVerifiedRuntimeRows(t *testing.T) {
remote := &fakeRemoteDebugInspector{targets: []browser.RemoteDebugTarget{{ID: "page-1", Type: "page", Title: "运营后台", URL: "https://example.com/orders"}}}
inspector := instanceTabsInspector{remoteDebug: remote}
@@ -373,10 +458,28 @@ func (m *fakeManagedExitObserver) ExpectStop(id string, generation uint64, pid i
func (m *fakeManagedExitObserver) ClearExpectedStop(string, uint64, int) { m.clearCalls++ }
type fakeManagedProfileInspector struct{ use browser.ProfileUse }
type fakeManagedProfileInspector struct {
use browser.ProfileUse
err error
}
func (i fakeManagedProfileInspector) InspectProfile(context.Context, string) (browser.ProfileUse, error) {
return i.use, nil
return i.use, i.err
}
type fakeInstanceDataRemover struct {
calls int
userDataDir string
err error
}
func (r *fakeInstanceDataRemover) RemoveInstanceData(ctx context.Context, userDataDir string) error {
if err := ctx.Err(); err != nil {
return err
}
r.calls++
r.userDataDir = userDataDir
return r.err
}
type fakeManagedStopper struct {