Skip to content

Commit

Permalink
feat: support executing bin again when bin exit (#363)
Browse files Browse the repository at this point in the history
* feat: support executing bin again when bin exit

* fix bug and add feature flag

* add rerun test

* disable rerun in default

* fix logic error

* delete unnecessary code

* add default option in toml

Co-authored-by: xiantang <zhujingdi1998@gmail.com>
  • Loading branch information
amikai and xiantang authored Jan 5, 2023
1 parent d0a697a commit 078d8ff
Show file tree
Hide file tree
Showing 4 changed files with 194 additions and 31 deletions.
4 changes: 4 additions & 0 deletions air_example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@ stop_on_error = true
send_interrupt = false
# Delay after sending Interrupt signal
kill_delay = 500 # ms
# Rerun binary or not
rerun = false
# Delay after each executions
rerun_delay = 500
# Add additional arguments when running binary (bin/full_bin). Will run './tmp/main hello world'.
args_bin = ["hello", "world"]

Expand Down
8 changes: 8 additions & 0 deletions runner/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ type cfgBuild struct {
StopOnError bool `toml:"stop_on_error"`
SendInterrupt bool `toml:"send_interrupt"`
KillDelay time.Duration `toml:"kill_delay"`
Rerun bool `toml:"rerun"`
RerunDelay int `toml:"rerun_delay"`
regexCompiled []*regexp.Regexp
}

Expand Down Expand Up @@ -213,6 +215,8 @@ func defaultConfig() Config {
ArgsBin: []string{},
ExcludeRegex: []string{"_test.go"},
Delay: 0,
Rerun: false,
RerunDelay: 500,
}
if runtime.GOOS == PlatformWindows {
build.Bin = `tmp\main.exe`
Expand Down Expand Up @@ -328,6 +332,10 @@ func (c *Config) buildDelay() time.Duration {
return time.Duration(c.Build.Delay) * time.Millisecond
}

func (c *Config) rerunDelay() time.Duration {
return time.Duration(c.Build.RerunDelay) * time.Millisecond
}

func (c *Config) binPath() string {
return filepath.Join(c.Root, c.Build.Bin)
}
Expand Down
89 changes: 58 additions & 31 deletions runner/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"path/filepath"
"strings"
"sync"
"sync/atomic"
"time"

"github.com/fsnotify/fsnotify"
Expand All @@ -32,6 +33,7 @@ type Engine struct {

mu sync.RWMutex
watchers uint
round uint64
fileChecksums *checksumMap

ll sync.Mutex // lock for logger
Expand Down Expand Up @@ -431,36 +433,37 @@ func (e *Engine) building() error {
}

func (e *Engine) runBin() error {
var err error
e.runnerLog("running...")
// control killFunc should be kill or not
killCh := make(chan struct{})
wg := sync.WaitGroup{}
go func() {
// listen to binStopCh
// cleanup() will close binStopCh when engine stop
// start() will close binStopCh when file changed
<-e.binStopCh
close(killCh)

command := strings.Join(append([]string{e.config.Build.Bin}, e.runArgs...), " ")
cmd, stdout, stderr, err := e.startCmd(command)
if err != nil {
return err
}
select {
case <-e.exitCh:
wg.Wait()
close(e.canExit)
default:
}

go func() {
_, _ = io.Copy(os.Stdout, stdout)
_, _ = cmd.Process.Wait()
}()

go func() {
_, _ = io.Copy(os.Stderr, stderr)
_, _ = cmd.Process.Wait()
}()
killFunc := func(cmd *exec.Cmd, stdout io.ReadCloser, stderr io.ReadCloser, killCh chan struct{}, processExit chan struct{}, wg *sync.WaitGroup) {
defer wg.Done()
select {
// the process haven't exited yet, kill it
case <-killCh:
break

// the process is exited, return
case <-processExit:
return
}

killFunc := func(cmd *exec.Cmd, stdout io.ReadCloser, stderr io.ReadCloser) {
defer func() {
select {
case <-e.exitCh:
e.mainDebug("exit in killFunc")
close(e.canExit)
default:
}
}()
// when invoke close() it will return
<-e.binStopCh
e.mainDebug("trying to kill pid %d, cmd %+v", cmd.Process.Pid, cmd.Args)
defer func() {
stdout.Close()
Expand All @@ -483,12 +486,36 @@ func (e *Engine) runBin() error {
e.mainLog("failed to remove %s, error: %s", e.config.rel(e.config.binPath()), err)
}
}
e.withLock(func() {
close(e.binStopCh)
e.binStopCh = make(chan bool)
go killFunc(cmd, stdout, stderr)
})
e.mainDebug("running process pid %v", cmd.Process.Pid)

e.runnerLog("running...")
go func() {
for {
select {
case <-killCh:
return
default:
command := strings.Join(append([]string{e.config.Build.Bin}, e.runArgs...), " ")
cmd, stdout, stderr, _ := e.startCmd(command)
processExit := make(chan struct{})
e.mainDebug("running process pid %v", cmd.Process.Pid)

wg.Add(1)
atomic.AddUint64(&e.round, 1)
go killFunc(cmd, stdout, stderr, killCh, processExit, &wg)

_, _ = io.Copy(os.Stdout, stdout)
_, _ = io.Copy(os.Stderr, stderr)
_, _ = cmd.Process.Wait()
close(processExit)

if !e.config.Build.Rerun {
return
}
time.Sleep(e.config.rerunDelay())
}
}
}()

return nil
}

Expand Down
124 changes: 124 additions & 0 deletions runner/engine_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"os/signal"
"runtime"
"strings"
"sync/atomic"
"syscall"
"testing"
"time"
Expand Down Expand Up @@ -110,6 +111,85 @@ func TestRegexes(t *testing.T) {
}
}

func TestRerun(t *testing.T) {
tmpDir := initWithQuickExitGoCode(t)
// change dir to tmpDir
err := os.Chdir(tmpDir)
if err != nil {
t.Fatalf("Should not be fail: %s.", err)
}
engine, err := NewEngine("", true)
engine.config.Build.ExcludeUnchanged = true
engine.config.Build.Rerun = true
engine.config.Build.RerunDelay = 100
if err != nil {
t.Fatalf("Should not be fail: %s.", err)
}
go func() {
engine.Run()
t.Logf("engine run")
}()

time.Sleep(time.Second * 1)

// stop engine
engine.Stop()
time.Sleep(time.Second * 1)
t.Logf("engine stopped")

if atomic.LoadUint64(&engine.round) <= 1 {
t.Fatalf("The engine did't rerun")
}
}

func TestRerunWhenFileChanged(t *testing.T) {
tmpDir := initWithQuickExitGoCode(t)
// change dir to tmpDir
err := os.Chdir(tmpDir)
if err != nil {
t.Fatalf("Should not be fail: %s.", err)
}
engine, err := NewEngine("", true)
engine.config.Build.ExcludeUnchanged = true
engine.config.Build.Rerun = true
engine.config.Build.RerunDelay = 100
if err != nil {
t.Fatalf("Should not be fail: %s.", err)
}
go func() {
engine.Run()
t.Logf("engine run")
}()
time.Sleep(time.Second * 1)

roundBeforeChange := atomic.LoadUint64(&engine.round)

t.Logf("start change main.go")
// change file of main.go
// just append a new empty line to main.go
time.Sleep(time.Second * 2)
file, err := os.OpenFile("main.go", os.O_APPEND|os.O_WRONLY, 0o644)
if err != nil {
t.Fatalf("Should not be fail: %s.", err)
}
defer file.Close()
_, err = file.WriteString("\n")
if err != nil {
t.Fatalf("Should not be fail: %s.", err)
}

time.Sleep(time.Second * 1)
// stop engine
engine.Stop()
time.Sleep(time.Second * 1)
t.Logf("engine stopped")

roundAfterChange := atomic.LoadUint64(&engine.round)
if roundBeforeChange+1 >= roundAfterChange {
t.Fatalf("The engine didn't rerun")
}
}

func TestRunBin(t *testing.T) {
engine, err := NewEngine("", true)
if err != nil {
Expand Down Expand Up @@ -490,6 +570,50 @@ func initWithBuildFailedCode(t *testing.T) string {
return tempDir
}

func initWithQuickExitGoCode(t *testing.T) string {
tempDir := t.TempDir()
t.Logf("tempDir: %s", tempDir)
// generate golang code to tempdir
err := generateQuickExitGoCode(tempDir)
if err != nil {
t.Fatalf("Should not be fail: %s.", err)
}
return tempDir
}

func generateQuickExitGoCode(dir string) error {
code := `package main
// You can edit this code!
// Click here and start typing.
import "fmt"
func main() {
fmt.Println("Hello, 世界")
}
`
file, err := os.Create(dir + "/main.go")
if err != nil {
return err
}
_, err = file.WriteString(code)

// generate go mod file
mod := `module air.sample.com
go 1.17
`
file, err = os.Create(dir + "/go.mod")
if err != nil {
return err
}
_, err = file.WriteString(mod)
if err != nil {
return err
}
return nil
}

func generateBuildErrorGoCode(dir string) error {
code := `package main
// You can edit this code!
Expand Down

0 comments on commit 078d8ff

Please sign in to comment.