From c7692f830c556e1c3a5bbec594ec83acc57bf903 Mon Sep 17 00:00:00 2001 From: purian23 Date: Fri, 23 Oct 2026 01:42:07 -0400 Subject: [PATCH] fix(core): prevent clipboard & wellbeing data loss from concurrent sessions Port 2.0 --- core/internal/server/clipboard/manager.go | 17 +++++++++------ .../internal/server/clipboard/manager_test.go | 18 ++++++++++++++++ .../internal/server/wellbeing/manager_test.go | 21 +++++++++++++++++++ core/internal/server/wellbeing/store.go | 5 +++++ 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/core/internal/server/clipboard/manager.go b/core/internal/server/clipboard/manager.go index 9995cdfa5..17b61f4a3 100644 --- a/core/internal/server/clipboard/manager.go +++ b/core/internal/server/clipboard/manager.go @@ -29,6 +29,7 @@ import ( _ "golang.org/x/image/webp" bolt "go.etcd.io/bbolt" + berrors "go.etcd.io/bbolt/errors" wlclient "github.com/duxlabs-dev/dux-go/wayland/client" "github.com/duxlabs-dev/dux-go/wayland/ext_data_control" @@ -55,8 +56,14 @@ func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error) configPath, _ := getConfigPath() + db, err := openDB(dbPath) + if err != nil { + return nil, fmt.Errorf("failed to open db: %w", err) + } + m := &Manager{ config: config, + db: db, configPath: configPath, display: display, wlCtx: wlCtx, @@ -69,6 +76,7 @@ func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error) if !config.Disabled { if err := m.setupRegistry(); err != nil { + db.Close() return nil, err } } @@ -78,12 +86,6 @@ func NewManager(wlCtx wlcontext.WaylandContext, config Config) (*Manager, error) go m.watchConfig() - db, err := openDB(dbPath) - if err != nil { - return nil, fmt.Errorf("failed to open db: %w", err) - } - m.db = db - if err := m.migrateHashes(); err != nil { log.Errorf("Failed to migrate hashes: %v", err) } @@ -117,6 +119,9 @@ func openDB(path string) (*bolt.DB, error) { if err == nil { return db, nil } + if errors.Is(err, berrors.ErrTimeout) { + return nil, err + } log.Errorf("Clipboard db corrupted, salvaging readable entries: %v", err) diff --git a/core/internal/server/clipboard/manager_test.go b/core/internal/server/clipboard/manager_test.go index 8c21518c2..9821c961b 100644 --- a/core/internal/server/clipboard/manager_test.go +++ b/core/internal/server/clipboard/manager_test.go @@ -887,6 +887,24 @@ func TestOpenDB_HealsGarbageFile(t *testing.T) { require.NoError(t, err) } +func TestOpenDB_LockedByAnotherProcessKeepsHistory(t *testing.T) { + path := filepath.Join(t.TempDir(), "db") + holder, err := openDB(path) + require.NoError(t, err) + m := &Manager{config: DefaultConfig(), db: holder, dbPath: path} + require.NoError(t, m.storeEntry(Entry{Data: []byte("x"), MimeType: "text/plain"})) + + _, err = openDB(path) + require.Error(t, err) + require.NoError(t, holder.Close()) + + reopened, err := openDB(path) + require.NoError(t, err) + defer reopened.Close() + m = &Manager{config: DefaultConfig(), db: reopened, dbPath: path} + assert.Len(t, m.GetHistory(), 1) +} + func TestStoreEntry_CorruptDBReturnsErrorNotPanic(t *testing.T) { for _, tt := range corruptDBCases { t.Run(tt.name, func(t *testing.T) { diff --git a/core/internal/server/wellbeing/manager_test.go b/core/internal/server/wellbeing/manager_test.go index b55d13b50..fd8872008 100644 --- a/core/internal/server/wellbeing/manager_test.go +++ b/core/internal/server/wellbeing/manager_test.go @@ -194,3 +194,24 @@ func TestAlertRecordSurvivesRestart(t *testing.T) { assert.Nil(t, s.Limit, "the alert already fired before the restart") } } + +func TestOpenDBLockedByAnotherProcessKeepsData(t *testing.T) { + path := filepath.Join(t.TempDir(), "db") + c := &clock{now: time.Date(2026, 10, 1, 12, 0, 0, 0, time.UTC)} + now := func() time.Time { return c.now } + + m, err := newManager(path, now) + require.NoError(t, err) + m.SetState("firefox", true, 1) + c.advance(10 * time.Second) + m.Flush() + + _, err = newManager(path, now) + require.Error(t, err) + m.Close() + + m, err = newManager(path, now) + require.NoError(t, err) + defer m.Close() + assert.Equal(t, int64(10), m.Summary(1)[0].Active) +} diff --git a/core/internal/server/wellbeing/store.go b/core/internal/server/wellbeing/store.go index 37558dac1..5d047c2b3 100644 --- a/core/internal/server/wellbeing/store.go +++ b/core/internal/server/wellbeing/store.go @@ -2,6 +2,7 @@ package wellbeing import ( "encoding/json" + "errors" "fmt" "os" "path/filepath" @@ -11,6 +12,7 @@ import ( "github.com/duxlabs-dev/dux-shell/core/internal/log" "github.com/duxlabs-dev/dux-shell/core/internal/utils" bolt "go.etcd.io/bbolt" + berrors "go.etcd.io/bbolt/errors" ) var ( @@ -31,6 +33,9 @@ func openDB(path string) (*bolt.DB, error) { if err == nil { return db, nil } + if errors.Is(err, berrors.ErrTimeout) { + return nil, err + } log.Errorf("Wellbeing db unreadable, starting over: %v", err) corruptPath := path + ".corrupt" os.Remove(corruptPath)