155 lines
4.4 KiB
Go
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...)
|
|
}
|