219 lines
4.6 KiB
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
|
|
}
|