Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 12 additions & 3 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,18 @@ package cmd
import (
"errors"
"fmt"
"time"

d "github.com/dhth/mult/internal/domain"
"github.com/dhth/mult/internal/executor"
"github.com/dhth/mult/internal/ui"
"github.com/spf13/cobra"
)

const maxNumRuns = 1000
const (
maxNumRuns = 1000
processWaitDelay = time.Second
)

var (
errInvalidNumRunsRequested = errors.New("invalid number of runs requested")
Expand Down Expand Up @@ -62,7 +67,7 @@ func NewRootCommand() *cobra.Command {

return nil
},
RunE: func(_ *cobra.Command, args []string) error {
RunE: func(cmd *cobra.Command, args []string) error {
var nRuns int
if interactive {
fmt.Printf("number of runs?\n")
Expand All @@ -87,7 +92,11 @@ func NewRootCommand() *cobra.Command {
StopOnFirstSuccess: stopOnFirstSuccess,
}

return ui.RenderUI(args, config)
ctx := cmd.Context()
runner := executor.New(ctx, processWaitDelay)
defer runner.Shutdown()

return ui.RenderUI(ctx, args, config, runner)
},
}

Expand Down
71 changes: 71 additions & 0 deletions internal/executor/runner.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package executor

import (
"context"
"errors"
"os/exec"
"sync"
"time"
)

var (
ErrClosed = errors.New("executor is closed")
errEmptyCommand = errors.New("command cannot be empty")
)

type Runner struct {
ctx context.Context
cancel context.CancelFunc
waitDelay time.Duration

mu sync.Mutex
closed bool
wg sync.WaitGroup
}

func New(parent context.Context, waitDelay time.Duration) *Runner {
ctx, cancel := context.WithCancel(parent)

return &Runner{
ctx: ctx,
cancel: cancel,
waitDelay: waitDelay,
}
}

func (r *Runner) Run(command, env []string) ([]byte, error) {
if len(command) == 0 {
return nil, errEmptyCommand
}

r.mu.Lock()
if r.closed {
r.mu.Unlock()
return nil, ErrClosed
}
if err := r.ctx.Err(); err != nil {
r.mu.Unlock()
return nil, err
}
r.wg.Add(1)
r.mu.Unlock()

defer r.wg.Done()

c := exec.CommandContext(r.ctx, command[0], command[1:]...)
c.Env = env
c.WaitDelay = r.waitDelay

return c.CombinedOutput()
}

func (r *Runner) Shutdown() {
r.mu.Lock()
if !r.closed {
r.closed = true
r.cancel()
}
r.mu.Unlock()

r.wg.Wait()
}
16 changes: 4 additions & 12 deletions internal/ui/cmds.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@ package ui
import (
"fmt"
"os"
"os/exec"
"time"

tea "charm.land/bubbletea/v2"
"github.com/dhth/mult/internal/executor"
)

func chooseRunEntry(runNum int) tea.Cmd {
Expand All @@ -27,19 +27,11 @@ func runAfterDelay(interval time.Duration, iterationNum int) tea.Cmd {
})
}

func runCmd(cmd []string, iterationNum int) tea.Cmd {
func runCmd(runner *executor.Runner, cmd []string, iterationNum int) tea.Cmd {
return func() tea.Msg {
var c *exec.Cmd

if len(cmd) == 1 {
c = exec.Command(cmd[0])
} else {
c = exec.Command(cmd[0], cmd[1:]...)
}

c.Env = append(os.Environ(), fmt.Sprintf("MULT_RUN_NUM=%d", iterationNum+1))
env := append(os.Environ(), fmt.Sprintf("MULT_RUN_NUM=%d", iterationNum+1))
startTime := time.Now()
out, err := c.CombinedOutput()
out, err := runner.Run(cmd, env)
endTime := time.Now()
return CmdRanMsg{
iterationNum: iterationNum,
Expand Down
4 changes: 3 additions & 1 deletion internal/ui/initial.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import (
"charm.land/bubbles/v2/list"
"charm.land/lipgloss/v2"
d "github.com/dhth/mult/internal/domain"
"github.com/dhth/mult/internal/executor"
)

func InitialModel(cmd []string, config d.Config) Model {
func InitialModel(cmd []string, config d.Config, runner *executor.Runner) Model {
stackItems := make([]list.Item, config.NumRuns)

for i := range config.NumRuns {
Expand All @@ -29,6 +30,7 @@ func InitialModel(cmd []string, config d.Config) Model {

m := Model{
cmd: cmd,
runner: runner,
msg: userMsg{},
config: config,
lastRunIndex: -1,
Expand Down
10 changes: 6 additions & 4 deletions internal/ui/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
tea "charm.land/bubbletea/v2"
"charm.land/lipgloss/v2"
d "github.com/dhth/mult/internal/domain"
"github.com/dhth/mult/internal/executor"
)

type Pane uint
Expand All @@ -33,6 +34,7 @@ type userMsg struct {

type Model struct {
cmd []string
runner *executor.Runner
config d.Config
runList list.Model
lastRunIndex int
Expand Down Expand Up @@ -61,14 +63,14 @@ type Model struct {
func (m Model) Init() tea.Cmd {
var cmds []tea.Cmd
cmds = append(cmds, hideHelp(time.Second*30))
cmds = append(cmds, runCmd(m.cmd, 0))
cmds = append(cmds, runCmd(m.runner, m.cmd, 0))

if m.config.Sequential {
return tea.Batch(cmds...)
}

for i := 1; i < m.config.NumRuns; i++ {
cmds = append(cmds, runCmd(m.cmd, i))
cmds = append(cmds, runCmd(m.runner, m.cmd, i))
}

return tea.Batch(cmds...)
Expand Down Expand Up @@ -115,12 +117,12 @@ func (m *Model) clearRunList() tea.Cmd {

func (m Model) restartRuns() tea.Cmd {
if m.config.Sequential {
return runCmd(m.cmd, 0)
return runCmd(m.runner, m.cmd, 0)
}

var cmds []tea.Cmd
for i := 0; i < m.config.NumRuns; i++ {
cmds = append(cmds, runCmd(m.cmd, i))
cmds = append(cmds, runCmd(m.runner, m.cmd, i))
}

return tea.Batch(cmds...)
Expand Down
6 changes: 4 additions & 2 deletions internal/ui/ui.go
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
package ui

import (
"context"
"errors"
"fmt"
"os"

tea "charm.land/bubbletea/v2"
d "github.com/dhth/mult/internal/domain"
"github.com/dhth/mult/internal/executor"
)

var errFailedToConfigureDebugging = errors.New("failed to configure debugging")

func RenderUI(cmd []string, config d.Config) error {
func RenderUI(ctx context.Context, cmd []string, config d.Config, runner *executor.Runner) error {
if len(os.Getenv("DEBUG")) > 0 {
f, err := tea.LogToFile("debug.log", "debug")
if err != nil {
Expand All @@ -20,7 +22,7 @@ func RenderUI(cmd []string, config d.Config) error {
defer f.Close()
}

p := tea.NewProgram(InitialModel(cmd, config))
p := tea.NewProgram(InitialModel(cmd, config, runner), tea.WithContext(ctx))
_, err := p.Run()

return err
Expand Down
4 changes: 2 additions & 2 deletions internal/ui/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
if ok {
nextRunItem.RunStatus = d.Running
cmds = append(cmds, m.runList.SetItem(i+1, nextRunItem))
cmds = append(cmds, runCmd(m.cmd, i+1))
cmds = append(cmds, runCmd(m.runner, m.cmd, i+1))
}
} else {
nextRunItem, ok := m.runList.Items()[i+1].(cmdRunItem)
Expand All @@ -200,7 +200,7 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {

runItem.RunStatus = d.Running
cmds = append(cmds, m.runList.SetItem(msg.iterationNum, runItem))
cmds = append(cmds, runCmd(m.cmd, msg.iterationNum))
cmds = append(cmds, runCmd(m.runner, m.cmd, msg.iterationNum))

case CmdRunChosenMsg:
if m.config.FollowResults {
Expand Down