Skip to content

Commit e174089

Browse files
authored
feat: handle ctrl+c in reporting (#2080)
1 parent 33bb2e3 commit e174089

File tree

4 files changed

+163
-0
lines changed

4 files changed

+163
-0
lines changed

cmd/installer/cli/install.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ type InstallCmdFlags struct {
8282
func InstallCmd(ctx context.Context, name string) *cobra.Command {
8383
var flags InstallCmdFlags
8484

85+
ctx, cancel := context.WithCancel(ctx)
86+
8587
cmd := &cobra.Command{
8688
Use: "install",
8789
Short: fmt.Sprintf("Install %s", name),
@@ -94,13 +96,20 @@ func InstallCmd(ctx context.Context, name string) *cobra.Command {
9496
},
9597
PostRun: func(cmd *cobra.Command, args []string) {
9698
runtimeconfig.Cleanup()
99+
cancel() // Cancel context when command completes
97100
},
98101
RunE: func(cmd *cobra.Command, args []string) error {
99102
clusterID := metrics.ClusterID()
100103
metricsReporter := NewInstallReporter(
101104
replicatedAppURL(), flags.license.Spec.LicenseID, clusterID, cmd.CalledAs(),
102105
)
103106
metricsReporter.ReportInstallationStarted(ctx)
107+
108+
// Setup signal handler with the metrics reporter cleanup function
109+
signalHandler(ctx, cancel, func(ctx context.Context, err error) {
110+
metricsReporter.ReportInstallationFailed(ctx, err)
111+
})
112+
104113
if err := runInstall(cmd.Context(), name, flags, metricsReporter); err != nil {
105114
metricsReporter.ReportInstallationFailed(ctx, err)
106115
return err

cmd/installer/cli/join.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ type JoinCmdFlags struct {
4646
func JoinCmd(ctx context.Context, name string) *cobra.Command {
4747
var flags JoinCmdFlags
4848

49+
ctx, cancel := context.WithCancel(ctx)
50+
4951
cmd := &cobra.Command{
5052
Use: "join <url> <token>",
5153
Short: fmt.Sprintf("Join %s", name),
@@ -61,6 +63,7 @@ func JoinCmd(ctx context.Context, name string) *cobra.Command {
6163
},
6264
PostRun: func(cmd *cobra.Command, args []string) {
6365
runtimeconfig.Cleanup()
66+
cancel() // Cancel context when command completes
6467
},
6568
RunE: func(cmd *cobra.Command, args []string) error {
6669
logrus.Debugf("fetching join token remotely")
@@ -70,6 +73,12 @@ func JoinCmd(ctx context.Context, name string) *cobra.Command {
7073
}
7174
metricsReporter := NewJoinReporter(jcmd.InstallationSpec.MetricsBaseURL, jcmd.ClusterID, cmd.CalledAs())
7275
metricsReporter.ReportJoinStarted(ctx)
76+
77+
// Setup signal handler with the metrics reporter cleanup function
78+
signalHandler(ctx, cancel, func(ctx context.Context, err error) {
79+
metricsReporter.ReportJoinFailed(ctx, err)
80+
})
81+
7382
if err := runJoin(cmd.Context(), name, flags, jcmd, metricsReporter); err != nil {
7483
metricsReporter.ReportJoinFailed(ctx, err)
7584
return err

cmd/installer/cli/signal.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package cli
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"os/signal"
8+
"syscall"
9+
10+
"github.com/sirupsen/logrus"
11+
)
12+
13+
// osExit is a variable to make testing easier
14+
var osExit = os.Exit
15+
16+
// signalHandler sets up handling for signals to ensure cleanup functions are called.
17+
func signalHandler(ctx context.Context, cancel context.CancelFunc, cleanupFuncs ...func(context.Context, error)) {
18+
sigChan := make(chan os.Signal, 1)
19+
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
20+
21+
go func() {
22+
select {
23+
case sig := <-sigChan:
24+
logrus.Debugf("Received signal: %v", sig)
25+
err := fmt.Errorf("command interrupted by signal: %v", sig)
26+
27+
for _, cleanup := range cleanupFuncs {
28+
cleanup(ctx, err)
29+
}
30+
31+
// Cancel the context after cleanup functions run
32+
cancel()
33+
34+
// Exit with non-zero status
35+
osExit(1)
36+
case <-ctx.Done():
37+
// Context was canceled elsewhere, do nothing
38+
return
39+
}
40+
}()
41+
}

cmd/installer/cli/signal_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package cli
2+
3+
import (
4+
"context"
5+
"os"
6+
"sync"
7+
"syscall"
8+
"testing"
9+
"time"
10+
11+
"github.com/stretchr/testify/assert"
12+
)
13+
14+
func Test_signalHandler_Signal(t *testing.T) {
15+
// Create a context with cancel function
16+
ctx, cancel := context.WithCancel(context.Background())
17+
defer cancel()
18+
19+
// Create a waitgroup to synchronize the test
20+
var wg sync.WaitGroup
21+
wg.Add(1)
22+
23+
// Track if cleanup function was called
24+
cleanupCalled := false
25+
cleanupError := ""
26+
27+
// Mock cleanup function
28+
cleanup := func(ctx context.Context, err error) {
29+
cleanupCalled = true
30+
if err != nil {
31+
cleanupError = err.Error()
32+
}
33+
wg.Done()
34+
}
35+
36+
// Save original os.Exit and restore after test
37+
originalOsExit := osExit
38+
defer func() { osExit = originalOsExit }()
39+
40+
exitCode := 0
41+
osExit = func(code int) {
42+
exitCode = code
43+
// Instead of exiting, just cancel the context
44+
cancel()
45+
}
46+
47+
// Set up the signal handler
48+
signalHandler(ctx, cancel, cleanup)
49+
50+
// Send a signal to trigger the handler
51+
p, err := os.FindProcess(os.Getpid())
52+
if err != nil {
53+
t.Fatalf("Failed to find process: %v", err)
54+
}
55+
56+
// Send SIGINT to trigger the handler
57+
err = p.Signal(syscall.SIGINT)
58+
if err != nil {
59+
t.Fatalf("Failed to send signal: %v", err)
60+
}
61+
62+
// Wait for cleanup to be called with a timeout
63+
waitCh := make(chan struct{})
64+
go func() {
65+
wg.Wait()
66+
close(waitCh)
67+
}()
68+
69+
select {
70+
case <-waitCh:
71+
// Success - cleanup was called
72+
case <-time.After(1 * time.Second):
73+
t.Fatal("Timed out waiting for cleanup function to be called")
74+
}
75+
76+
// Verify cleanup was called with the expected error
77+
assert.True(t, cleanupCalled, "Cleanup function should have been called")
78+
assert.Contains(t, cleanupError, "command interrupted by signal: interrupt")
79+
assert.Equal(t, 1, exitCode, "Exit code should be 1")
80+
}
81+
82+
func Test_signalHandler_ContextDone(t *testing.T) {
83+
// Create a context with cancel function
84+
ctx, cancel := context.WithCancel(context.Background())
85+
86+
// We expect cleanup NOT to be called when context is cancelled
87+
cleanupCalled := false
88+
89+
cleanup := func(ctx context.Context, err error) {
90+
cleanupCalled = true
91+
}
92+
93+
// Set up the signal handler
94+
signalHandler(ctx, cancel, cleanup)
95+
96+
// Cancel the context
97+
cancel()
98+
99+
// Give some time for any handlers to run
100+
time.Sleep(100 * time.Millisecond)
101+
102+
// Verify cleanup was NOT called
103+
assert.False(t, cleanupCalled, "Cleanup function should not have been called when context is done")
104+
}

0 commit comments

Comments
 (0)