Files
LazyPM/pkg/task/runner.go

219 lines
4.6 KiB
Go

package task
import (
"context"
"fmt"
"log/slog"
"os"
tea "charm.land/bubbletea/v2"
"github.com/LazyBachelor/LazyPM/internal/models"
"github.com/charmbracelet/x/term"
)
type App = models.App
type Config = models.Config
type Tasker = models.Tasker
type Interface = models.Interface
type InterfaceType = models.InterfaceType
type ValidatedInterface = models.ValidatedInterface
type ValidationFeedback = models.ValidationFeedback
type QuestionnaireKeysProvider interface {
QuestionnaireKeys(InterfaceType) []string
}
var ErrUserQuit = models.ErrUserQuit
type TaskRunner struct {
app *App
logger *slog.Logger
}
func NewTaskRunner(app *App) *TaskRunner {
var logger *slog.Logger
if app != nil {
logger = app.Logger
}
return &TaskRunner{
app: app,
logger: logger,
}
}
func (r *TaskRunner) Run(ctx context.Context, t Tasker, i Interface, iType InterfaceType) (runErr error) {
config := t.Config()
details := t.Details(iType)
lifecycle := NewRunLifecycle(r.app, config, details, iType, r.logger)
defer func() {
runErr = lifecycle.Finish(ctx, runErr)
}()
collector := lifecycle.collector
config = lifecycle.config
collector.log("info", "task run started")
// Setup
if err := t.Setup(ctx); err != nil {
return fmt.Errorf("failed to setup task: %w", err)
}
// Intro screen
if err := runIntro(details); err != nil {
return err
}
// Validation
feedbackChan := make(chan ValidationFeedback, 10)
quitChan := make(chan bool, 1)
submitChan := make(chan models.ValidationTrigger, 1)
if validated, ok := i.(ValidatedInterface); ok {
validated.SetChannels(feedbackChan, quitChan)
validated.SetSubmitChan(submitChan)
}
if r.app != nil {
r.app.SubmitChan = submitChan
}
engine := &ValidationEngine{task: t}
doneChan, stopChan := engine.Start(ctx, submitChan, func(feedback ValidationFeedback, source models.ValidationTriggerSource) {
collector.recordValidation(feedback, source)
if feedback.Success {
collector.setCompleted(true)
feedback.Message = "Task completed successfully!"
} else if feedback.Message == "" {
feedback.Message = "Task not completed!"
}
if r.app != nil {
r.app.CurrentFeedback = &feedback
}
select {
case feedbackChan <- feedback:
default:
}
})
// Run interface
interfaceErr := make(chan error, 1)
go func() {
interfaceErr <- i.Run(ctx, config)
}()
select {
case <-doneChan:
close(stopChan)
close(quitChan)
collector.setCompleted(true)
if err := <-interfaceErr; err != nil {
return fmt.Errorf("task interface failed during shutdown: %w", err)
}
case err := <-interfaceErr:
close(stopChan)
close(quitChan)
if err != nil {
return fmt.Errorf("task interface failed: %w", err)
}
}
if oldState, err := term.GetState(uintptr(os.Stdin.Fd())); err == nil {
term.Restore(uintptr(os.Stdin.Fd()), oldState)
}
fmt.Print("\033[0m\033[?25h")
// Questionnaire
if err := runQuestionnaire(t, iType, collector); err != nil {
return err
}
collector.log("info", "task run finished")
return nil
}
func (r *RunLifecycle) Finish(ctx context.Context, runErr error) error {
if runErr != nil {
r.collector.setError(runErr)
r.collector.log("error", runErr.Error())
}
run := r.collector.finalize()
if r.metricsStore != nil {
if err := r.metricsStore.Append(ctx, r.details.Title, run); err != nil && runErr == nil {
return fmt.Errorf("persist metrics: %w", err)
}
}
if r.app != nil && r.app.Stats != nil {
if err := r.app.Stats.RecordTaskRun(ctx, run); err != nil && runErr == nil {
return fmt.Errorf("update global stats: %w", err)
}
}
return runErr
}
func runIntro(details models.TaskDetails) error {
model, err := tea.NewProgram(NewTaskModel(details)).Run()
if err != nil {
return err
}
if m, ok := model.(interface{ GetUserQuit() bool }); ok {
if m.GetUserQuit() {
return ErrUserQuit
}
}
return nil
}
func runQuestionnaire(t Tasker, iType InterfaceType, collector *taskRunCollector) error {
questions := t.Questions(iType)
keys := []string{}
if provider, ok := t.(QuestionnaireKeysProvider); ok {
keys = provider.QuestionnaireKeys(iType)
}
model, err := tea.NewProgram(NewQuestionnaireModel(questions, keys)).Run()
if err != nil {
return err
}
var completed bool
var answers map[string]any
if m, ok := model.(interface {
GetCompleted() bool
GetAnswers() map[string]any
}); ok {
completed = m.GetCompleted()
answers = m.GetAnswers()
}
userQuit := false
if m, ok := model.(interface{ GetUserQuit() bool }); ok {
userQuit = m.GetUserQuit()
}
collector.recordQuestionnaire(completed, userQuit, answers)
if userQuit {
return ErrUserQuit
}
return nil
}