@@ -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"
@@ -221,71 +222,6 @@ func runWorkflowAndProcessData(engine workflow.Engine, logger *zerolog.Logger, n
221222 return err
222223}
223224
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-
289225func help (_ * cobra.Command , _ []string ) error {
290226 helpProvided = true
291227 args := utils .RemoveSimilar (os .Args [1 :], "--" ) // remove all double dash arguments to avoid issues with the help command
@@ -548,11 +484,52 @@ func initExtensions(engine workflow.Engine, config configuration.Configuration)
548484 }
549485}
550486
487+ // tearDown handles sending analytics and instrumentation
488+ // It is used both for normal exit and signal-triggered exit
489+ func tearDown (ctx context.Context , err error , errorList []error , startTime time.Time , ua networking.UserAgentInfo , cliAnalytics analytics.Analytics , networkAccess networking.NetworkAccess ) int {
490+ if err != nil {
491+
492+ errorList , err = processError (err , errorList )
493+
494+ for _ , tempError := range errorList {
495+ if tempError != nil {
496+ cliAnalytics .AddError (tempError )
497+ }
498+ }
499+ }
500+
501+ exitCode := cliv2 .DeriveExitCode (err )
502+ globalLogger .Printf ("Deriving Exit Code %d (cause: %v)" , exitCode , err )
503+
504+ displayError (err , globalEngine .GetUserInterface (), globalConfiguration , ctx )
505+
506+ updateInstrumentationDataBeforeSending (cliAnalytics , startTime , ua , exitCode )
507+
508+ if ! globalConfiguration .GetBool (configuration .ANALYTICS_DISABLED ) {
509+ sendAnalytics (cliAnalytics , globalLogger )
510+ }
511+ sendInstrumentation (globalEngine , cliAnalytics .GetInstrumentation (), globalLogger )
512+
513+ // cleanup resources in use
514+ // WARNING: deferred actions will execute AFTER cleanup; only defer if not impacted by this
515+ if _ , cleanupErr := globalEngine .Invoke (basic_workflows .WORKFLOWID_GLOBAL_CLEANUP ); cleanupErr != nil {
516+ globalLogger .Printf ("Failed to cleanup %v" , cleanupErr )
517+ }
518+
519+ if globalConfiguration .GetBool (configuration .DEBUG ) {
520+ writeLogFooter (exitCode , errorList , globalConfiguration , networkAccess )
521+ }
522+
523+ return exitCode
524+ }
525+
551526func MainWithErrorCode () int {
552527 initDebugBuild ()
553528
554529 errorList := []error {}
555530 errorListMutex := sync.Mutex {}
531+ var tearDownOnce sync.Once
532+ var finalExitCode int
556533
557534 startTime := time .Now ()
558535 var err error
@@ -656,6 +633,27 @@ func MainWithErrorCode() int {
656633 cliAnalytics .GetInstrumentation ().SetStage (instrumentation .DetermineStage (cliAnalytics .IsCiEnvironment ()))
657634 cliAnalytics .GetInstrumentation ().SetStatus (analytics .Success )
658635
636+ // Set up signal handling to send instrumentation on premature termination
637+ signalChan := make (chan os.Signal , 1 )
638+ signal .Notify (signalChan , syscall .SIGINT , syscall .SIGTERM )
639+ go func () {
640+ sig := <- signalChan
641+ globalLogger .Printf ("Received signal %v, attempting to send instrumentation before exit" , sig )
642+
643+ // TODO: Replace with proper error catalog error for signal termination
644+ signalError := cli .NewGeneralCLIFailureError (fmt .Sprintf ("terminated by signal: %v" , sig ))
645+ signalError .ErrorCode = "SNYK-CLI-0025"
646+
647+ tearDownOnce .Do (func () {
648+ errorListMutex .Lock ()
649+ errorListCopy := append ([]error {}, errorList ... )
650+ errorListMutex .Unlock ()
651+
652+ finalExitCode = tearDown (ctx , signalError , errorListCopy , startTime , ua , cliAnalytics , networkAccess )
653+ })
654+ os .Exit (finalExitCode )
655+ }()
656+
659657 setTimeout (globalConfiguration , func () {
660658 os .Exit (constants .SNYK_EXIT_CODE_EX_UNAVAILABLE )
661659 })
@@ -681,40 +679,18 @@ func MainWithErrorCode() int {
681679 // ignore
682680 }
683681
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- }
692- }
693-
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 )
682+ // Stop signal handling before cleanup to prevent race conditions
683+ signal .Stop (signalChan )
700684
701- if ! globalConfiguration .GetBool (configuration .ANALYTICS_DISABLED ) {
702- sendAnalytics (cliAnalytics , globalLogger )
703- }
704- sendInstrumentation (globalEngine , cliAnalytics .GetInstrumentation (), globalLogger )
705-
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- }
685+ tearDownOnce .Do (func () {
686+ errorListMutex .Lock ()
687+ errorListCopy := append ([]error {}, errorList ... )
688+ errorListMutex .Unlock ()
712689
713- if debugEnabled {
714- writeLogFooter (exitCode , errorList , globalConfiguration , networkAccess )
715- }
690+ finalExitCode = tearDown (ctx , err , errorListCopy , startTime , ua , cliAnalytics , networkAccess )
691+ })
716692
717- return exitCode
693+ return finalExitCode
718694}
719695
720696func processError (err error , errorList []error ) ([]error , error ) {
0 commit comments