diff --git a/cmd/build.go b/cmd/build.go index 31ebd66..42a266c 100644 --- a/cmd/build.go +++ b/cmd/build.go @@ -47,7 +47,7 @@ Given no arguments, builds all packages. Otherwise, builds only the specified pa // TODO: Parallelism. for _, pkg := range packages { buildOpts.Package = pkg - if err := internal.Build(repo, buildOpts); err != nil { + if err := internal.Build(cmd.Context(), repo, buildOpts); err != nil { return err } } diff --git a/cmd/run.go b/cmd/run.go index 84c790a..08bce0f 100644 --- a/cmd/run.go +++ b/cmd/run.go @@ -1,11 +1,14 @@ package cmd import ( + "context" "errors" "fmt" "os" "os/exec" + "os/signal" "path/filepath" + "syscall" "github.com/deref/uni/internal" "github.com/spf13/cobra" @@ -61,7 +64,16 @@ export const main = async (...args: string[]) => { runOpts.Args = args[1:] - err = internal.Run(repo, runOpts) + ctx, cancel := context.WithCancel(cmd.Context()) + shutdown := make(chan os.Signal, 1) + signal.Notify(shutdown, syscall.SIGINT) + go func() { + _ = <-shutdown + fmt.Fprintf(os.Stderr, "Received shutdown signal. Waiting for process to finish.\n") + cancel() + }() + + err = internal.Run(ctx, repo, runOpts) var exitErr *exec.ExitError if errors.As(err, &exitErr) { os.Exit(exitErr.ExitCode()) diff --git a/internal/build.go b/internal/build.go index da0f17f..a5786d6 100644 --- a/internal/build.go +++ b/internal/build.go @@ -1,6 +1,7 @@ package internal import ( + "context" "fmt" "io/ioutil" "os" @@ -17,7 +18,7 @@ type BuildOptions struct { Watch bool } -func Build(repo *Repository, opts BuildOptions) error { +func Build(ctx context.Context, repo *Repository, opts BuildOptions) error { pkg := opts.Package packageDir := path.Join(repo.OutDir, "dist", pkg.Name) @@ -161,7 +162,7 @@ void (async () => { }, } }, - }.Run() + }.Run(ctx) } type funcProcess struct { @@ -172,7 +173,7 @@ func (proc *funcProcess) Start() error { return proc.start() } -func (proc *funcProcess) Kill() error { +func (proc *funcProcess) Stop() error { return nil } diff --git a/internal/run.go b/internal/run.go index dbd6a63..b317579 100644 --- a/internal/run.go +++ b/internal/run.go @@ -18,15 +18,21 @@ package internal import ( + "context" "fmt" "io/ioutil" "os" "os/exec" "path" + "runtime" + "syscall" + "time" "github.com/evanw/esbuild/pkg/api" ) +var maxProcStopWait = 5 * time.Second + type RunOptions struct { Watch bool Entrypoint string @@ -34,11 +40,8 @@ type RunOptions struct { BuildOnly bool } -// TODO: Need to handle interrupts in order to have a higher chance -// of cleaning up temporary files. - -// Status code may be returend within an exec.ExitError return value. -func Run(repo *Repository, opts RunOptions) error { +// Status code may be returned within an exec.ExitError return value. +func Run(ctx context.Context, repo *Repository, opts RunOptions) error { if err := EnsureTmp(repo); err != nil { return err } @@ -118,10 +121,11 @@ if (typeof main === 'function') { node.Stdin = os.Stdin node.Stdout = os.Stdout node.Stderr = os.Stderr + node.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} return &cmdProcess{cmd: node} }, - }.Run() + }.Run(ctx) } type cmdProcess struct { @@ -132,11 +136,22 @@ func (proc *cmdProcess) Start() error { return proc.cmd.Start() } -func (proc *cmdProcess) Kill() error { +func (proc *cmdProcess) Stop() error { if proc.cmd.Process == nil { return nil } - return proc.cmd.Process.Kill() + + if runtime.GOOS == "windows" { + return proc.cmd.Process.Kill() + } + + go func() { + // TODO: Make the wait time configurable. + time.Sleep(maxProcStopWait) + _ = syscall.Kill(-proc.cmd.Process.Pid, syscall.SIGKILL) + }() + + return syscall.Kill(-proc.cmd.Process.Pid, syscall.SIGTERM) } func (proc *cmdProcess) Wait() error { diff --git a/internal/watch.go b/internal/watch.go index 5abf531..1570ddc 100644 --- a/internal/watch.go +++ b/internal/watch.go @@ -1,6 +1,7 @@ package internal import ( + "context" "fmt" "log" "os" @@ -22,10 +23,10 @@ type buildAndWatch struct { type process interface { Start() error Wait() error - Kill() error + Stop() error } -func (opts buildAndWatch) Run() error { +func (opts buildAndWatch) Run(ctx context.Context) error { repo := opts.Repository plugins := append([]api.Plugin{}, opts.Esbuild.Plugins...) @@ -86,6 +87,11 @@ func (opts buildAndWatch) Run() error { for { proc := opts.CreateProcess() done := make(chan error, 1) + waitDone := func() { + if err := <-done; err != nil { + fmt.Fprintf(os.Stderr, "could not wait for process to finish: %v\n", err) + } + } buildOK := len(result.Errors) == 0 shouldStart := buildOK && !waitForChange @@ -104,8 +110,10 @@ func (opts buildAndWatch) Run() error { } select { case <-abort: - if err := proc.Kill(); err != nil { - fmt.Fprintf(os.Stderr, "could not kill: %v\n", err) + if err := proc.Stop(); err != nil { + fmt.Fprintf(os.Stderr, "could not stop: %v\n", err) + } else { + waitDone() } return nil case <-restart: @@ -119,8 +127,10 @@ func (opts buildAndWatch) Run() error { break loop } } - if err := proc.Kill(); err != nil { - fmt.Fprintf(os.Stderr, "could not kill: %v\n", err) + if err := proc.Stop(); err != nil { + fmt.Fprintf(os.Stderr, "could not stop: %v\n", err) + } else { + waitDone() } result = result.Rebuild() waitForChange = false @@ -152,9 +162,17 @@ func (opts buildAndWatch) Run() error { close(abort) return err } + case <-ctx.Done(): + close(abort) + return nil } } }) + } else { + go func() { + <-ctx.Done() + close(abort) + }() } return g.Wait()