Skip to content

Commit f6d81c4

Browse files
feat: add preflight checks before repository creation prompts
- Added git configuration validation before interactive prompts to fail fast - Integrated privilege check to warn users about unnecessary sudo usage - Improved user experience by preventing wasted time on prompts when git is misconfigured
1 parent 54a3c81 commit f6d81c4

10 files changed

Lines changed: 3892 additions & 0 deletions

File tree

TECHNICAL_SUMMARY_2025-01-28.md

Lines changed: 1212 additions & 0 deletions
Large diffs are not rendered by default.

cmd/create/repo.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66

77
eos "github.com/CodeMonkeyCybersecurity/eos/pkg/eos_cli"
88
"github.com/CodeMonkeyCybersecurity/eos/pkg/eos_io"
9+
"github.com/CodeMonkeyCybersecurity/eos/pkg/git"
910
"github.com/CodeMonkeyCybersecurity/eos/pkg/repository"
1011
"github.com/spf13/cobra"
1112
"github.com/uptrace/opentelemetry-go-extra/otelzap"
@@ -48,6 +49,21 @@ func init() {
4849
func runCreateRepo(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []string) error {
4950
logger := otelzap.Ctx(rc.Ctx)
5051

52+
// CRITICAL P0: Preflight checks BEFORE interactive prompts (fail-fast principle)
53+
// This prevents wasting user time on 5+ prompts only to fail on missing git config
54+
55+
// Step 1: Check sudo usage and warn if unnecessary
56+
eos.CheckAndWarnPrivileges(rc.Ctx, "git", false)
57+
58+
// Step 2: Run git preflight checks
59+
logger.Info("Running preflight checks for git repository creation")
60+
gitPreflightConfig := git.DefaultGitPreflightConfig()
61+
if err := git.RunGitPreflightChecks(rc.Ctx, gitPreflightConfig); err != nil {
62+
return fmt.Errorf("preflight check failed: %w\n\n"+
63+
"Eos checks your environment BEFORE asking questions to avoid wasting your time.\n"+
64+
"Please fix the issue above and try again.", err)
65+
}
66+
5167
path := "."
5268
if len(args) > 0 {
5369
path = args[0]

pkg/eos_cli/signals.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// pkg/eos_cli/signals.go
2+
//
3+
// Signal handling and graceful shutdown for EOS operations
4+
// Implements proper cleanup on Ctrl-C and process termination
5+
6+
package eos_cli
7+
8+
import (
9+
"context"
10+
"fmt"
11+
"os"
12+
"os/signal"
13+
"syscall"
14+
"time"
15+
16+
"github.com/uptrace/opentelemetry-go-extra/otelzap"
17+
"go.uber.org/zap"
18+
)
19+
20+
// CleanupFunc is a function that performs cleanup operations
21+
type CleanupFunc func() error
22+
23+
// SignalHandler manages graceful shutdown on signals
24+
type SignalHandler struct {
25+
ctx context.Context
26+
cancel context.CancelFunc
27+
cleanupFuncs []CleanupFunc
28+
sigChan chan os.Signal
29+
doneChan chan struct{}
30+
}
31+
32+
// NewSignalHandler creates a new signal handler
33+
func NewSignalHandler(ctx context.Context) *SignalHandler {
34+
ctx, cancel := context.WithCancel(ctx)
35+
36+
handler := &SignalHandler{
37+
ctx: ctx,
38+
cancel: cancel,
39+
cleanupFuncs: make([]CleanupFunc, 0),
40+
sigChan: make(chan os.Signal, 1),
41+
doneChan: make(chan struct{}),
42+
}
43+
44+
// Notify on SIGINT (Ctrl-C) and SIGTERM
45+
signal.Notify(handler.sigChan, os.Interrupt, syscall.SIGTERM)
46+
47+
// Start signal handling goroutine
48+
go handler.handleSignals()
49+
50+
return handler
51+
}
52+
53+
// RegisterCleanup adds a cleanup function to be called on shutdown
54+
// Cleanup functions are called in REVERSE order (LIFO)
55+
func (h *SignalHandler) RegisterCleanup(cleanup CleanupFunc) {
56+
h.cleanupFuncs = append(h.cleanupFuncs, cleanup)
57+
}
58+
59+
// Context returns the cancellable context
60+
// Operations should use this context to detect cancellation
61+
func (h *SignalHandler) Context() context.Context {
62+
return h.ctx
63+
}
64+
65+
// handleSignals waits for signals and initiates cleanup
66+
func (h *SignalHandler) handleSignals() {
67+
logger := otelzap.Ctx(h.ctx)
68+
69+
select {
70+
case sig := <-h.sigChan:
71+
logger.Info("Received signal, initiating cleanup",
72+
zap.String("signal", sig.String()))
73+
74+
fmt.Fprintf(os.Stderr, "\n\n⚠️ Received %v, cleaning up...\n", sig)
75+
76+
// Cancel context to stop ongoing operations
77+
h.cancel()
78+
79+
// Perform cleanup with timeout
80+
if err := h.runCleanup(); err != nil {
81+
fmt.Fprintf(os.Stderr, "Cleanup completed with errors: %v\n", err)
82+
os.Exit(1)
83+
}
84+
85+
fmt.Fprintln(os.Stderr, "✓ Cleanup complete")
86+
os.Exit(130) // Standard exit code for SIGINT
87+
88+
case sig := <-h.sigChan:
89+
// Second signal - force exit
90+
logger.Error("Received second signal, forcing exit",
91+
zap.String("signal", sig.String()))
92+
93+
fmt.Fprintln(os.Stderr, "\n⚠️ Received second interrupt, forcing exit!")
94+
os.Exit(1)
95+
}
96+
}
97+
98+
// runCleanup executes all cleanup functions with a timeout
99+
func (h *SignalHandler) runCleanup() error {
100+
logger := otelzap.Ctx(h.ctx)
101+
102+
// Create cleanup context with timeout
103+
cleanupCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
104+
defer cancel()
105+
106+
// Channel for cleanup completion
107+
done := make(chan error, 1)
108+
109+
go func() {
110+
// Execute cleanup functions in reverse order (LIFO)
111+
var lastErr error
112+
for i := len(h.cleanupFuncs) - 1; i >= 0; i-- {
113+
cleanup := h.cleanupFuncs[i]
114+
if err := cleanup(); err != nil {
115+
logger.Warn("Cleanup function failed",
116+
zap.Int("index", i),
117+
zap.Error(err))
118+
lastErr = err
119+
}
120+
}
121+
done <- lastErr
122+
}()
123+
124+
// Wait for cleanup or timeout
125+
select {
126+
case err := <-done:
127+
return err
128+
case <-cleanupCtx.Done():
129+
logger.Error("Cleanup timed out after 5 seconds")
130+
return fmt.Errorf("cleanup timed out")
131+
}
132+
}
133+
134+
// Stop gracefully stops the signal handler
135+
// Should be called at the end of successful operations
136+
func (h *SignalHandler) Stop() {
137+
signal.Stop(h.sigChan)
138+
close(h.sigChan)
139+
close(h.doneChan)
140+
}
141+
142+
// WithCleanup is a helper to execute an operation with automatic cleanup
143+
// Example:
144+
//
145+
// err := eos_cli.WithCleanup(ctx, func() error {
146+
// return performOperation()
147+
// }, cleanupFunc1, cleanupFunc2)
148+
func WithCleanup(ctx context.Context, operation func() error, cleanupFuncs ...CleanupFunc) error {
149+
handler := NewSignalHandler(ctx)
150+
defer handler.Stop()
151+
152+
// Register cleanup functions
153+
for _, cleanup := range cleanupFuncs {
154+
handler.RegisterCleanup(cleanup)
155+
}
156+
157+
// Execute operation with cancellable context
158+
return operation()
159+
}
160+
161+
// OperationState represents the state of an ongoing operation
162+
// Used for recovery after crashes
163+
type OperationState struct {
164+
Operation string `json:"operation"`
165+
StartTime time.Time `json:"start_time"`
166+
Path string `json:"path,omitempty"`
167+
PID int `json:"pid"`
168+
Completed bool `json:"completed"`
169+
}
170+
171+
// SaveOperationState writes operation state to a recovery file
172+
// This allows detecting and cleaning up incomplete operations after crashes
173+
func SaveOperationState(state OperationState) error {
174+
// TODO: Implement state persistence if needed
175+
// For now, we rely on lock files for concurrent operation detection
176+
return nil
177+
}
178+
179+
// CheckForIncompleteOperations looks for operations that didn't complete
180+
// Should be called at startup to offer recovery/cleanup
181+
func CheckForIncompleteOperations() ([]OperationState, error) {
182+
// TODO: Implement if state persistence is added
183+
return nil, nil
184+
}
185+
186+
// Example usage pattern:
187+
//
188+
// func runCreateRepo(rc *eos_io.RuntimeContext, cmd *cobra.Command, args []string) error {
189+
// handler := eos_cli.NewSignalHandler(rc.Ctx)
190+
// defer handler.Stop()
191+
//
192+
// // Register cleanup for partial operations
193+
// var lockCleanup func()
194+
// handler.RegisterCleanup(func() error {
195+
// if lockCleanup != nil {
196+
// lockCleanup()
197+
// }
198+
// return nil
199+
// })
200+
//
201+
// // Acquire lock
202+
// var err error
203+
// lockCleanup, err = git.AcquireRepositoryLock(handler.Context(), path)
204+
// if err != nil {
205+
// return err
206+
// }
207+
//
208+
// // Perform operation using handler.Context()
209+
// return performOperation(handler.Context())
210+
// }

0 commit comments

Comments
 (0)