Skip to content

Commit 1999f8b

Browse files
committed
Refactor context handling
Stop using context.Background() and use a root context that's got signal.NotifyContext() on it. This allows control-C to interrupt everything. Use the main context for the MCP handler as well
1 parent 1e761ea commit 1999f8b

File tree

11 files changed

+288
-193
lines changed

11 files changed

+288
-193
lines changed

cmd/tiger/main.go

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,64 @@
11
package main
22

3-
import "github.com/timescale/tiger-cli/internal/tiger/cmd"
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"os"
8+
"os/signal"
9+
"syscall"
10+
11+
"go.uber.org/zap"
12+
13+
"github.com/timescale/tiger-cli/internal/tiger/cmd"
14+
"github.com/timescale/tiger-cli/internal/tiger/logging"
15+
)
416

517
func main() {
6-
cmd.Execute()
18+
if err := run(); err != nil {
19+
// Check if it's a custom exit code error
20+
if exitErr, ok := err.(interface{ ExitCode() int }); ok {
21+
os.Exit(exitErr.ExitCode())
22+
}
23+
os.Exit(1)
24+
}
25+
os.Exit(0)
26+
}
27+
28+
func run() (err error) {
29+
ctx, cancel := notifyContext(context.Background())
30+
defer func() {
31+
cancel()
32+
if r := recover(); r != nil {
33+
err = errors.Join(err, fmt.Errorf("panic: %v", r))
34+
_, _ = fmt.Fprintln(os.Stderr, err.Error())
35+
}
36+
}()
37+
err = cmd.Execute(ctx)
38+
return
39+
}
40+
41+
// noifyContext sets up graceful shutdown handling and returns a context and
42+
// cleanup function. This is nearly identical to [signal.NotifyContext], except
43+
// that it logs a message when a signal is received and also restores the default
44+
// signal handling behavior.
45+
func notifyContext(parent context.Context) (context.Context, context.CancelFunc) {
46+
ctx, cancel := context.WithCancel(parent)
47+
48+
sigChan := make(chan os.Signal, 1)
49+
signal.Notify(sigChan, os.Interrupt, syscall.SIGINT, syscall.SIGTERM)
50+
go func() {
51+
select {
52+
case sig := <-sigChan:
53+
logging.Info("Received interrupt signal", zap.Stringer("signal", sig))
54+
signal.Stop(sigChan) // Restore default signal handling behavior
55+
cancel()
56+
case <-ctx.Done():
57+
}
58+
}()
59+
60+
return ctx, func() {
61+
cancel()
62+
signal.Stop(sigChan)
63+
}
764
}

internal/tiger/cmd/auth_test.go

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cmd
22

