Skip to content

Commit 1686db5

Browse files
fix: add timeout handling to legacy cli (#4950)
* fix: use deadline context for timeout in legacy workflow * fix: tests & refactor error handling for deadline * fix: display error better * chore: use context.WithTimeout Co-authored-by: Casey Marshall <[email protected]> * chore: assert error is nil Co-authored-by: Casey Marshall <[email protected]> * chore: implement pr suggestions * fix: test --------- Co-authored-by: Casey Marshall <[email protected]>
1 parent a0308f1 commit 1686db5

File tree

4 files changed

+96
-36
lines changed

4 files changed

+96
-36
lines changed

cliv2/cmd/cliv2/main.go

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ package main
44
import _ "github.com/snyk/go-application-framework/pkg/networking/fips_enable"
55

66
import (
7+
"context"
78
"encoding/json"
9+
"errors"
810
"fmt"
911
"io"
1012
"os"
@@ -13,6 +15,9 @@ import (
1315
"time"
1416

1517
"github.com/rs/zerolog"
18+
"github.com/spf13/cobra"
19+
"github.com/spf13/pflag"
20+
1621
"github.com/snyk/cli-extension-dep-graph/pkg/depgraph"
1722
"github.com/snyk/cli-extension-iac-rules/iacrules"
1823
"github.com/snyk/cli-extension-sbom/pkg/sbom"
@@ -24,18 +29,14 @@ import (
2429
"github.com/snyk/go-application-framework/pkg/app"
2530
"github.com/snyk/go-application-framework/pkg/auth"
2631
"github.com/snyk/go-application-framework/pkg/configuration"
27-
2832
localworkflows "github.com/snyk/go-application-framework/pkg/local_workflows"
2933
"github.com/snyk/go-application-framework/pkg/networking"
3034
"github.com/snyk/go-application-framework/pkg/runtimeinfo"
3135
"github.com/snyk/go-application-framework/pkg/utils"
3236
"github.com/snyk/go-application-framework/pkg/workflow"
3337
"github.com/snyk/go-httpauth/pkg/httpauth"
3438
"github.com/snyk/snyk-iac-capture/pkg/capture"
35-
3639
snykls "github.com/snyk/snyk-ls/ls_extension"
37-
"github.com/spf13/cobra"
38-
"github.com/spf13/pflag"
3940
)
4041

4142
var internalOS string
@@ -355,7 +356,8 @@ func handleError(err error) HandleError {
355356

356357
func displayError(err error) {
357358
if err != nil {
358-
if _, ok := err.(*exec.ExitError); !ok {
359+
var exitError *exec.ExitError
360+
if !errors.As(err, &exitError) {
359361
if globalConfiguration.GetBool(localworkflows.OUTPUT_CONFIG_KEY_JSON) {
360362
jsonError := JsonErrorStruct{
361363
Ok: false,
@@ -366,7 +368,11 @@ func displayError(err error) {
366368
jsonErrorBuffer, _ := json.MarshalIndent(jsonError, "", " ")
367369
fmt.Println(string(jsonErrorBuffer))
368370
} else {
369-
fmt.Println(err)
371+
if errors.Is(err, context.DeadlineExceeded) {
372+
fmt.Println("command timed out")
373+
} else {
374+
fmt.Println(err)
375+
}
370376
}
371377
}
372378
}
@@ -479,8 +485,9 @@ func setTimeout(config configuration.Configuration, onTimeout func()) {
479485
}
480486
debugLogger.Printf("Command timeout set for %d seconds", timeout)
481487
go func() {
482-
<-time.After(time.Duration(timeout) * time.Second)
483-
fmt.Fprintf(os.Stderr, "command timed out\n")
488+
const gracePeriodForSubProcesses = 3
489+
<-time.After(time.Duration(timeout+gracePeriodForSubProcesses) * time.Second)
490+
fmt.Fprintf(os.Stdout, "command timed out")
484491
onTimeout()
485492
}()
486493
}

cliv2/cmd/cliv2/main_test.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,13 @@ import (
55
"testing"
66
"time"
77

8-
"github.com/snyk/go-application-framework/pkg/configuration"
9-
localworkflows "github.com/snyk/go-application-framework/pkg/local_workflows"
10-
"github.com/snyk/go-application-framework/pkg/workflow"
118
"github.com/spf13/cobra"
129
"github.com/spf13/pflag"
1310
"github.com/stretchr/testify/assert"
11+
12+
"github.com/snyk/go-application-framework/pkg/configuration"
13+
localworkflows "github.com/snyk/go-application-framework/pkg/local_workflows"
14+
"github.com/snyk/go-application-framework/pkg/workflow"
1415
)
1516

1617
func cleanup() {

cliv2/internal/cliv2/cliv2.go

Lines changed: 34 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ Entry point class for the CLIv2 version.
44
package cliv2
55

66
import (
7+
"context"
78
_ "embed"
9+
"errors"
810
"fmt"
911
"io"
1012
"log"
@@ -13,6 +15,7 @@ import (
1315
"path"
1416
"regexp"
1517
"strings"
18+
"time"
1619

1720
"github.com/gofrs/flock"
1821
"github.com/snyk/go-application-framework/pkg/configuration"
@@ -74,6 +77,11 @@ func NewCLIv2(config configuration.Configuration, debugLogger *log.Logger) (*CLI
7477
return &cli, nil
7578
}
7679

80+
// SetV1BinaryLocation for testing purposes
81+
func (c *CLI) SetV1BinaryLocation(filePath string) {
82+
c.v1BinaryLocation = filePath
83+
}
84+
7785
func (c *CLI) Init() (err error) {
7886
c.DebugLogger.Println("Init start")
7987

@@ -200,7 +208,7 @@ func (c *CLI) GetBinaryLocation() string {
200208
}
201209

202210
func (c *CLI) printVersion() {
203-
fmt.Fprintln(c.stdout, GetFullVersion())
211+
_, _ = fmt.Fprintln(c.stdout, GetFullVersion())
204212
}
205213

206214
func (c *CLI) commandVersion(passthroughArgs []string) error {
@@ -235,8 +243,8 @@ func (c *CLI) commandAbout(proxyInfo *proxy.ProxyInfo, passthroughArgs []string)
235243
}
236244

237245
fmt.Printf("Package: %s \n", strings.ReplaceAll(strings.ReplaceAll(fPath, "/licenses/", ""), "/"+f.Name(), ""))
238-
fmt.Fprintln(c.stdout, string(data))
239-
fmt.Fprint(c.stdout, separator)
246+
_, _ = fmt.Fprintln(c.stdout, string(data))
247+
_, _ = fmt.Fprint(c.stdout, separator)
240248
}
241249
}
242250

@@ -341,15 +349,15 @@ func PrepareV1EnvironmentVariables(
341349
}
342350

343351
func (c *CLI) PrepareV1Command(
352+
ctx context.Context,
344353
cmd string,
345354
args []string,
346355
proxyInfo *proxy.ProxyInfo,
347356
integrationName string,
348357
integrationVersion string,
349358
) (snykCmd *exec.Cmd, err error) {
350359
proxyAddress := fmt.Sprintf("http://%s:%[email protected]:%d", proxy.PROXY_USERNAME, proxyInfo.Password, proxyInfo.Port)
351-
352-
snykCmd = exec.Command(cmd, args...)
360+
snykCmd = exec.CommandContext(ctx, cmd, args...)
353361
snykCmd.Env, err = PrepareV1EnvironmentVariables(c.env, integrationName, integrationVersion, proxyAddress, proxyInfo.CertificateLocation, c.globalConfig, args)
354362

355363
if len(c.WorkingDirectory) > 0 {
@@ -360,8 +368,17 @@ func (c *CLI) PrepareV1Command(
360368
}
361369

362370
func (c *CLI) executeV1Default(proxyInfo *proxy.ProxyInfo, passThroughArgs []string) error {
371+
timeout := c.globalConfig.GetInt(configuration.TIMEOUT)
372+
var ctx context.Context
373+
var cancel context.CancelFunc
374+
if timeout == 0 {
375+
ctx = context.Background()
376+
} else {
377+
ctx, cancel = context.WithTimeout(context.Background(), time.Duration(timeout)*time.Second)
378+
defer cancel()
379+
}
363380

364-
snykCmd, err := c.PrepareV1Command(c.v1BinaryLocation, passThroughArgs, proxyInfo, c.GetIntegrationName(), GetFullVersion())
381+
snykCmd, err := c.PrepareV1Command(ctx, c.v1BinaryLocation, passThroughArgs, proxyInfo, c.GetIntegrationName(), GetFullVersion())
365382

366383
if c.DebugLogger.Writer() != io.Discard {
367384
c.DebugLogger.Println("Launching: ")
@@ -397,13 +414,16 @@ func (c *CLI) executeV1Default(proxyInfo *proxy.ProxyInfo, passThroughArgs []str
397414
snykCmd.Stderr = c.stderr
398415

399416
if err != nil {
400-
if evWarning, ok := err.(EnvironmentWarning); ok {
401-
fmt.Fprintln(c.stdout, "WARNING! ", evWarning)
417+
var evWarning EnvironmentWarning
418+
if errors.As(err, &evWarning) {
419+
_, _ = fmt.Fprintln(c.stdout, "WARNING! ", evWarning)
402420
}
403421
}
404422

405423
err = snykCmd.Run()
406-
424+
if errors.Is(ctx.Err(), context.DeadlineExceeded) {
425+
return ctx.Err()
426+
}
407427
return err
408428
}
409429

@@ -427,14 +447,17 @@ func DeriveExitCode(err error) int {
427447
returnCode := constants.SNYK_EXIT_CODE_OK
428448

429449
if err != nil {
430-
if exitError, ok := err.(*exec.ExitError); ok {
450+
var exitError *exec.ExitError
451+
452+
if errors.As(err, &exitError) {
431453
returnCode = exitError.ExitCode()
454+
} else if errors.Is(err, context.DeadlineExceeded) {
455+
returnCode = constants.SNYK_EXIT_CODE_EX_UNAVAILABLE
432456
} else {
433457
// got an error but it's not an ExitError
434458
returnCode = constants.SNYK_EXIT_CODE_ERROR
435459
}
436460
}
437-
438461
return returnCode
439462
}
440463

cliv2/internal/cliv2/cliv2_test.go

Lines changed: 43 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package cliv2_test
22

33
import (
4+
"context"
45
"io"
56
"log"
67
"os"
78
"os/exec"
89
"path"
10+
"runtime"
911
"sort"
1012
"testing"
1113
"time"
@@ -270,9 +272,11 @@ func Test_prepareV1Command(t *testing.T) {
270272
cacheDir := getCacheDir(t)
271273
config := configuration.NewInMemory()
272274
config.Set(configuration.CACHE_PATH, cacheDir)
273-
cli, _ := cliv2.NewCLIv2(config, discardLogger)
275+
cli, err := cliv2.NewCLIv2(config, discardLogger)
276+
assert.NoError(t, err)
274277

275278
snykCmd, err := cli.PrepareV1Command(
279+
context.Background(),
276280
"someExecutable",
277281
expectedArgs,
278282
getProxyInfoForTest(),
@@ -297,11 +301,12 @@ func Test_extractOnlyOnce(t *testing.T) {
297301
assert.NoDirExists(t, tmpDir)
298302

299303
// create instance under test
300-
cli, _ := cliv2.NewCLIv2(config, discardLogger)
304+
cli, err := cliv2.NewCLIv2(config, discardLogger)
305+
assert.NoError(t, err)
306+
assert.NoError(t, cli.Init())
301307

302308
// run once
303-
assert.Nil(t, cli.Init())
304-
cli.Execute(getProxyInfoForTest(), []string{"--help"})
309+
err = cli.Execute(getProxyInfoForTest(), []string{"--help"})
305310
assert.FileExists(t, cli.GetBinaryLocation())
306311
fileInfo1, _ := os.Stat(cli.GetBinaryLocation())
307312

@@ -310,7 +315,7 @@ func Test_extractOnlyOnce(t *testing.T) {
310315

311316
// run twice
312317
assert.Nil(t, cli.Init())
313-
cli.Execute(getProxyInfoForTest(), []string{"--help"})
318+
_ = cli.Execute(getProxyInfoForTest(), []string{"--help"})
314319
assert.FileExists(t, cli.GetBinaryLocation())
315320
fileInfo2, _ := os.Stat(cli.GetBinaryLocation())
316321

@@ -326,7 +331,8 @@ func Test_init_extractDueToInvalidBinary(t *testing.T) {
326331
assert.NoDirExists(t, tmpDir)
327332

328333
// create instance under test
329-
cli, _ := cliv2.NewCLIv2(config, discardLogger)
334+
cli, err := cliv2.NewCLIv2(config, discardLogger)
335+
assert.NoError(t, err)
330336

331337
// fill binary with invalid data
332338
_ = os.MkdirAll(tmpDir, 0755)
@@ -363,8 +369,9 @@ func Test_executeRunV2only(t *testing.T) {
363369
assert.NoDirExists(t, tmpDir)
364370

365371
// create instance under test
366-
cli, _ := cliv2.NewCLIv2(config, discardLogger)
367-
assert.Nil(t, cli.Init())
372+
cli, err := cliv2.NewCLIv2(config, discardLogger)
373+
assert.NoError(t, err)
374+
assert.NoError(t, cli.Init())
368375

369376
actualReturnCode := cliv2.DeriveExitCode(cli.Execute(getProxyInfoForTest(), []string{"--version"}))
370377
assert.Equal(t, expectedReturnCode, actualReturnCode)
@@ -380,8 +387,9 @@ func Test_executeUnknownCommand(t *testing.T) {
380387
config.Set(configuration.CACHE_PATH, cacheDir)
381388

382389
// create instance under test
383-
cli, _ := cliv2.NewCLIv2(config, discardLogger)
384-
assert.Nil(t, cli.Init())
390+
cli, err := cliv2.NewCLIv2(config, discardLogger)
391+
assert.NoError(t, err)
392+
assert.NoError(t, cli.Init())
385393

386394
actualReturnCode := cliv2.DeriveExitCode(cli.Execute(getProxyInfoForTest(), []string{"bogusCommand"}))
387395
assert.Equal(t, expectedReturnCode, actualReturnCode)
@@ -427,8 +435,9 @@ func Test_clearCacheBigCache(t *testing.T) {
427435
config.Set(configuration.CACHE_PATH, cacheDir)
428436

429437
// create instance under test
430-
cli, _ := cliv2.NewCLIv2(config, discardLogger)
431-
assert.Nil(t, cli.Init())
438+
cli, err := cliv2.NewCLIv2(config, discardLogger)
439+
assert.NoError(t, err)
440+
assert.NoError(t, cli.Init())
432441

433442
// create folders and files in cache dir
434443
dir1 := path.Join(cli.CacheDirectory, "dir1")
@@ -447,8 +456,8 @@ func Test_clearCacheBigCache(t *testing.T) {
447456
_ = os.Mkdir(dir6, 0755)
448457

449458
// clear cache
450-
err := cli.ClearCache()
451-
assert.Nil(t, err)
459+
err = cli.ClearCache()
460+
assert.NoError(t, err)
452461

453462
// check if directories that need to be deleted don't exist
454463
assert.NoDirExists(t, dir1)
@@ -460,3 +469,23 @@ func Test_clearCacheBigCache(t *testing.T) {
460469
assert.DirExists(t, dir6)
461470
assert.FileExists(t, currentVersion)
462471
}
472+
473+
func Test_setTimeout(t *testing.T) {
474+
if //goland:noinspection ALL
475+
runtime.GOOS == "windows" {
476+
t.Skip("Skipping test on windows")
477+
}
478+
config := configuration.NewInMemory()
479+
cli, err := cliv2.NewCLIv2(config, discardLogger)
480+
assert.NoError(t, err)
481+
config.Set(configuration.TIMEOUT, 1)
482+
483+
// sleep for 2s
484+
cli.SetV1BinaryLocation("/bin/sleep")
485+
err = cli.Execute(getProxyInfoForTest(), []string{"2"})
486+
487+
assert.ErrorIs(t, err, context.DeadlineExceeded)
488+
489+
// ensure that -1 is correctly mapped if timeout is set
490+
assert.Equal(t, constants.SNYK_EXIT_CODE_EX_UNAVAILABLE, cliv2.DeriveExitCode(err))
491+
}

0 commit comments

Comments
 (0)