@@ -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"
@@ -75,6 +76,7 @@ import (
7576var internalOS string
7677var globalEngine workflow.Engine
7778var globalConfiguration configuration.Configuration
79+ var globalContext context.Context
7880var helpProvided bool
7981
8082var noopLogger zerolog.Logger = zerolog .New (io .Discard )
@@ -88,6 +90,7 @@ const (
8890 debug_level_flag string = "log-level"
8991 integrationNameFlag string = "integration-name"
9092 maxNetworkRequestAttempts string = "max-attempts"
93+ teardownTimeout = 5 * time .Second
9194)
9295
9396type JsonErrorStruct struct {
@@ -194,98 +197,33 @@ func runMainWorkflow(config configuration.Configuration, cmd *cobra.Command, arg
194197 globalLogger .Print ("Running " , name )
195198 globalEngine .GetAnalytics ().SetCommand (name )
196199
197- err = runWorkflowAndProcessData (globalEngine , globalLogger , name )
200+ err = runWorkflowAndProcessData (globalContext , globalEngine , globalLogger , name )
198201
199202 return err
200203}
201204
202- func runWorkflowAndProcessData (engine workflow.Engine , logger * zerolog.Logger , name string ) error {
205+ func runWorkflowAndProcessData (ctx context. Context , engine workflow.Engine , logger * zerolog.Logger , name string ) error {
203206 ic := engine .GetAnalytics ().GetInstrumentation ()
204207
205- output , err := engine .Invoke (workflow .NewWorkflowIdentifier (name ), workflow .WithInstrumentationCollector (ic ))
208+ output , err := engine .Invoke (workflow .NewWorkflowIdentifier (name ), workflow .WithContext ( ctx ), workflow . WithInstrumentationCollector (ic ))
206209 if err != nil {
207210 logger .Print ("Failed to execute the command! " , err )
208211 return err
209212 }
210213
211- outputFiltered , err := engine .Invoke (localworkflows .WORKFLOWID_FILTER_FINDINGS , workflow .WithInput (output ), workflow .WithInstrumentationCollector (ic ))
214+ outputFiltered , err := engine .Invoke (localworkflows .WORKFLOWID_FILTER_FINDINGS , workflow .WithContext ( ctx ), workflow . WithInput (output ), workflow .WithInstrumentationCollector (ic ))
212215 if err != nil {
213216 logger .Err (err ).Msg (err .Error ())
214217 return err
215218 }
216219
217- _ , err = engine .Invoke (localworkflows .WORKFLOWID_OUTPUT_WORKFLOW , workflow .WithInput (outputFiltered ), workflow .WithInstrumentationCollector (ic ))
220+ _ , err = engine .Invoke (localworkflows .WORKFLOWID_OUTPUT_WORKFLOW , workflow .WithContext ( ctx ), workflow . WithInput (outputFiltered ), workflow .WithInstrumentationCollector (ic ))
218221 if err == nil {
219222 err = getErrorFromWorkFlowData (engine , outputFiltered )
220223 }
221224 return err
222225}
223226
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-
289227func help (_ * cobra.Command , _ []string ) error {
290228 helpProvided = true
291229 args := utils .RemoveSimilar (os .Args [1 :], "--" ) // remove all double dash arguments to avoid issues with the help command
@@ -548,11 +486,55 @@ func initExtensions(engine workflow.Engine, config configuration.Configuration)
548486 }
549487}
550488
489+ // tearDown handles sending analytics and instrumentation
490+ // It is used both for normal exit and signal-triggered exit
491+ func tearDown (ctx context.Context , err error , errorList []error , startTime time.Time , ua networking.UserAgentInfo , cliAnalytics analytics.Analytics , networkAccess networking.NetworkAccess ) int {
492+ // Create a context with timeout for teardown operations to ensure we don't hang indefinitely
493+ teardownCtx , cancel := context .WithTimeout (ctx , teardownTimeout )
494+ defer cancel ()
495+
496+ if err != nil {
497+ errorList , err = processError (err , errorList )
498+
499+ for _ , tempError := range errorList {
500+ if tempError != nil {
501+ cliAnalytics .AddError (tempError )
502+ }
503+ }
504+ }
505+
506+ exitCode := cliv2 .DeriveExitCode (err )
507+ globalLogger .Printf ("Deriving Exit Code %d (cause: %v)" , exitCode , err )
508+
509+ displayError (err , globalEngine .GetUserInterface (), globalConfiguration , teardownCtx )
510+
511+ updateInstrumentationDataBeforeSending (cliAnalytics , startTime , ua , exitCode )
512+
513+ if ! globalConfiguration .GetBool (configuration .ANALYTICS_DISABLED ) {
514+ sendAnalytics (teardownCtx , cliAnalytics , globalLogger )
515+ }
516+ sendInstrumentation (teardownCtx , globalEngine , cliAnalytics .GetInstrumentation (), globalLogger )
517+
518+ // cleanup resources in use
519+ // WARNING: deferred actions will execute AFTER cleanup; only defer if not impacted by this
520+ if _ , cleanupErr := globalEngine .Invoke (basic_workflows .WORKFLOWID_GLOBAL_CLEANUP , workflow .WithContext (teardownCtx )); cleanupErr != nil {
521+ globalLogger .Printf ("Failed to cleanup %v" , cleanupErr )
522+ }
523+
524+ if globalConfiguration .GetBool (configuration .DEBUG ) {
525+ writeLogFooter (exitCode , errorList , globalConfiguration , networkAccess )
526+ }
527+
528+ return exitCode
529+ }
530+
551531func MainWithErrorCode () int {
552532 initDebugBuild ()
553533
554534 errorList := []error {}
555535 errorListMutex := sync.Mutex {}
536+ var tearDownOnce sync.Once
537+ var finalExitCode int
556538
557539 startTime := time .Now ()
558540 var err error
@@ -633,9 +615,11 @@ func MainWithErrorCode() int {
633615 return constants .SNYK_EXIT_CODE_ERROR
634616 }
635617
636- // init context
637- ctx := context .Background ()
618+ // init context with cancel function for signal handling
619+ ctx , ctxCancel := context .WithCancel (context .Background ())
620+ defer ctxCancel () // ensure context is canceled on exit
638621 ctx = context .WithValue (ctx , networking .InteractionIdKey , instrumentation .AssembleUrnFromUUID (interactionId ))
622+ globalContext = ctx
639623
640624 // add output flags as persistent flags
641625 outputWorkflow , _ := globalEngine .GetWorkflow (localworkflows .WORKFLOWID_OUTPUT_WORKFLOW )
@@ -656,6 +640,35 @@ func MainWithErrorCode() int {
656640 cliAnalytics .GetInstrumentation ().SetStage (instrumentation .DetermineStage (cliAnalytics .IsCiEnvironment ()))
657641 cliAnalytics .GetInstrumentation ().SetStatus (analytics .Success )
658642
643+ // prepare for signal handling
644+ signalChan := make (chan os.Signal , 1 )
645+ exitCodeChan := make (chan int , 1 )
646+
647+ if globalConfiguration .GetBool (configuration .PREVIEW_FEATURES_ENABLED ) {
648+ // Set up signal handling to send instrumentation on premature termination
649+ signal .Notify (signalChan , syscall .SIGINT , syscall .SIGTERM )
650+ go func () {
651+ sig := <- signalChan
652+ globalLogger .Printf ("Received signal %v, attempting to send instrumentation before exit" , sig )
653+
654+ // Cancel the context to terminate any running child processes
655+ ctxCancel ()
656+
657+ tearDownOnce .Do (func () {
658+ signalError := cli .NewTerminatedBySignalError (fmt .Sprintf ("Signal: %v" , sig ))
659+
660+ errorListMutex .Lock ()
661+ errorListCopy := append ([]error {}, errorList ... )
662+ errorListMutex .Unlock ()
663+
664+ finalExitCode = tearDown (ctx , signalError , errorListCopy , startTime , ua , cliAnalytics , networkAccess )
665+ })
666+ // Send exit code to main goroutine instead of calling os.Exit directly
667+ // This allows deferred functions (like lock cleanup) to run
668+ exitCodeChan <- finalExitCode
669+ }()
670+ }
671+
659672 setTimeout (globalConfiguration , func () {
660673 os .Exit (constants .SNYK_EXIT_CODE_EX_UNAVAILABLE )
661674 })
@@ -681,40 +694,29 @@ func MainWithErrorCode() int {
681694 // ignore
682695 }
683696
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- }
697+ // Check if signal handler already ran teardown
698+ select {
699+ case code := <- exitCodeChan :
700+ // Signal was received and teardown completed - return its exit code
701+ return code
702+ default :
703+ // No signal received - run normal teardown
692704 }
693705
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 )
706+ if globalConfiguration .GetBool (configuration .PREVIEW_FEATURES_ENABLED ) {
707+ // Stop signal handling before cleanup to prevent race conditions
708+ signal .Stop (signalChan )
703709 }
704- sendInstrumentation (globalEngine , cliAnalytics .GetInstrumentation (), globalLogger )
705710
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- }
711+ tearDownOnce .Do (func () {
712+ errorListMutex .Lock ()
713+ errorListCopy := append ([]error {}, errorList ... )
714+ errorListMutex .Unlock ()
712715
713- if debugEnabled {
714- writeLogFooter (exitCode , errorList , globalConfiguration , networkAccess )
715- }
716+ finalExitCode = tearDown (ctx , err , errorListCopy , startTime , ua , cliAnalytics , networkAccess )
717+ })
716718
717- return exitCode
719+ return finalExitCode
718720}
719721
720722func processError (err error , errorList []error ) ([]error , error ) {
0 commit comments