Files

155 lines
4.4 KiB
Go

// Cthulhu screen reader.
// Copyright (c) 2026 Storm Dragon
// Authors: Storm Dragon, Angus Kola
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along with
// this program. If not, see <https://www.gnu.org/licenses/gpl-3.0.html>.
package lifecycle
import (
"context"
"errors"
"fmt"
"log/slog"
)
type State string
const (
StateNew State = "New"
StateStarting State = "Starting"
StateRunning State = "Running"
StateStopping State = "Stopping"
StateStopped State = "Stopped"
StateFailed State = "Failed"
)
type Service struct {
Name string
Start func(context.Context) error
Stop func(context.Context) error
}
type Runner struct {
state State
services []Service
started []int
logger *slog.Logger
}
func New(services ...Service) (*Runner, error) {
return NewWithLogger(nil, services...)
}
func NewWithLogger(logger *slog.Logger, services ...Service) (*Runner, error) {
names := make(map[string]struct{}, len(services))
for index, service := range services {
switch {
case service.Name == "":
return nil, fmt.Errorf("service %d has no name", index)
case service.Start == nil:
return nil, fmt.Errorf("service %q has no start function", service.Name)
case service.Stop == nil:
return nil, fmt.Errorf("service %q has no stop function", service.Name)
}
if _, exists := names[service.Name]; exists {
return nil, fmt.Errorf("duplicate service name %q", service.Name)
}
names[service.Name] = struct{}{}
}
return &Runner{
state: StateNew,
services: append([]Service(nil), services...),
logger: logger,
}, nil
}
func (runner *Runner) State() State {
return runner.state
}
func (runner *Runner) Start(ctx context.Context) error {
if ctx == nil {
return errors.New("lifecycle start requires a non-nil context")
}
switch runner.state {
case StateNew:
case StateRunning, StateStarting, StateStopping, StateStopped, StateFailed:
return fmt.Errorf("cannot start from state %s", runner.state)
}
runner.state = StateStarting
for index, service := range runner.services {
if err := ctx.Err(); err != nil {
return runner.failStartup(fmt.Errorf("start %q canceled: %w", service.Name, err))
}
if err := service.Start(ctx); err != nil {
return runner.failStartup(fmt.Errorf("start %q: %w", service.Name, err))
}
runner.started = append(runner.started, index)
}
runner.state = StateRunning
return nil
}
func (runner *Runner) Stop(ctx context.Context) error {
if ctx == nil {
return errors.New("lifecycle stop requires a non-nil context")
}
switch runner.state {
case StateNew:
runner.state = StateStopped
return nil
case StateRunning:
case StateStopped:
return nil
case StateStarting, StateStopping, StateFailed:
return fmt.Errorf("cannot stop from state %s", runner.state)
}
runner.state = StateStopping
stopError := runner.stopStartedServices(ctx)
runner.state = StateStopped
return stopError
}
func (runner *Runner) failStartup(startError error) error {
runner.state = StateFailed
runner.state = StateStopping
stopError := runner.stopStartedServices(context.Background())
runner.state = StateStopped
return errors.Join(startError, stopError)
}
func (runner *Runner) stopStartedServices(ctx context.Context) error {
var stopErrors []error
for index := len(runner.started) - 1; index >= 0; index-- {
serviceIndex := runner.started[index]
service := runner.services[serviceIndex]
if runner.logger != nil {
runner.logger.Debug("lifecycle service stopping", "service", service.Name)
}
if err := service.Stop(ctx); err != nil {
stopErrors = append(stopErrors, fmt.Errorf("stop %q: %w", service.Name, err))
if runner.logger != nil {
runner.logger.Warn("lifecycle service stop failed", "service", service.Name, "error", err)
}
} else if runner.logger != nil {
runner.logger.Debug("lifecycle service stopped", "service", service.Name)
}
}
runner.started = nil
return errors.Join(stopErrors...)
}