-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Recieve and handle ctrl + c and exit application #953
Conversation
Codecov Report
@@ Coverage Diff @@
## master #953 +/- ##
==========================================
+ Coverage 73.36% 73.41% +0.05%
==========================================
Files 32 32
Lines 2440 2445 +5
==========================================
+ Hits 1790 1795 +5
Misses 540 540
Partials 110 110
Continue to review full report at Codecov.
|
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.
LGTM.
I would not add another option for this.
As I explained, we should not do this. This takes away the opportunity from the user to clean up. We could add an option which when enabled cancels the context, but we should definitely not call os.Exit(1) ourselves. If I have some sort of SQL transaction ongoing, this will just yank the control from my function and kill the process without me being able to roll it back. |
@AudriusButkevicius & @ansrivas can you two have a conversation about what the ideal solution is here? This is one potential solution for #945. |
@AudriusButkevicius I think the real problem here is the lack of a sensible default. For most use cases, |
An option with os.Exit encourages people to design their applications poorly, not performing cleanup. This library should be about argument parsing and based on the arguments calling functions, not about lifecycle management of application. I suspect you won't find a library in other languages that does os.Exit unasked |
If we have a default, they can read about it in the documentation and override it! The implementation right now provides no way to do this. |
Or we could simply have that handler print a message about intercepting the ctrl + c and request the user to override it! |
I think the default should be to do nothing, as thats least surprising from the API perspective with a out-of-the-box available handler that does os.Exit that users have to setup. Perhaps the default handler should cancel the context on interrupt, which if the user implemented his app as expected (propagating contexts all the way down), would lead to the same effect. |
I agree. Let me implement this. |
bb6eb9c
@AudriusButkevicius Is this better now? |
This totally makes sense here as this was exactly the confusion btw in #945 - how to propagate a context further for a cleanup. |
@ansrivas I think now you'd be able to do this: app.InterruptHandlerFunc = func(c *cli.Context) {
// received interrupt
// cleanup
} |
Hi @AudriusButkevicius I have made the necessary changes. Here's an example illustrating the use of the feature: package main
import (
"context"
"fmt"
"log"
"os"
"time"
"github.com/urfave/cli/v2"
)
func main() {
app := &cli.App{
Name: "long",
Usage: "this takes a long time to finish",
Action: func(c *cli.Context) error {
fmt.Println("working...")
time.Sleep(10 * time.Second)
return nil
},
}
app.Flags = []cli.Flag{
&cli.IntFlag{
Name: "count",
},
}
signalCount := 1
app.InterruptHandler = func(c *cli.Context, cancelFunc context.CancelFunc) {
go func() {
<-c.Done()
os.Exit(1)
}()
count := c.Int("count")
if signalCount < count {
fmt.Println("received interrupt")
} else {
fmt.Println("signal count exceeded, quitting")
cancelFunc()
}
signalCount++
}
err := app.Run(os.Args)
if err != nil {
log.Fatal(err)
}
} In the above example, I am waiting for the interrupt signal to be received 5 times before the context is cancelled. |
The CI is failing because |
@@ -151,6 +156,10 @@ func (a *App) Setup() { | |||
a.Action = helpCommand.Action | |||
} | |||
|
|||
if a.InterruptHandler == nil { | |||
a.InterruptHandler = CancelContextOnInterrupt | |||
} |
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.
Should not do this, user might set it to null not to get the context cancelled.
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.
Essentially, if this is null, we should not even try to setup signal handling, and leave it up to the user.
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.
I think if it's set to nil, we should actually set it to the Noop handler. We can add that to the documentation as well. It's easier that way to not cause nil panics
// This function is automatically executed when the context is cancelled due to the program | ||
// receiving an interrupt signal (Control+C). This function can be overridden to implement | ||
// desired behaviour in such a scenario | ||
InterruptHandler InterruptHandlerFunc |
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.
The function is executed when the application receives and interuption signal, what that does is configurable based on what the handler is doing.
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.
If we change this to signal handler, we should change the comment quite heavily.
for { | ||
<-signals | ||
c.App.InterruptHandler(c) | ||
} |
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.
for signal := range signals {}
@@ -37,13 +38,16 @@ func NewContext(app *App, set *flag.FlagSet, parentCtx *Context) *Context { | |||
|
|||
if c.Context == nil { | |||
ctx, cancel := context.WithCancel(context.Background()) | |||
signals := make(chan os.Signal, 1) | |||
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) |
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.
Because we capture TERM, perhaps we should not call it an interrupt handler but call it signal handler. I did not see that we capture more than a single signal before. Also, because we capture multiple, perhaps we should pass the signal to the handler.
var ExitOnInterrupt InterruptHandlerFunc = func(ctx *Context) { | ||
CancelContextOnInterrupt(ctx) | ||
<-ctx.Done() | ||
os.Exit(1) |
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.
I don't think we should cancel the context in this case.
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.
To be honest, if we start passing down signals, I think default should be os.Exit on sigterm, cancel context on sigint
I'd like to see testing around this. Specifically, I think cases should include tests for verifying that custom interrupt handlers (or signal handlers) are called when specified, tests for what happens with the defaults, and tests for what happens when a |
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.
clearing my review request for the moment - feel free to request me again later 🙏
I like the direction in which this is headed to. Let me further improve this :) |
Sorry for changing the course so many times. |
@AudriusButkevicius Please don't be sorry! I would change to course a million times to produce good software 🙂 |
I have struggled with developers not handling signals properly so much, so I'm really grateful that will come out of the box with I do strongly believe though, that cli must not run os.Exit. Simply return the error from Basically the original approach (which is currently tagged as v2) worked pretty fine in a setup similar to this one: app := &cli.App{
Name: "say",
Version: "0.1.0",
Commands: []*cli.Command{
{
Name: "hello",
Aliases: []string{"hi"},
Usage: "use it to see a description",
Description: "This is how we describe hello the function",
Action: func(ctx *cli.Context) error {
t := time.NewTicker(1 * time.Second)
defer func() {
t.Stop()
fmt.Println("Gracefully shutting down...")
time.Sleep(2 * time.Second)
}()
fmt.Println("Timer started")
for {
select {
case tt := <-t.C:
fmt.Println(tt.String())
case <-ctx.Done():
return ctx.Err()
}
}
},
},
},
}
if err := app.Run(os.Args); err != nil {
fmt.Printf("%+v\n", err)
os.Exit(1)
} So this way everything works perfectly. The only thing missing though is normally you can force close the app pressing another I think this would be the best of both worlds: user control and handling signals by default. |
I think this PR is dead. We've decided that the argument parsing library is not in the position to do signal handling, and instead allow users to pass a context to the cli which they can now cancel (by doing their own signal handling). |
This would also be perfect. Having a way to integrate context easily is one of the reasons I came to cli from cobra (my PR spf13/cobra#893 there implementing this for some reason is not getting response from maintainers). |
Fixes #945
We were correctly receiving
ctrl+c
signal and cancelling our application context. The missing piece of the puzzle was not listening for context cancellation and exiting the program.Since
ctrl+c
is a non standard program termination, I have used a non zero exit code i.e. 1 for the moment. Please let me know if we want to give the user an option to set a exit code themselves in case ofctrl+c
exit. We can then add an option in theApp
for that then.