-
Notifications
You must be signed in to change notification settings - Fork 7
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Graceful shutdown #11
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is kinda a lie, if we've got a timeout and then kill without waiting. Also, if we're logging this, we probably should log when that timeout fires. If we have both of those logging things, maybe they belong close together in the code? |
||
cancel() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There are two things going on here that are not "library-safe", but I think it's OK because this is "cmd" code and not "internal" code.
Not sure there is any action for this comment, just thinking aloud as I review this code. |
||
}() | ||
|
||
err = internal.Run(ctx, repo, runOpts) | ||
var exitErr *exec.ExitError | ||
if errors.As(err, &exitErr) { | ||
os.Exit(exitErr.ExitCode()) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,27 +18,30 @@ | |
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 | ||
Args []string | ||
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} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not familiar with this incantation. A comment would help. |
||
|
||
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 { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. b/c of the timeout, this really StopAndThenKill(), instead of either Stop or Kill. Maybe it's Kill(gracePeriod)? |
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This logic is strange to me. We async start a kill after a sleep, then we attempt a term and return that error code. Then the calling code needs the |
||
} | ||
|
||
func (proc *cmdProcess) Wait() error { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is not necessary. See elsewhere. |
||
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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. reviewer note: this looks right b/c of the following return, so you can't unintentionally close the abort channel twice 👍🏻 |
||
return nil | ||
} | ||
} | ||
}) | ||
} else { | ||
go func() { | ||
<-ctx.Done() | ||
close(abort) | ||
}() | ||
} | ||
|
||
return g.Wait() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to handle
SIGKILL
too?