-
-
Notifications
You must be signed in to change notification settings - Fork 174
Expand file tree
/
Copy pathmain.go
More file actions
169 lines (151 loc) · 6.57 KB
/
Copy pathmain.go
File metadata and controls
169 lines (151 loc) · 6.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
package main
import (
"errors"
"os"
"os/signal"
"strings"
"syscall"
"github.com/cloudposse/atmos/cmd"
errUtils "github.com/cloudposse/atmos/errors"
ioLayer "github.com/cloudposse/atmos/pkg/io"
log "github.com/cloudposse/atmos/pkg/logger"
"github.com/cloudposse/atmos/pkg/panics"
"github.com/cloudposse/atmos/pkg/signals"
)
func main() {
// Set up signal handling for graceful shutdown.
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
go func() {
for sig := range sigChan {
// While an interactive/TTY step owns the terminal, SIGINT belongs to
// the foreground child process - keep waiting instead of exiting.
if !shouldExitOnSignal(sig) {
continue
}
// Run registered exit cleanups (e.g. restore the terminal from raw
// mode) - os.Exit below skips deferred functions.
signals.RunExitCleanups()
// Clean up resources before exit.
cmd.Cleanup()
// Exit with correct POSIX exit code (128 + signal number).
// Use errUtils.OsExit to allow test interception (Go 1.25+ panics on os.Exit in tests).
if s, ok := sig.(syscall.Signal); ok {
errUtils.OsExit(128 + int(s))
}
// Fallback to SIGINT exit code if signal type assertion fails.
errUtils.OsExit(130)
}
}()
// Disable timestamp in logs so snapshots work. We will address this in a future PR updating styles, etc.
log.Default().SetReportTimestamp(false)
// Run the application and exit with the appropriate code.
// Use errUtils.OsExit to allow test interception (Go 1.25+ panics on os.Exit in tests).
errUtils.OsExit(run())
}
// run executes the main application logic and returns an exit code.
// This separation allows proper cleanup via defer before os.Exit in main().
func run() (exitCode int) {
// Install the global panic handler first so any subsequent panic
// (including inside cmd.Cleanup via the defer below) is turned
// into a friendly message + crash report. Order matters: the
// panic handler must be deferred BEFORE cmd.Cleanup so Go unwinds
// defers in LIFO order — Cleanup runs first, then Recover catches
// anything that escapes either Cleanup or the main call chain.
defer panics.Recover(&exitCode)
// Ensure cleanup happens on normal exit.
defer cmd.Cleanup()
// A generated toolchain proxy invokes this executable under a command name
// such as "ls". Dispatch it before generic flag handling so "ls --version"
// reaches the proxied tool rather than Atmos's --version handler.
if handled, err := cmd.TryRunToolchainProxy(os.Args); handled {
if err != nil {
if code, ok := silentExitCode(err); ok {
return code
}
errUtils.CaptureError(err)
formatted := errUtils.Format(err, errUtils.DefaultFormatterConfig())
_, _ = ioLayer.MaskWriter(os.Stderr).Write([]byte(strings.TrimRight(formatted, "\n") + "\n"))
return errUtils.GetExitCode(err)
}
return 0
}
// Handle --version flag at application entry point to avoid deep exit in command infrastructure.
// This eliminates the need for os.Exit in PersistentPreRun, making tests work with Go 1.25.
// Check os.Args directly since we're in main() (tests call cmd.Execute() directly).
// Note: Only intercept --version flag here. The "version" subcommand should go through
// normal Cobra flow to ensure PersistentPreRun executes (needed for proper logging setup).
if hasVersionFlag(os.Args) {
// Check for conflicting flags: --version and --use-version cannot be used together.
if hasUseVersionFlag(os.Args) {
// Print error directly since config/formatters aren't initialized yet.
// Use MaskWriter for consistent masking (gracefully falls back if not initialized).
maskedStderr := ioLayer.MaskWriter(os.Stderr)
_, _ = maskedStderr.Write([]byte("\nError: --version and --use-version cannot be used together\n\n"))
_, _ = maskedStderr.Write([]byte("Hints:\n"))
_, _ = maskedStderr.Write([]byte(" - Use --version to display the current Atmos version\n"))
_, _ = maskedStderr.Write([]byte(" - Use --use-version to run a command with a specific Atmos version\n\n"))
return 1
}
err := cmd.ExecuteVersion()
if err != nil {
errUtils.CaptureError(err)
formatted := errUtils.Format(err, errUtils.DefaultFormatterConfig())
_, _ = ioLayer.MaskWriter(os.Stderr).Write([]byte(strings.TrimRight(formatted, "\n") + "\n"))
return errUtils.GetExitCode(err)
}
return 0 // Exit normally after printing version.
}
err := cmd.Execute()
if err != nil {
// Silent exit-code carriers (terminal-handoff steps) propagate the
// child's code without themed rendering, which would query the
// terminal and can hang when stdin is still contended by the session.
if code, ok := silentExitCode(err); ok {
return code
}
// Capture error to Sentry if configured (safe to call even if Sentry not initialized).
errUtils.CaptureError(err)
// Format and print error using centralized formatter.
formatted := errUtils.Format(err, errUtils.DefaultFormatterConfig())
_, _ = ioLayer.MaskWriter(os.Stderr).Write([]byte(strings.TrimRight(formatted, "\n") + "\n"))
// Extract and use the correct exit code.
exitCode := errUtils.GetExitCode(err)
log.Debug("Exiting with exit code", "code", exitCode)
return exitCode
}
return 0
}
// silentExitCode reports the exit code to use when err is a silent exit-code
// carrier (a terminal-handoff step that exited non-zero). Such errors must
// propagate the code without themed rendering, which would query the terminal
// and can hang when the session still contends for stdin.
func silentExitCode(err error) (int, bool) {
var exitCodeErr errUtils.ExitCodeError
if errors.As(err, &exitCodeErr) && exitCodeErr.Silent {
return exitCodeErr.Code, true
}
return 0, false
}
// shouldExitOnSignal reports whether the process should exit in response to sig.
// SIGINT is ignored while a foreground interactive/TTY step owns the terminal
// (the child process handles Ctrl-C). SIGTERM always exits as an escape hatch.
func shouldExitOnSignal(sig os.Signal) bool {
return sig != os.Interrupt || !signals.InterruptExitSuspended()
}
// hasVersionFlag checks if --version flag is present in args.
// Only checks for --version as the first argument after the program name
// to catch the simple "atmos --version" case for early exit; other flag
// combinations go through normal Cobra processing.
func hasVersionFlag(args []string) bool {
return len(args) > 1 && args[1] == "--version"
}
// hasUseVersionFlag checks if --use-version flag is present in args.
func hasUseVersionFlag(args []string) bool {
for _, arg := range args {
if arg == "--use-version" || strings.HasPrefix(arg, "--use-version=") {
return true
}
}
return false
}