@@ -11,9 +11,10 @@ import (
1111 "io"
1212 "os"
1313 "os/exec"
14- "strconv "
14+ "os/signal "
1515 "strings"
1616 "sync"
17+ "syscall"
1718 "time"
1819
1920 "github.com/google/uuid"
@@ -88,6 +89,7 @@ const (
8889 debug_level_flag string = "log-level"
8990 integrationNameFlag string = "integration-name"
9091 maxNetworkRequestAttempts string = "max-attempts"
92+ teardownTimeout = 5 * time .Second
9193)
9294
9395type JsonErrorStruct struct {
@@ -221,71 +223,6 @@ func runWorkflowAndProcessData(engine workflow.Engine, logger *zerolog.Logger, n
221223 return err
222224}
223225
224- func sendAnalytics (analytics analytics.Analytics , debugLogger * zerolog.Logger ) {
225- debugLogger .Print ("Sending Analytics" )
226-
227- analytics .SetApiUrl (globalConfiguration .GetString (configuration .API_URL ))
228-
229- res , err := analytics .Send ()
230- if err != nil {
231- debugLogger .Err (err ).Msg ("Failed to send Analytics" )
232- return
233- }
234- defer func () { _ = res .Body .Close () }()
235-
236- successfullySend := 200 <= res .StatusCode && res .StatusCode < 300
237- if successfullySend {
238- debugLogger .Print ("Analytics successfully send" )
239- } else {
240- var details string
241- if res != nil {
242- details = res .Status
243- }
244-
245- debugLogger .Print ("Failed to send Analytics:" , details )
246- }
247- }
248-
249- func sendInstrumentation (eng workflow.Engine , instrumentor analytics.InstrumentationCollector , logger * zerolog.Logger ) {
250- // Avoid duplicate data to be sent for IDE integrations that use the CLI
251- if ! shallSendInstrumentation (eng .GetConfiguration (), instrumentor ) {
252- logger .Print ("This CLI call is not instrumented!" )
253- return
254- }
255-
256- // add temporary static nodejs binary flag, remove once linuxstatic is official
257- staticNodeJsBinaryBool , parseErr := strconv .ParseBool (constants .StaticNodeJsBinary )
258- if parseErr != nil {
259- logger .Print ("Failed to parse staticNodeJsBinary:" , parseErr )
260- } else {
261- // the legacycli:: prefix is added to maintain compatibility with our monitoring dashboard
262- instrumentor .AddExtension ("legacycli::static-nodejs-binary" , staticNodeJsBinaryBool )
263- }
264-
265- logger .Print ("Sending Instrumentation" )
266- data , err := analytics .GetV2InstrumentationObject (instrumentor , analytics .WithLogger (logger ))
267- if err != nil {
268- logger .Err (err ).Msg ("Failed to derive data object" )
269- }
270-
271- v2InstrumentationData := utils .ValueOf (json .Marshal (data ))
272- localConfiguration := globalConfiguration .Clone ()
273- // the report analytics workflow needs --experimental to run
274- // we pass the flag here so that we report at every interaction
275- localConfiguration .Set (configuration .FLAG_EXPERIMENTAL , true )
276- localConfiguration .Set ("inputData" , string (v2InstrumentationData ))
277- _ , err = eng .InvokeWithConfig (
278- localworkflows .WORKFLOWID_REPORT_ANALYTICS ,
279- localConfiguration ,
280- )
281-
282- if err != nil {
283- logger .Err (err ).Msg ("Failed to send Instrumentation" )
284- } else {
285- logger .Print ("Instrumentation successfully sent" )
286- }
287- }
288-
289226func help (_ * cobra.Command , _ []string ) error {
290227 helpProvided = true
291228 args := utils .RemoveSimilar (os .Args [1 :], "--" ) // remove all double dash arguments to avoid issues with the help command
@@ -548,11 +485,55 @@ func initExtensions(engine workflow.Engine, config configuration.Configuration)
548485 }
549486}
550487
488+ // tearDown handles sending analytics and instrumentation
489+ // It is used both for normal exit and signal-triggered exit
490+ func tearDown (ctx context.Context , err error , errorList []error , startTime time.Time , ua networking.UserAgentInfo , cliAnalytics analytics.Analytics , networkAccess networking.NetworkAccess ) int {
491+ // Create a context with timeout for teardown operations to ensure we don't hang indefinitely
492+ teardownCtx , cancel := context .WithTimeout (ctx , teardownTimeout )
493+ defer cancel ()
494+
495+ if err != nil {
496+ errorList , err = processError (err , errorList )
497+
498+ for _ , tempError := range errorList {
499+ if tempError != nil {
500+ cliAnalytics .AddError (tempError )
501+ }
502+ }
503+ }
504+
505+ exitCode := cliv2 .DeriveExitCode (err )
506+ globalLogger .Printf ("Deriving Exit Code %d (cause: %v)" , exitCode , err )
507+
508+ displayError (err , globalEngine .GetUserInterface (), globalConfiguration , teardownCtx )
509+
510+ updateInstrumentationDataBeforeSending (cliAnalytics , startTime , ua , exitCode )
511+
512+ if ! globalConfiguration .GetBool (configuration .ANALYTICS_DISABLED ) {
513+ sendAnalytics (teardownCtx , cliAnalytics , globalLogger )
514+ }
515+ sendInstrumentation (teardownCtx , globalEngine , cliAnalytics .GetInstrumentation (), globalLogger )
516+
517+ // cleanup resources in use
518+ // WARNING: deferred actions will execute AFTER cleanup; only defer if not impacted by this
519+ if _ , cleanupErr := globalEngine .Invoke (basic_workflows .WORKFLOWID_GLOBAL_CLEANUP , workflow .WithContext (teardownCtx )); cleanupErr != nil {
520+ globalLogger .Printf ("Failed to cleanup %v" , cleanupErr )
521+ }
522+
523+ if globalConfiguration .GetBool (configuration .DEBUG ) {
524+ writeLogFooter (exitCode , errorList , globalConfiguration , networkAccess )
525+ }
526+
527+ return exitCode
528+ }
529+
551530func MainWithErrorCode () int {
552531 initDebugBuild ()
553532
554533 errorList := []error {}
555534 errorListMutex := sync.Mutex {}
535+ var tearDownOnce sync.Once
536+ var finalExitCode int
556537
557538 startTime := time .Now ()
558539 var err error
@@ -656,6 +637,32 @@ func MainWithErrorCode() int {
656637 cliAnalytics .GetInstrumentation ().SetStage (instrumentation .DetermineStage (cliAnalytics .IsCiEnvironment ()))
657638 cliAnalytics .GetInstrumentation ().SetStatus (analytics .Success )
658639
640+ // prepare for signal handling
641+ signalChan := make (chan os.Signal , 1 )
642+ exitCodeChan := make (chan int , 1 )
643+
644+ if globalConfiguration .GetBool (configuration .PREVIEW_FEATURES_ENABLED ) {
645+ // Set up signal handling to send instrumentation on premature termination
646+ signal .Notify (signalChan , syscall .SIGINT , syscall .SIGTERM )
647+ go func () {
648+ sig := <- signalChan
649+ globalLogger .Printf ("Received signal %v, attempting to send instrumentation before exit" , sig )
650+
651+ tearDownOnce .Do (func () {
652+ signalError := cli .NewTerminatedBySignalError (fmt .Sprintf ("Signal: %v" , sig ))
653+
654+ errorListMutex .Lock ()
655+ errorListCopy := append ([]error {}, errorList ... )
656+ errorListMutex .Unlock ()
657+
658+ finalExitCode = tearDown (ctx , signalError , errorListCopy , startTime , ua , cliAnalytics , networkAccess )
659+ })
660+ // Send exit code to main goroutine instead of calling os.Exit directly
661+ // This allows deferred functions (like lock cleanup) to run
662+ exitCodeChan <- finalExitCode
663+ }()
664+ }
665+
659666 setTimeout (globalConfiguration , func () {
660667 os .Exit (constants .SNYK_EXIT_CODE_EX_UNAVAILABLE )
661668 })
@@ -681,40 +688,29 @@ func MainWithErrorCode() int {
681688 // ignore
682689 }
683690
684- if err != nil {
685- errorList , err = processError (err , errorList )
686-
687- for _ , tempError := range errorList {
688- if tempError != nil {
689- cliAnalytics .AddError (tempError )
690- }
691- }
691+ // Check if signal handler already ran teardown
692+ select {
693+ case code := <- exitCodeChan :
694+ // Signal was received and teardown completed - return its exit code
695+ return code
696+ default :
697+ // No signal received - run normal teardown
692698 }
693699
694- displayError (err , globalEngine .GetUserInterface (), globalConfiguration , ctx )
695-
696- exitCode := cliv2 .DeriveExitCode (err )
697- globalLogger .Printf ("Deriving Exit Code %d (cause: %v)" , exitCode , err )
698-
699- updateInstrumentationDataBeforeSending (cliAnalytics , startTime , ua , exitCode )
700-
701- if ! globalConfiguration .GetBool (configuration .ANALYTICS_DISABLED ) {
702- sendAnalytics (cliAnalytics , globalLogger )
700+ if globalConfiguration .GetBool (configuration .PREVIEW_FEATURES_ENABLED ) {
701+ // Stop signal handling before cleanup to prevent race conditions
702+ signal .Stop (signalChan )
703703 }
704- sendInstrumentation (globalEngine , cliAnalytics .GetInstrumentation (), globalLogger )
705704
706- // cleanup resources in use
707- // WARNING: deferred actions will execute AFTER cleanup; only defer if not impacted by this
708- _ , err = globalEngine .Invoke (basic_workflows .WORKFLOWID_GLOBAL_CLEANUP )
709- if err != nil {
710- globalLogger .Printf ("Failed to cleanup %v" , err )
711- }
705+ tearDownOnce .Do (func () {
706+ errorListMutex .Lock ()
707+ errorListCopy := append ([]error {}, errorList ... )
708+ errorListMutex .Unlock ()
712709
713- if debugEnabled {
714- writeLogFooter (exitCode , errorList , globalConfiguration , networkAccess )
715- }
710+ finalExitCode = tearDown (ctx , err , errorListCopy , startTime , ua , cliAnalytics , networkAccess )
711+ })
716712
717- return exitCode
713+ return finalExitCode
718714}
719715
720716func processError (err error , errorList []error ) ([]error , error ) {
0 commit comments