Skip to content

Commit

Permalink
Merge pull request #47 from richbl/dev
Browse files Browse the repository at this point in the history
Controllers refactor to improve maintainability
  • Loading branch information
richbl authored Dec 27, 2024
2 parents 22013f3 + bbd7304 commit e943938
Show file tree
Hide file tree
Showing 7 changed files with 442 additions and 370 deletions.
145 changes: 96 additions & 49 deletions cmd/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import (
logger "github.com/richbl/go-ble-sync-cycle/internal/logging"
speed "github.com/richbl/go-ble-sync-cycle/internal/speed"
video "github.com/richbl/go-ble-sync-cycle/internal/video-player"

"tinygo.org/x/bluetooth"
)

// Application constants
Expand All @@ -38,44 +36,46 @@ func main() {
// Hello world!
log.Println(appPrefix, "Starting", appName, appVersion)

// Load configuration
cfg := loadConfig("config.toml")

// Initialize the shutdown manager and exit handler
sm := NewShutdownManager(shutdownTimeout)
exitHandler := NewExitHandler(sm)
sm.Start()
// Initialize utility services
sm, exitHandler := initializeUtilityServices(cfg)

// Initialize the logger with the configured log level and exit handler
logger.Initialize(cfg.App.LogLevel)
logger.SetExitHandler(func() {
sm.initiateShutdown()
exitHandler.HandleExit()
})
// Initialize application controllers
controllers := initializeControllers(cfg, exitHandler)

// Initialize the application controllers
controllers, componentType, err := setupAppControllers(*cfg)
if err != nil {
logger.Fatal(componentType, "failed to create controllers:", err.Error())
return
}
// Scan for BLE device
bleDeviceDiscovery(sm.shutdownCtx.ctx, controllers, exitHandler)

// Scan for the BLE characteristic and handle context cancellation
bleChar, err := scanForBLECharacteristic(sm.Context(), controllers)
if err != nil {
// Start and monitor services for BLE and video components
monitorServiceRunners(startServiceRunners(sm, controllers))

if err != context.Canceled {
logger.Fatal(logger.BLE, "failed to scan for BLE characteristic:", err.Error())
// Wait for final shutdown sequences to complete and wave goodbye!
sm.Wait()
waveGoodbye()
}

// monitorServiceRunners monitors the services and logs any errors encountered
func monitorServiceRunners(runners []*ServiceRunner) {

for _, runner := range runners {

if err := runner.Error(); err != nil {
logger.Fatal(logger.APP, "service error:", err.Error())
return
}

exitHandler.HandleExit()
return
}
}

// startServiceRunners starts the BLE and video service runners and returns a slice of service runners
func startServiceRunners(sm *ShutdownManager, controllers appControllers) []*ServiceRunner {

// Create and run the BLE service runner
bleRunner := NewServiceRunner(sm, "BLE")
bleRunner.Run(func(ctx context.Context) error {
return controllers.bleController.GetBLEUpdates(ctx, controllers.speedController, bleChar)
return controllers.bleController.GetBLEUpdates(ctx, controllers.speedController)
})

// Create and run the video service runner
Expand All @@ -84,17 +84,57 @@ func main() {
return controllers.videoPlayer.Start(ctx, controllers.speedController)
})

// Wait for services to complete and check for errors
for _, runner := range []*ServiceRunner{bleRunner, videoRunner} {
if err := runner.Error(); err != nil {
logger.Fatal(logger.APP, "service error:", err.Error())
return []*ServiceRunner{bleRunner, videoRunner}
}

// bleDeviceDiscovery scans for the BLE device and CSC speed characteristic
func bleDeviceDiscovery(ctx context.Context, controllers appControllers, exitHandler *ExitHandler) {

err := scanForBLECharacteristic(ctx, controllers)
if err != nil {

if err != context.Canceled {
logger.Fatal(logger.BLE, "failed to scan for BLE characteristic:", err.Error())
return
}

exitHandler.HandleExit()
}
}

// Wait for final shutdown sequences to complete and wave goodbye!
sm.Wait()
waveGoodbye()
// initializeUtilityServices initializes the core components of the application, including the shutdown manager,
// exit handler, and logger
func initializeUtilityServices(cfg *config.Config) (*ShutdownManager, *ExitHandler) {

// Initialize the shutdown manager and exit handler
sm := NewShutdownManager(shutdownTimeout)
exitHandler := NewExitHandler(sm)
sm.Start()

// Initialize the logger
logger.Initialize(cfg.App.LogLevel)

// Set the exit handler for the shutdown manager
logger.SetExitHandler(func() {
sm.initiateShutdown()
exitHandler.HandleExit()
})

return sm, exitHandler
}

// initializeControllers initializes the application controllers, including the speed controller,
// video player, and BLE controller. It returns the initialized controllers
func initializeControllers(cfg *config.Config, exitHandler *ExitHandler) appControllers {

controllers, componentType, err := setupAppControllers(*cfg)

if err != nil {
logger.Fatal(componentType, "failed to create controllers:", err.Error())
exitHandler.HandleExit()
}

return controllers
}

// setupAppControllers creates and initializes all application controllers
Expand All @@ -119,31 +159,38 @@ func setupAppControllers(cfg config.Config) (appControllers, logger.ComponentTyp
}

// scanForBLECharacteristic handles the initial BLE device discovery and characteristic scanning
// using a context for cancellation and returns the discovered characteristic or an error
func scanForBLECharacteristic(ctx context.Context, controllers appControllers) (*bluetooth.DeviceCharacteristic, error) {
func scanForBLECharacteristic(ctx context.Context, controllers appControllers) error {

// Create a channel to receive the result of the BLE characteristic scan
resultsChan := make(chan struct {
char *bluetooth.DeviceCharacteristic
err error
}, 1)
// Create a channel to receive errors from the scan goroutine
errChan := make(chan error, 1)

// BLE peripheral scan and connect
go func() {
defer close(resultsChan)
char, err := controllers.bleController.GetBLECharacteristic(ctx, controllers.speedController)
resultsChan <- struct {
char *bluetooth.DeviceCharacteristic
err error
}{char, err}
defer close(errChan)
scanResult, err := controllers.bleController.ScanForBLEPeripheral(ctx)
if err != nil {
errChan <- err
return
}

connectResult, err := controllers.bleController.ConnectToBLEPeripheral(scanResult)
if err != nil {
errChan <- err
return
}

// Get the BLE characteristic from the connected device
err = controllers.bleController.GetBLECharacteristic(connectResult)
errChan <- err
}()

select {
case <-ctx.Done():
fmt.Print("\r") // Clear the ^C character from the terminal line
logger.Info(logger.BLE, "user-generated interrupt, stopping BLE discovery...")
return nil, ctx.Err()
case result := <-resultsChan:
return result.char, result.err
return ctx.Err()
case result := <-errChan:
return result
}
}

Expand Down
129 changes: 9 additions & 120 deletions cmd/service_runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,6 @@ package main

import (
"context"
"os"
"os/signal"
"sync"
"syscall"
"time"

logger "github.com/richbl/go-ble-sync-cycle/internal/logging"
)

// ServiceRunner manages individual service goroutines and their lifecycle
Expand All @@ -18,101 +11,6 @@ type ServiceRunner struct {
errChan chan error
}

// ShutdownManager handles graceful shutdown of application components and coordinates cleanup
// operations with context cancellations and timeout management
type ShutdownManager struct {
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
cleanupFuncs []func()
timeout time.Duration
terminated chan struct{}
cleanupOnce sync.Once
}

// ExitHandler coordinates the final application shutdown sequence
type ExitHandler struct {
sm *ShutdownManager
}

// NewShutdownManager creates a new ShutdownManager with the specified timeout duration
func NewShutdownManager(timeout time.Duration) *ShutdownManager {

ctx, cancel := context.WithCancel(context.Background())

return &ShutdownManager{
ctx: ctx,
cancel: cancel,
wg: sync.WaitGroup{},
terminated: make(chan struct{}),
timeout: timeout,
}
}

// Context returns the ShutdownManager's context for cancellation propagation
func (sm *ShutdownManager) Context() context.Context {
return sm.ctx
}

// Wait blocks until the shutdown sequence is complete
func (sm *ShutdownManager) Wait() {
<-sm.terminated
}

// WaitGroup returns the ShutdownManager's WaitGroup for goroutine synchronization
func (sm *ShutdownManager) WaitGroup() *sync.WaitGroup {
return &sm.wg
}

// AddCleanupFn adds a cleanup function to be executed during shutdown
// Note that cleanup functions are executed in reverse order of registration
func (sm *ShutdownManager) AddCleanupFn(fn func()) {
sm.cleanupFuncs = append(sm.cleanupFuncs, fn)
}

// Start begins listening for shutdown signals (SIGINT, SIGTERM)
// When a signal is received, it initiates the shutdown sequence
func (sm *ShutdownManager) Start() {

sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)

go func() {
<-sigChan
sm.initiateShutdown()
}()
}

// initiateShutdown coordinates the shutdown sequence, including timeout management and cleanup
// function execution, and ensures the shutdown sequence runs only once
func (sm *ShutdownManager) initiateShutdown() {

sm.cleanupOnce.Do(func() {
sm.cancel()
timeoutCtx, timeoutCancel := context.WithTimeout(context.Background(), sm.timeout)
defer timeoutCancel()
done := make(chan struct{})

go func() {
sm.wg.Wait()
close(done)
}()

select {
case <-done:
case <-timeoutCtx.Done():
logger.Warn(logger.APP, "shutdown timed out, some goroutines may not have cleaned up properly")
}

// Execute cleanup functions (reverse order)
for i := len(sm.cleanupFuncs) - 1; i >= 0; i-- {
sm.cleanupFuncs[i]()
}

close(sm.terminated)
})
}

// NewServiceRunner creates a new ServiceRunner with the specified name and ShutdownManager
func NewServiceRunner(sm *ShutdownManager, name string) *ServiceRunner {

Expand All @@ -127,14 +25,19 @@ func NewServiceRunner(sm *ShutdownManager, name string) *ServiceRunner {
// handling cleanup and error propagation
func (sr *ServiceRunner) Run(fn func(context.Context) error) {

sr.sm.wg.Add(1)
sr.sm.WaitGroup().Add(1)

go func() {
defer sr.sm.wg.Done()
defer sr.sm.WaitGroup().Done()

if err := fn(sr.sm.ctx); err != nil && err != context.Canceled {
if err := fn(sr.sm.shutdownCtx.ctx); err != nil && err != context.Canceled {
sr.errChan <- err
sr.sm.cancel()

// Initiate shutdown on error
if sm := sr.sm; sm != nil {
sm.shutdownCtx.cancel()
}

}

close(sr.errChan)
Expand All @@ -150,19 +53,5 @@ func (sr *ServiceRunner) Error() error {
default:
return nil
}
}

// NewExitHandler creates a new ExitHandler with the specified shutdown manager
func NewExitHandler(sm *ShutdownManager) *ExitHandler {
return &ExitHandler{sm: sm}
}

// HandleExit coordinates the final shutdown sequence and exits the application
func (h *ExitHandler) HandleExit() {

if h.sm != nil {
h.sm.Wait()
}

waveGoodbye()
}
Loading

0 comments on commit e943938

Please sign in to comment.