Skip to content

Commit 5954eb3

Browse files
authored
Merge pull request #171 from richbl/dev
refactor(app): 🧑‍💻 Added goroutine wrappers (r/w mutexes) …
2 parents 4f50979 + 4720610 commit 5954eb3

12 files changed

Lines changed: 467 additions & 250 deletions

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ require (
77
github.com/diamondburned/gotk4-adwaita/pkg v0.0.0-20250703085337-e94555b846b6
88
github.com/diamondburned/gotk4/pkg v0.3.2-0.20250703063411-16654385f59a
99
github.com/gen2brain/go-mpv v0.2.3
10+
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728
1011
github.com/stretchr/testify v1.11.1
1112
tinygo.org/x/bluetooth v0.13.0
1213
)
1314

1415
require (
1516
github.com/KarpelesLab/weak v0.1.1 // indirect
16-
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20250301202403-da16c1255728 // indirect
1717
go4.org/unsafe/assume-no-moving-gc v0.0.0-20231121144256-b99613f794b6 // indirect
1818
golang.org/x/sync v0.17.0 // indirect
1919
)

internal/ble/sensor_controller.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"fmt"
77
"os"
88
"sync"
9+
"sync/atomic"
910
"time"
1011

1112
"tinygo.org/x/bluetooth"
@@ -34,6 +35,7 @@ type blePeripheralDetails struct {
3435
type Controller struct {
3536
blePeripheralDetails blePeripheralDetails
3637
speedConfig config.SpeedConfig
38+
InstanceID int64
3739
}
3840

3941
// actionParams encapsulates parameters for BLE actions
@@ -46,6 +48,9 @@ type actionParams[T any] struct {
4648
// Mutex for synchronizing adapter access
4749
var AdapterMu sync.Mutex
4850

51+
// Instance counter to distinguish between controller object instances
52+
var bleInstanceCounter atomic.Int64
53+
4954
// Error definitions
5055
var (
5156
// General BLE errors
@@ -80,20 +85,26 @@ func NewBLEController(bleConfig config.BLEConfig, speedConfig config.SpeedConfig
8085
AdapterMu.Lock()
8186
defer AdapterMu.Unlock()
8287

88+
// Increment instance counter
89+
instanceID := bleInstanceCounter.Add(1)
90+
91+
logger.Debug(logger.BackgroundCtx, logger.BLE, fmt.Sprintf("creating BLE controller object (id:%04d)...", instanceID))
92+
8393
bleAdapter := bluetooth.DefaultAdapter
8494

8595
if err := bleAdapter.Enable(); err != nil {
86-
return nil, fmt.Errorf(errFormat, "failed to enable BLE central controller", err)
96+
return nil, fmt.Errorf(errFormat, "failed to enable BLE controller", err)
8797
}
8898

89-
logger.Info(logger.BackgroundCtx, logger.BLE, "created new BLE central controller")
99+
logger.Info(logger.BackgroundCtx, logger.BLE, fmt.Sprintf("created BLE controller object (id:%04d)", instanceID))
90100

91101
return &Controller{
92102
blePeripheralDetails: blePeripheralDetails{
93103
bleConfig: bleConfig,
94104
bleAdapter: *bleAdapter,
95105
},
96106
speedConfig: speedConfig,
107+
InstanceID: instanceID,
97108
}, nil
98109
}
99110

internal/services/shutdown_manager.go

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"os"
88
"os/signal"
99
"sync"
10+
"sync/atomic"
1011
"syscall"
1112
"time"
1213

@@ -21,28 +22,39 @@ type smContext struct {
2122
cancel context.CancelFunc
2223
}
2324

24-
// ShutdownManager represents a shutdown manager that manages a component lifecycle
25+
// ShutdownManager manages an application lifecycle
2526
type ShutdownManager struct {
26-
context smContext
27-
errChan chan error
28-
cleanup []func()
29-
wg sync.WaitGroup
30-
timeout time.Duration
27+
context smContext
28+
errChan chan error
29+
cleanup []func()
30+
wg sync.WaitGroup
31+
timeout time.Duration
32+
InstanceID int64
3133
}
3234

35+
// Instance counter to distinguish between shutdown manager objects
36+
var shutdownInstanceCounter atomic.Int64
37+
3338
// NewShutdownManager creates a new shutdown manager
3439
func NewShutdownManager(timeout time.Duration) *ShutdownManager {
3540

41+
instanceID := shutdownInstanceCounter.Add(1)
42+
43+
logger.Debug(logger.BackgroundCtx, logger.APP, fmt.Sprintf("creating shutdown manager object (id:%04d)...", instanceID))
44+
3645
// Create a context with a timeout
3746
ctx, cancel := context.WithCancel(logger.BackgroundCtx)
3847

48+
logger.Debug(logger.BackgroundCtx, logger.APP, fmt.Sprintf("created shutdown manager object (id:%04d)", instanceID))
49+
3950
return &ShutdownManager{
4051
context: smContext{
4152
ctx: ctx,
4253
cancel: cancel,
4354
},
44-
timeout: timeout,
45-
errChan: make(chan error, 1),
55+
timeout: timeout,
56+
InstanceID: instanceID,
57+
errChan: make(chan error, 1),
4658
}
4759
}
4860

@@ -88,6 +100,8 @@ func (sm *ShutdownManager) Start() {
88100
// Shutdown shuts down the shutdown manager
89101
func (sm *ShutdownManager) Shutdown() {
90102

103+
logger.Debug(logger.BackgroundCtx, logger.APP, fmt.Sprintf("shutting down shutdown manager object (id:%04d)...", sm.InstanceID))
104+
91105
sm.context.cancel()
92106
done := make(chan struct{})
93107

@@ -98,15 +112,20 @@ func (sm *ShutdownManager) Shutdown() {
98112

99113
select {
100114
case <-done:
115+
logger.Debug(logger.BackgroundCtx, logger.APP, fmt.Sprintf("shutdown manager (id:%04d) services stopped", sm.InstanceID))
116+
101117
case <-time.After(sm.timeout):
102-
logger.Warn(sm.context.ctx, logger.APP, "shutdown timed out")
118+
logger.Debug(logger.BackgroundCtx, logger.APP, fmt.Sprintf("shutdown manager (id:%04d) shutdown timed out", sm.InstanceID))
119+
103120
}
104121

105122
// Execute cleanup functions in reverse order
106123
for i := len(sm.cleanup) - 1; i >= 0; i-- {
107124
sm.cleanup[i]()
108125
}
109126

127+
logger.Debug(logger.BackgroundCtx, logger.APP, fmt.Sprintf("shutdown manager object (id:%04d) shutdown complete", sm.InstanceID))
128+
110129
}
111130

112131
// Context returns the shutdown manager's context

internal/services/shutdown_manager_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"testing"
77
"time"
88

9+
"github.com/richbl/go-ble-sync-cycle/internal/logger"
910
sm "github.com/richbl/go-ble-sync-cycle/internal/services"
1011
)
1112

@@ -17,6 +18,8 @@ var (
1718
// TestNewShutdownManager tests the creation of a new shutdown manager
1819
func TestNewShutdownManager(t *testing.T) {
1920

21+
logger.Initialize("debug")
22+
2023
timeout := 5 * time.Second
2124
manager := sm.NewShutdownManager(timeout)
2225

internal/session/controllers.go

Lines changed: 94 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -94,8 +94,13 @@ func (m *StateManager) StartSession() error {
9494
func (m *StateManager) StopSession() error {
9595

9696
m.mu.Lock()
97+
9798
shutdownMgr := m.shutdownMgr
9899
wasPending := m.PendingStart
100+
101+
// Log what we're destroying
102+
m.logControllersRelease(shutdownMgr)
103+
99104
m.state = StateLoaded
100105
m.PendingStart = false
101106
m.mu.Unlock()
@@ -104,50 +109,90 @@ func (m *StateManager) StopSession() error {
104109
return errNoActiveSession
105110
}
106111

112+
ctx := logger.BackgroundCtx
113+
if shutdownMgr != nil {
114+
ctx = *shutdownMgr.Context()
115+
}
116+
107117
if wasPending {
108-
logger.Info(*shutdownMgr.Context(), logger.BLE, "stop requested, canceling pending session setup...")
118+
logger.Info(ctx, logger.APP, "stop requested, canceling pending session setup...")
109119
} else {
110-
logger.Info(*shutdownMgr.Context(), logger.BLE, "stop requested, canceling active session...")
120+
logger.Info(ctx, logger.APP, "stop requested, canceling active session...")
111121
}
112122

113-
// Trigger shutdown (cancels ctx, waits wg—like Ctrl+C)
114123
fmt.Fprint(os.Stdout, "\r") // Clear the ^C character from the terminal line
115124

125+
// Stop the shutdown manager
116126
if shutdownMgr != nil {
117127
shutdownMgr.Shutdown()
118128
}
119129

120-
// Clear resources under lock
130+
m.clearResources()
131+
m.stopBLEScan(ctx)
132+
133+
if wasPending {
134+
logger.Info(ctx, logger.APP, "stopped pending session startup")
135+
} else {
136+
logger.Info(ctx, logger.APP, "session stopped")
137+
}
138+
139+
return nil
140+
}
141+
142+
// logControllersRelease logs the release of controller objects
143+
func (m *StateManager) logControllersRelease(shutdownMgr *services.ShutdownManager) {
144+
145+
if m.controllers == nil || shutdownMgr == nil {
146+
return
147+
}
148+
149+
ctx := *shutdownMgr.Context()
150+
151+
if m.controllers.bleController != nil {
152+
logger.Info(ctx, logger.BLE, fmt.Sprintf("releasing BLE controller object (id:%04d)", m.controllers.bleController.InstanceID))
153+
}
154+
if m.controllers.speedController != nil {
155+
logger.Info(ctx, logger.SPEED, fmt.Sprintf("releasing speed controller object (id:%04d)", m.controllers.speedController.InstanceID))
156+
}
157+
if m.controllers.videoPlayer != nil {
158+
logger.Info(ctx, logger.VIDEO, fmt.Sprintf("releasing video controller object (id:%04d)", m.controllers.videoPlayer.InstanceID))
159+
}
160+
161+
}
162+
163+
// clearResources clears the session resources
164+
func (m *StateManager) clearResources() {
165+
121166
m.mu.Lock()
167+
defer m.mu.Unlock()
168+
122169
m.controllers = nil
123170
m.shutdownMgr = nil
124171
m.activeConfig = nil
125-
m.mu.Unlock()
172+
173+
logger.Debug(logger.BackgroundCtx, logger.APP, "controllers and shutdown manager objects released")
174+
175+
}
176+
177+
// stopBLEScan stops the BLE scan
178+
func (m *StateManager) stopBLEScan(ctx context.Context) {
126179

127180
// Stop any ongoing scan under mutex
128181
ble.AdapterMu.Lock()
129182
defer ble.AdapterMu.Unlock()
130183

131184
if err := bluetooth.DefaultAdapter.StopScan(); err != nil {
132-
logger.Warn(*shutdownMgr.Context(), logger.BLE, fmt.Sprintf("failed to stop current BLE scan: %v", err))
133-
} else {
134-
logger.Info(*shutdownMgr.Context(), logger.BLE, "BLE scan stopped")
135-
}
136-
137-
if wasPending {
138-
logger.Info(*shutdownMgr.Context(), logger.APP, "stopped pending session startup")
185+
logger.Warn(ctx, logger.BLE, fmt.Sprintf("failed to stop current BLE scan: %v", err))
139186
} else {
140-
logger.Info(*shutdownMgr.Context(), logger.APP, "session stopped")
187+
logger.Info(ctx, logger.BLE, "BLE scan stopped")
141188
}
142189

143-
return nil
144190
}
145191

146192
// BatteryLevel returns the current battery level from the BLE controller
147193
func (m *StateManager) BatteryLevel() byte {
148194

149-
m.mu.RLock()
150-
defer m.mu.RUnlock()
195+
defer m.readLock()()
151196

152197
if m.controllers != nil && m.controllers.bleController != nil {
153198
return m.controllers.bleController.BatteryLevelLast()
@@ -159,8 +204,7 @@ func (m *StateManager) BatteryLevel() byte {
159204
// CurrentSpeed returns the current smoothed speed from the speed controller
160205
func (m *StateManager) CurrentSpeed() (float64, string) {
161206

162-
m.mu.RLock()
163-
defer m.mu.RUnlock()
207+
defer m.readLock()()
164208

165209
// Use ActiveConfig here to ensure we return the units of the active running session
166210
cfg := m.activeConfig
@@ -179,8 +223,7 @@ func (m *StateManager) CurrentSpeed() (float64, string) {
179223
// VideoTimeRemaining returns the formatted time remaining string (HH:MM:SS)
180224
func (m *StateManager) VideoTimeRemaining() string {
181225

182-
m.mu.RLock()
183-
defer m.mu.RUnlock()
226+
defer m.readLock()()
184227

185228
noTime := "--:--:--"
186229

@@ -199,8 +242,7 @@ func (m *StateManager) VideoTimeRemaining() string {
199242
// VideoPlaybackRate returns the current video playback multiplier (e.g. 1.0x)
200243
func (m *StateManager) VideoPlaybackRate() float64 {
201244

202-
m.mu.RLock()
203-
defer m.mu.RUnlock()
245+
defer m.readLock()()
204246

205247
if m.controllers == nil || m.controllers.videoPlayer == nil {
206248
return 0.0
@@ -216,28 +258,32 @@ func (m *StateManager) initializeControllers() (*controllers, error) {
216258
cfg := m.activeConfig
217259
m.mu.RUnlock()
218260

261+
logger.Debug(logger.BackgroundCtx, logger.APP, "creating and initializing controllers...")
262+
219263
if cfg == nil {
220264
return nil, errNoActiveConfig
221265
}
222266

223-
logger.Debug(logger.BackgroundCtx, logger.APP, "creating speed controller...")
267+
logger.Debug(logger.BackgroundCtx, logger.APP, "creating new speed controller...")
224268

225269
speedController := speed.NewSpeedController(cfg.Speed.SmoothingWindow)
226270

227-
logger.Debug(logger.BackgroundCtx, logger.APP, "creating video controller...")
271+
logger.Debug(logger.BackgroundCtx, logger.APP, "creating new video controller...")
228272

229273
videoPlayer, err := video.NewPlaybackController(cfg.Video, cfg.Speed)
230274
if err != nil {
231275
return nil, fmt.Errorf("failed to create video controller: %w", err)
232276
}
233277

234-
logger.Debug(logger.BackgroundCtx, logger.APP, "creating BLE controller...")
278+
logger.Debug(logger.BackgroundCtx, logger.APP, "creating new BLE controller...")
235279

236280
bleController, err := ble.NewBLEController(cfg.BLE, cfg.Speed)
237281
if err != nil {
238282
return nil, fmt.Errorf("failed to create BLE controller: %w", err)
239283
}
240284

285+
logger.Debug(logger.BackgroundCtx, logger.APP, "all controllers created and initialized")
286+
241287
return &controllers{
242288
speedController: speedController,
243289
videoPlayer: videoPlayer,
@@ -310,6 +356,29 @@ func (m *StateManager) startServices(ctrl *controllers, shutdownMgr *services.Sh
310356

311357
}
312358

359+
// cleanupStartFailure handles cleaning manager state when session startup fails
360+
func (m *StateManager) cleanupStartFailure(shutdownMgr *services.ShutdownManager) {
361+
362+
logger.Debug(logger.BackgroundCtx, logger.APP, "resetting controllers and session state...")
363+
364+
m.mu.Lock()
365+
m.PendingStart = false
366+
m.state = StateLoaded
367+
m.controllers = nil
368+
m.shutdownMgr = nil
369+
m.activeConfig = nil
370+
371+
m.mu.Unlock()
372+
373+
// ensure the shutdown manager... uh... shuts down
374+
if shutdownMgr != nil {
375+
shutdownMgr.Shutdown()
376+
}
377+
378+
logger.Debug(logger.BackgroundCtx, logger.APP, "controllers and session state reset complete")
379+
380+
}
381+
313382
// runService helper to launch a service with standard error handling and logging
314383
func (m *StateManager) runService(shutdownMgr *services.ShutdownManager, service string, action func(context.Context) error) {
315384

0 commit comments

Comments
 (0)