Compare commits

..

No commits in common. "main" and "v0.1.0" have entirely different histories.
main ... v0.1.0

4 changed files with 30 additions and 66 deletions

View File

@ -1,3 +1,3 @@
# Tortoise # Go Shell Runner
Library for asyncronously executing shell commands in Go Library for asyncronously executing shell commands in Go

2
go.mod
View File

@ -1,3 +1,3 @@
module git.iamthefij.com/iamthefij/tortoise module git.iamthefij.com/iamthefij/go-shell-runner
go 1.21.4 go 1.21.4

13
main.go
View File

@ -1,4 +1,4 @@
package tortoise package shellrunner
import ( import (
"bytes" "bytes"
@ -45,10 +45,9 @@ func NewShellRunnerWithShell(shell string) *ShellRunner {
cmdQueue: make(chan func() *CommandResult), cmdQueue: make(chan func() *CommandResult),
results: make(chan *CommandResult, MAX_RESULTS), results: make(chan *CommandResult, MAX_RESULTS),
stopChan: make(chan struct{}), stopChan: make(chan struct{}),
callbacks: make(chan func()), callbacks: make(chan func(), MAX_RESULTS),
shell: shell, shell: shell,
activeCmds: make(map[*exec.Cmd]struct{}), activeCmds: make(map[*exec.Cmd]struct{}),
isStopped: true,
} }
} }
@ -76,20 +75,16 @@ func (sr *ShellRunner) Start() {
} }
} }
}() }()
sr.isStopped = false
} }
// AddCommand adds a shell command to be executed with an optional callback. // AddCommand adds a shell command to be executed with an optional callback.
// No commands can be added if the runner has been stopped or not yet started. // No commands can be added if the runner has been stopped.
// The callback is executed asynchronously after the command has completed.
// The order of command execution and callback invocation can be expected to be preserved.
func (sr *ShellRunner) AddCommand(command string, callback func(*CommandResult)) error { func (sr *ShellRunner) AddCommand(command string, callback func(*CommandResult)) error {
sr.mu.Lock() sr.mu.Lock()
defer sr.mu.Unlock() defer sr.mu.Unlock()
if sr.isStopped { if sr.isStopped {
return errors.New("runner is stopped, cannot add new commands") return errors.New("runner has been stopped, cannot add new commands")
} }
sr.cmdQueue <- func() *CommandResult { sr.cmdQueue <- func() *CommandResult {

View File

@ -1,16 +1,13 @@
package tortoise_test package shellrunner_test
import ( import (
"sync"
"testing" "testing"
"time" "time"
"git.iamthefij.com/iamthefij/tortoise" shellrunner "git.iamthefij.com/iamthefij/go-shell-runner"
) )
func TestShellRunnerNoCallback(t *testing.T) { func TestShellRunnerNoCallback(t *testing.T) {
t.Parallel()
cases := []struct { cases := []struct {
command string command string
output string output string
@ -25,7 +22,7 @@ func TestShellRunnerNoCallback(t *testing.T) {
t.Run(c.command, func(t *testing.T) { t.Run(c.command, func(t *testing.T) {
t.Parallel() t.Parallel()
runner := tortoise.NewShellRunner() runner := shellrunner.NewShellRunner()
runner.Start() runner.Start()
// Test command without callback // Test command without callback
@ -46,71 +43,52 @@ func TestShellRunnerNoCallback(t *testing.T) {
func TestShellRunnerCallback(t *testing.T) { func TestShellRunnerCallback(t *testing.T) {
t.Parallel() t.Parallel()
runner := tortoise.NewShellRunner() runner := shellrunner.NewShellRunner()
runner.Start() runner.Start()
// Test command with callback // Test command with callback
outputString := ""
callbackWait := sync.WaitGroup{}
callbackWait.Add(1)
if err := runner.AddCommand("echo callback a", func(result *tortoise.CommandResult) {
if result.Output != "callback a\n" {
t.Fatalf("expected 'callback a', got '%s'", result.Output)
}
outputString = outputString + "a"
callbackWait.Done()
}); err != nil {
t.Fatalf("unexpected error adding command: %v", err)
}
callbackWait.Add(1)
if err := runner.AddCommand("echo callback b", func(result *tortoise.CommandResult) {
if result.Output != "callback b\n" {
t.Fatalf("expected 'callback b', got '%s'", result.Output)
}
outputString = outputString + "b"
callbackWait.Done()
}); err != nil {
t.Fatalf("unexpected error adding command: %v", err)
}
// Timeout waiting for callbacks
done := make(chan struct{}) done := make(chan struct{})
go func() {
callbackWait.Wait()
close(done)
}()
callbackReached := false
if err := runner.AddCommand("echo callback test", func(result *shellrunner.CommandResult) {
callbackReached = true
if result.Output != "callback test\n" {
t.Fatalf("expected 'callback test', got '%s'", result.Output)
}
close(done)
}); err != nil {
t.Fatalf("unexpected error adding command: %v", err)
}
// Timeout waiting for callback
select { select {
case <-done: case <-done:
case <-time.After(2 * time.Second): case <-time.After(2 * time.Second):
t.Fatal("callbacks timed out") t.Fatal("callback timed out")
} }
if outputString != "ab" { if !callbackReached {
t.Fatal("callbacks was not reached in order:", outputString) t.Fatal("callback was not reached")
} }
runner.Stop() runner.Stop()
// Make sure stop and kill both exit gracefully after the runner is stopped // Make sure stop and kill both exit gracefully after the runner is stopped
runner.Stop() runner.Stop()
runner.Kill() runner.Stop()
} }
func TestShellRunnerKillWithTimeout(t *testing.T) { func TestShellRunnerKillWithTimeout(t *testing.T) {
t.Parallel() t.Parallel()
runner := tortoise.NewShellRunner() runner := shellrunner.NewShellRunner()
runner.Start() runner.Start()
// Test command with callback // Test command with callback
callbackReached := false callbackReached := false
if err := runner.AddCommand("sleep 10 && echo callback test", func(result *tortoise.CommandResult) { if err := runner.AddCommand("sleep 10 && echo callback test", func(result *shellrunner.CommandResult) {
callbackReached = true callbackReached = true
if result.Output != "callback test\n" { if result.Output != "callback test\n" {
t.Fatalf("expected 'callback test', got '%s'", result.Output) t.Fatalf("expected 'callback test', got '%s'", result.Output)
@ -132,7 +110,7 @@ func TestShellRunnerKillWithTimeout(t *testing.T) {
} }
func TestStopPreventsNewCommands(t *testing.T) { func TestStopPreventsNewCommands(t *testing.T) {
runner := tortoise.NewShellRunner() runner := shellrunner.NewShellRunner()
runner.Start() runner.Start()
runner.Stop() runner.Stop()
@ -142,12 +120,3 @@ func TestStopPreventsNewCommands(t *testing.T) {
t.Fatal("expected error when adding command after stop, but got none") t.Fatal("expected error when adding command after stop, but got none")
} }
} }
func TestAddingPriorToStart(t *testing.T) {
runner := tortoise.NewShellRunner()
err := runner.AddCommand("echo should not run", nil)
if err == nil {
t.Fatal("Should have failed to add prior to starting runner")
}
}