33
import (
44
"bytes"
5+
"context"
56
"encoding/json"
67
"errors"
78
"fmt"
@@ -70,24 +71,27 @@ func setupAuthTest(t *testing.T) string {
7071
return tmpDir
7172
}
7273

73-
func executeAuthCommand(args ...string) (string, error) {
74+
func executeAuthCommand(ctx context.Context, args ...string) (string, error) {
7475
// Use buildRootCmd() to get a complete root command with all flags and subcommands
75-
testRoot := buildRootCmd()
76+
testRoot, err := buildRootCmd(ctx)
77+
if err != nil {
78+
return "", err
79+
}
7680

7781
buf := new(bytes.Buffer)
7882
testRoot.SetOut(buf)
7983
testRoot.SetErr(buf)
8084
testRoot.SetArgs(args)
8185

82-
err := testRoot.Execute()
86+
err = testRoot.Execute()
8387
return buf.String(), err
8488
}
8589

8690
func TestAuthLogin_KeyAndProjectIDFlags(t *testing.T) {
8791
setupAuthTest(t)
8892

8993
// Execute login command with public and secret key flags and project ID
90-
output, err := executeAuthCommand("auth", "login", "--public-key", "test-public-key", "--secret-key", "test-secret-key", "--project-id", "test-project-123")
94+
output, err := executeAuthCommand(t.Context(), "auth", "login", "--public-key", "test-public-key", "--secret-key", "test-secret-key", "--project-id", "test-project-123")
9195
if err != nil {
9296
t.Fatalf("Login failed: %v", err)
9397
}
@@ -120,7 +124,7 @@ func TestAuthLogin_KeyFlags_NoProjectID(t *testing.T) {
120124

121125
// Execute login command with only public and secret key flags (no project ID)
122126
// This should fail since project ID is now required
123-
_, err := executeAuthCommand("auth", "login", "--public-key", "test-public-key", "--secret-key", "test-secret-key")
127+
_, err := executeAuthCommand(t.Context(), "auth", "login", "--public-key", "test-public-key", "--secret-key", "test-secret-key")
124128
if err == nil {
125129
t.Fatal("Expected login to fail without project ID, but it succeeded")
126130
}
@@ -149,7 +153,7 @@ func TestAuthLogin_KeyAndProjectIDEnvironmentVariables(t *testing.T) {
149153
defer os.Unsetenv("TIGER_PROJECT_ID")
150154

151155
// Execute login command with project ID flag but using env vars for keys
152-
output, err := executeAuthCommand("auth", "login")
156+
output, err := executeAuthCommand(t.Context(), "auth", "login")
153157
if err != nil {
154158
t.Fatalf("Login failed: %v", err)
155159
}
@@ -184,7 +188,7 @@ func TestAuthLogin_KeyEnvironmentVariables_ProjectIDFlag(t *testing.T) {
184188
defer os.Unsetenv("TIGER_SECRET_KEY")
185189

186190
// Execute login command with project ID flag but using env vars for keys
187-
output, err := executeAuthCommand("auth", "login", "--project-id", "test-project-456")
191+
output, err := executeAuthCommand(t.Context(), "auth", "login", "--project-id", "test-project-456")
188192
if err != nil {
189193
t.Fatalf("Login failed: %v", err)
190194
}
@@ -434,7 +438,7 @@ func TestAuthLogin_OAuth_SingleProject(t *testing.T) {
434438
}, "project-123")
435439

436440
// Execute login command - the mocked openBrowser will handle the callback automatically
437-
output, err := executeAuthCommand("auth", "login")
441+
output, err := executeAuthCommand(t.Context(), "auth", "login")
438442

439443
if err != nil {
440444
t.Fatalf("Login failed: %v", err)
@@ -491,7 +495,7 @@ func TestAuthLogin_OAuth_MultipleProjects(t *testing.T) {
491495
}
492496

493497
// Execute login command - both mocked functions will handle OAuth flow and project selection
494-
output, err := executeAuthCommand("auth", "login")
498+
output, err := executeAuthCommand(t.Context(), "auth", "login")
495499

496500
if err != nil {
497501
t.Fatalf("Login failed: %v", err)
@@ -535,7 +539,7 @@ func TestAuthLogin_KeyringFallback(t *testing.T) {
535539
// by ensuring the API key gets stored to file when keyring might not be available
536540

537541
// Execute login command with public and secret key flags and project ID
538-
output, err := executeAuthCommand("auth", "login", "--public-key", "fallback-public", "--secret-key", "fallback-secret", "--project-id", "test-project-fallback")
542+
output, err := executeAuthCommand(t.Context(), "auth", "login", "--public-key", "fallback-public", "--secret-key", "fallback-secret", "--project-id", "test-project-fallback")
539543
if err != nil {
540544
t.Fatalf("Login failed: %v", err)
541545
}
@@ -571,7 +575,7 @@ func TestAuthLogin_KeyringFallback(t *testing.T) {
571575
}
572576

573577
// Test status with file-only storage
574-
output, err = executeAuthCommand("auth", "status")
578+
output, err = executeAuthCommand(t.Context(), "auth", "status")
575579
if err != nil {
576580
t.Fatalf("Status failed with file storage: %v", err)
577581
}
@@ -580,7 +584,7 @@ func TestAuthLogin_KeyringFallback(t *testing.T) {
580584
}
581585

582586
// Test logout with file-only storage
583-
output, err = executeAuthCommand("auth", "logout")
587+
output, err = executeAuthCommand(t.Context(), "auth", "logout")
584588
if err != nil {
585589
t.Fatalf("Logout failed with file storage: %v", err)
586590
}
@@ -607,7 +611,7 @@ func TestAuthLogin_EnvironmentVariable_FileOnly(t *testing.T) {
607611
defer os.Unsetenv("TIGER_PROJECT_ID")
608612

609613
// Execute login command without any flags (all from env vars)
610-
output, err := executeAuthCommand("auth", "login")
614+
output, err := executeAuthCommand(t.Context(), "auth", "login")
611615
if err != nil {
612616
t.Fatalf("Login failed: %v", err)
613617
}
@@ -652,7 +656,7 @@ func TestAuthStatus_LoggedIn(t *testing.T) {
652656
}
653657

654658
// Execute status command
655-
output, err := executeAuthCommand("auth", "status")
659+
output, err := executeAuthCommand(t.Context(), "auth", "status")
656660
if err != nil {
657661
t.Fatalf("Status failed: %v", err)
658662
}
@@ -666,7 +670,7 @@ func TestAuthStatus_NotLoggedIn(t *testing.T) {
666670
setupAuthTest(t)
667671

668672
// Execute status command without being logged in
669-
_, err := executeAuthCommand("auth", "status")
673+
_, err := executeAuthCommand(t.Context(), "auth", "status")
670674
if err == nil {
671675
t.Fatal("Expected status to fail when not logged in")
672676
}
@@ -693,7 +697,7 @@ func TestAuthLogout_Success(t *testing.T) {
693697
}
694698

695699
// Execute logout command
696-
output, err := executeAuthCommand("auth", "logout")
700+
output, err := executeAuthCommand(t.Context(), "auth", "logout")
697701
if err != nil {
698702
t.Fatalf("Logout failed: %v", err)
699703
}

internal/tiger/cmd/auth_validation_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@ func TestAuthLogin_APIKeyValidationFailure(t *testing.T) {
4242
defer config.RemoveCredentials()
4343

4444
// Execute login command with public and secret key flags - should fail validation
45-
output, err := executeAuthCommand("auth", "login", "--public-key", "invalid-public", "--secret-key", "invalid-secret", "--project-id", "test-project-invalid")
45+
output, err := executeAuthCommand(t.Context(), "auth", "login", "--public-key", "invalid-public", "--secret-key", "invalid-secret", "--project-id", "test-project-invalid")
4646
if err == nil {
4747
t.Fatal("Expected login to fail with invalid keys, but it succeeded")
4848
}
@@ -96,7 +96,7 @@ func TestAuthLogin_APIKeyValidationSuccess(t *testing.T) {
9696
defer config.RemoveCredentials()
9797

9898
// Execute login command with public and secret key flags - should succeed
99-
output, err := executeAuthCommand("auth", "login", "--public-key", "valid-public", "--secret-key", "valid-secret", "--project-id", "test-project-valid")
99+
output, err := executeAuthCommand(t.Context(), "auth", "login", "--public-key", "valid-public", "--secret-key", "valid-secret", "--project-id", "test-project-valid")
100100
if err != nil {
101101
t.Fatalf("Expected login to succeed with valid keys, got error: %v", err)
102102
}

0 commit comments

Comments
 (0)