Skip to content

Commit

Permalink
feat: warn, if LS does not match client requirements (#559)
Browse files Browse the repository at this point in the history
  • Loading branch information
bastiandoetsch authored Jun 27, 2024
1 parent 1de2c7f commit 76b5ef0
Show file tree
Hide file tree
Showing 18 changed files with 526 additions and 25 deletions.
8 changes: 6 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,11 @@ proxy-test:

instance-test:
@echo "==> Running instance tests"
@export SMOKE_TESTS=1 && cd application/server && go test -run Test_SmokeWorkspaceScan && cd -
@export SMOKE_TESTS=1 && cd application/server && go test $(TIMEOUT) -failfast -run Test_SmokeWorkspaceScan && cd -

instance-standard-test:
@echo "==> Running instance tests for the standard environment"
@export SMOKE_TESTS=1 && cd application/server && go test -run Test_Smoke && cd -
@export SMOKE_TESTS=1 && cd application/server && go test $(TIMEOUT) -failfast -run Test_Smoke && cd -
@echo "==> Checking Eclipse storage buckets..."
@curl -sSL https://static.snyk.io/eclipse/stable/p2.index

Expand Down Expand Up @@ -122,6 +122,10 @@ else
-gcflags="all=-N -l"
endif

.PHONY: build-release
build-release: $(TOOLS_BIN)/go-licenses
@LICENSES=$(go-licenses report . --ignore github.com/snyk/snyk-ls) goreleaser release --clean --snapshot

## run: Compile and run LSP server.
.PHONY: run
run:
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ within `initializationOptions?: LSPAny;` we support the following settings:
// Specifies the authentication method to use: "token" for Snyk API token or "oauth" for Snyk OAuth flow. Default is token.
"snykCodeApi": "https://deeproxy.snyk.io",
// Specifies the Snyk Code API endpoint to use. Default is https://deeproxy.snyk.io
"requiredProtocolVersion": "11"
}
```
Expand Down
16 changes: 16 additions & 0 deletions application/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"sync"
"time"

"github.com/snyk/go-application-framework/pkg/runtimeinfo"
"github.com/snyk/snyk-ls/infrastructure/cli/cli_constants"
"github.com/snyk/snyk-ls/internal/logging"

Expand Down Expand Up @@ -190,6 +191,7 @@ type Config struct {
logger *zerolog.Logger
storage StorageWithCallbacks
m sync.Mutex
clientProtocolVersion string
}

func CurrentConfig() *Config {
Expand All @@ -207,6 +209,8 @@ func SetCurrentConfig(config *Config) {
currentConfig = config
}

func (c *Config) ClientProtocolVersion() string { return c.clientProtocolVersion }

func IsDevelopment() bool {
parseBool, _ := strconv.ParseBool(Development)
return parseBool
Expand Down Expand Up @@ -259,6 +263,14 @@ func initWorkFlowEngine(c *Config) {
if err != nil {
c.Logger().Warn().Err(err).Msg("unable to initialize workflow engine")
}

// if running in standalone-mode, runtime info is not set, else, when in extension mode
// it's already set by the CLI initialization
// see https://github.com/snyk/cli/blob/main/cliv2/cmd/cliv2/main.go#L460
if c.engine.GetRuntimeInfo() == nil {
rti := runtimeinfo.New(runtimeinfo.WithName("snyk-ls"), runtimeinfo.WithVersion(Version))
c.engine.SetRuntimeInfo(rti)
}
}

func getNewScrubbingLogger(c *Config) *zerolog.Logger {
Expand Down Expand Up @@ -926,3 +938,7 @@ func (c *Config) IsAnalyticsPermitted() bool {

return found
}

func (c *Config) SetClientProtocolVersion(requiredProtocolVersion string) {
c.clientProtocolVersion = requiredProtocolVersion
}
1 change: 1 addition & 0 deletions application/server/configuration.go
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ func InitializeSettings(c *config.Config, settings lsp.Settings) {
updateAutoAuthentication(c, settings)
updateDeviceInformation(c, settings)
updateAutoScan(c, settings)
c.SetClientProtocolVersion(settings.RequiredProtocolVersion)
}

func UpdateSettings(c *config.Config, settings lsp.Settings) {
Expand Down
77 changes: 70 additions & 7 deletions application/server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,13 @@ import (
"github.com/snyk/snyk-ls/domain/ide/command"
"github.com/snyk/snyk-ls/domain/ide/converter"
"github.com/snyk/snyk-ls/domain/ide/hover"
noti "github.com/snyk/snyk-ls/domain/ide/notification"
"github.com/snyk/snyk-ls/domain/ide/workspace"
"github.com/snyk/snyk-ls/domain/snyk"
"github.com/snyk/snyk-ls/infrastructure/cli"
"github.com/snyk/snyk-ls/infrastructure/cli/cli_constants"
"github.com/snyk/snyk-ls/infrastructure/cli/install"
"github.com/snyk/snyk-ls/internal/data_structure"
"github.com/snyk/snyk-ls/internal/lsp"
"github.com/snyk/snyk-ls/internal/progress"
"github.com/snyk/snyk-ls/internal/uri"
Expand All @@ -59,7 +63,7 @@ func Start(c *config.Config) {
c.ConfigureLogging(srv)
logger := c.Logger().With().Str("method", "server.Start").Logger()
di.Init()
initHandlers(c, srv, handlers)
initHandlers(srv, handlers)

logger.Info().Msg("Starting up...")
srv = srv.Start(channel.Header("")(os.Stdin, os.Stdout))
Expand All @@ -75,7 +79,7 @@ func Start(c *config.Config) {
const textDocumentDidOpenOperation = "textDocument/didOpen"
const textDocumentDidSaveOperation = "textDocument/didSave"

func initHandlers(c *config.Config, srv *jrpc2.Server, handlers handler.Map) {
func initHandlers(srv *jrpc2.Server, handlers handler.Map) {
handlers["initialize"] = initializeHandler(srv)
handlers["initialized"] = initializedHandler(srv)
handlers["textDocument/didChange"] = textDocumentDidChangeHandler()
Expand Down Expand Up @@ -123,12 +127,12 @@ func workspaceWillDeleteFilesHandler() jrpc2.Handler {
return handler.New(func(ctx context.Context, params lsp.DeleteFilesParams) (any, error) {
ws := workspace.Get()
for _, file := range params.Files {
path := uri.PathFromUri(file.Uri)
pathFromUri := uri.PathFromUri(file.Uri)

// Instead of branching whether it's a file or a folder, we'll attempt to remove both and the redundant case
// will be a no-op
ws.RemoveFolder(path)
ws.DeleteFile(path)
ws.RemoveFolder(pathFromUri)
ws.DeleteFile(pathFromUri)
}
return nil, nil
})
Expand Down Expand Up @@ -277,6 +281,63 @@ func initializeHandler(srv *jrpc2.Server) handler.Func {
return result, nil
})
}

func handleProtocolVersion(c *config.Config, noti noti.Notifier, ourProtocolVersion string, clientProtocolVersion string) {
logger := c.Logger().With().Str("method", "handleProtocolVersion").Logger()
if clientProtocolVersion == "" {
logger.Debug().Msg("no client protocol version specified")
return
}

if clientProtocolVersion == ourProtocolVersion || ourProtocolVersion == "development" {
logger.Debug().Msg("protocol version is the same")
return
}

if clientProtocolVersion != ourProtocolVersion {
m := fmt.Sprintf(
"Your Snyk plugin requires a different binary. The client-side required protocol version does not match "+
"the running language server protocol version. Required: %s, Actual: %s. "+
"You can update to the necessary version by enabling automatic management of binaries in the settings. "+
"Alternatively, you can manually download the correct binary by clicking the button.",
clientProtocolVersion,
ourProtocolVersion,
)
logger.Error().Msg(m)
actions := data_structure.NewOrderedMap[snyk.MessageAction, snyk.CommandData]()

openBrowserCommandData := snyk.CommandData{
Title: "Download manually in browser",
CommandId: snyk.OpenBrowserCommand,
Arguments: []any{getDownloadURL(c)},
}

actions.Add(snyk.MessageAction(openBrowserCommandData.Title), openBrowserCommandData)
doNothingKey := "Cancel"
// if we don't provide a commandId, nothing is done
actions.Add(snyk.MessageAction(doNothingKey), snyk.CommandData{Title: doNothingKey})

msg := snyk.ShowMessageRequest{
Message: m,
Type: snyk.Error,
Actions: actions,
}
noti.Send(msg)
}
}

func getDownloadURL(c *config.Config) (u string) {
gafConfig := c.Engine().GetConfiguration()

runsEmbeddedFromCLI := gafConfig.Get(cli_constants.EXECUTION_MODE_KEY) == cli_constants.EXECUTION_MODE_VALUE_EXTENSION

if runsEmbeddedFromCLI {
return install.GetCLIDownloadURL(c, install.DefaultBaseURL, c.Engine().GetNetworkAccess().GetUnauthorizedHttpClient())
} else {
return install.GetLSDownloadURL(c, c.Engine().GetNetworkAccess().GetUnauthorizedHttpClient())
}
}

func initializedHandler(srv *jrpc2.Server) handler.Func {
return handler.New(func(ctx context.Context, params lsp.InitializedParams) (any, error) {
// Logging these messages only after the client has been initialized.
Expand All @@ -295,6 +356,8 @@ func initializedHandler(srv *jrpc2.Server) handler.Func {

logger := c.Logger().With().Str("method", "initializedHandler").Logger()

handleProtocolVersion(c, di.Notifier(), config.LsProtocolVersion, c.ClientProtocolVersion())

// CLI & Authentication initialization
err := di.Scanner().Init()
if err != nil {
Expand Down Expand Up @@ -509,8 +572,8 @@ func textDocumentHover() jrpc2.Handler {
c := config.CurrentConfig()
c.Logger().Info().Str("method", "TextDocumentHover").Interface("params", params).Msg("RECEIVING")

path := uri.PathFromUri(params.TextDocument.URI)
hoverResult := di.HoverService().GetHover(path, converter.FromPosition(params.Position))
pathFromUri := uri.PathFromUri(params.TextDocument.URI)
hoverResult := di.HoverService().GetHover(pathFromUri, converter.FromPosition(params.Position))
return hoverResult, nil
})
}
Expand Down
1 change: 1 addition & 0 deletions application/server/server_smoke_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ func Test_SmokeWorkspaceScan(t *testing.T) {
ossFile := "package.json"
iacFile := "main.tf"
codeFile := "app.js"
testutil.CreateDummyProgressListener(t)

type test struct {
name string
Expand Down
108 changes: 107 additions & 1 deletion application/server/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/snyk/go-application-framework/pkg/runtimeinfo"
"github.com/snyk/snyk-ls/application/config"
"github.com/snyk/snyk-ls/application/di"
"github.com/snyk/snyk-ls/domain/ide/command"
Expand All @@ -45,9 +46,11 @@ import (
"github.com/snyk/snyk-ls/domain/observability/ux"
"github.com/snyk/snyk-ls/domain/snyk"
"github.com/snyk/snyk-ls/infrastructure/cli"
"github.com/snyk/snyk-ls/infrastructure/cli/cli_constants"
"github.com/snyk/snyk-ls/infrastructure/cli/install"
"github.com/snyk/snyk-ls/infrastructure/code"
"github.com/snyk/snyk-ls/internal/lsp"
"github.com/snyk/snyk-ls/internal/notification"
"github.com/snyk/snyk-ls/internal/progress"
"github.com/snyk/snyk-ls/internal/testutil"
"github.com/snyk/snyk-ls/internal/uri"
Expand Down Expand Up @@ -148,7 +151,7 @@ func startServer(callBackFn onCallbackFn, jsonRPCRecorder *testutil.JsonRPCRecor
c.ConfigureLogging(nil)

// the learn service isnt needed as the smoke tests use it directly
initHandlers(c, srv, handlers)
initHandlers(srv, handlers)

return loc
}
Expand Down Expand Up @@ -205,6 +208,30 @@ func Test_initialize_containsServerInfo(t *testing.T) {
assert.Equal(t, config.LsProtocolVersion, result.ServerInfo.Version)
}

func Test_initialized_shouldCheckRequiredProtocolVersion(t *testing.T) {
loc, jsonRpcRecorder := setupServer(t)

params := lsp.InitializeParams{
InitializationOptions: lsp.Settings{RequiredProtocolVersion: "22"},
}

config.LsProtocolVersion = "12"

rsp, err := loc.Client.Call(ctx, "initialize", params)
require.NoError(t, err)
var result lsp.InitializeResult
err = rsp.UnmarshalResult(&result)
require.NoError(t, err)

_, err = loc.Client.Call(ctx, "initialized", params)
require.NoError(t, err)
assert.Eventuallyf(t, func() bool {
callbacks := jsonRpcRecorder.Callbacks()
return len(callbacks) > 0
}, time.Second*10, time.Millisecond,
"did not receive callback because of wrong protocol version")
}

func Test_initialize_shouldDefaultToTokenAuthentication(t *testing.T) {
loc, _ := setupServer(t)

Expand Down Expand Up @@ -1102,3 +1129,82 @@ func Test_MonitorClientProcess(t *testing.T) {
expectedMinimumDuration, _ := time.ParseDuration("999ms")
assert.True(t, monitorClientProcess(pid) > expectedMinimumDuration)
}

func Test_getDownloadURL(t *testing.T) {
t.Run("CLI", func(t *testing.T) {
c := testutil.UnitTest(t)
c.Engine().GetConfiguration().Set(cli_constants.EXECUTION_MODE_KEY, cli_constants.EXECUTION_MODE_VALUE_EXTENSION)

downloadURL := getDownloadURL(c)

// default CLI fallback, as we're not mocking the CLI calls
assert.Contains(t, downloadURL, "cli")
})

t.Run("LS standalone", func(t *testing.T) {
testutil.NotOnWindows(t, "don't want to handle the exe extension")
c := testutil.UnitTest(t)
engine := c.Engine()
engine.GetConfiguration().Set(cli_constants.EXECUTION_MODE_KEY, cli_constants.EXECUTION_MODE_VALUE_STANDALONE)
engine.SetRuntimeInfo(
runtimeinfo.New(
runtimeinfo.WithName("snyk-ls"),
runtimeinfo.WithVersion("v1.234"),
),
)

downloadURL := getDownloadURL(c)

prefix := "https://static.snyk.io/snyk-ls/12/snyk-ls"
assert.True(t, strings.HasPrefix(downloadURL, prefix), downloadURL+" does not start with "+prefix)
})
}

func Test_handleProtocolVersion(t *testing.T) {
t.Run("required != current", func(t *testing.T) {
c := testutil.UnitTest(t)

ourProtocolVersion := "12"
reqProtocolVersion := "1"

notificationReceived := make(chan bool)
f := func(params any) {
mrq, ok := params.(snyk.ShowMessageRequest)
require.True(t, ok)
require.Contains(t, mrq.Message, "does not match")
notificationReceived <- true
}
testNotifier := notification.NewNotifier()
go testNotifier.CreateListener(f)
handleProtocolVersion(
c,
testNotifier,
ourProtocolVersion,
reqProtocolVersion,
)

assert.Eventuallyf(t, func() bool {
return <-notificationReceived
}, 10*time.Second, 10*time.Millisecond, "no message sent via notifier")
})

t.Run("required == current", func(t *testing.T) {
c := testutil.UnitTest(t)
ourProtocolVersion := "11"
f := func(params any) {
require.FailNow(t, "did not expect a message")
}

testNotifier := notification.NewNotifier()
go testNotifier.CreateListener(f)

handleProtocolVersion(
c,
testNotifier,
ourProtocolVersion,
ourProtocolVersion,
)
// give goroutine of callback function a chance to fail the test
time.Sleep(time.Second)
})
}
6 changes: 6 additions & 0 deletions infrastructure/cli/cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import (
"sync"
"time"

"github.com/rs/zerolog"

"github.com/snyk/snyk-ls/application/config"
noti "github.com/snyk/snyk-ls/domain/ide/notification"
"github.com/snyk/snyk-ls/domain/observability/error_reporting"
Expand Down Expand Up @@ -85,11 +87,15 @@ func (c SnykCli) Execute(ctx context.Context, cmd []string, workingDir string) (

func (c SnykCli) doExecute(ctx context.Context, cmd []string, workingDir string) ([]byte, error) {
command := c.getCommand(cmd, workingDir, ctx)
command.Stderr = c.c.Logger()
output, err := command.Output()
return output, err
}

func (c SnykCli) getCommand(cmd []string, workingDir string, ctx context.Context) *exec.Cmd {
if c.c.Logger().GetLevel() < zerolog.InfoLevel {
cmd = append(cmd, "-d")
}
command := exec.CommandContext(ctx, cmd[0], cmd[1:]...)
command.Dir = workingDir
cliEnv := AppendCliEnvironmentVariables(os.Environ(), true)
Expand Down
Loading

0 comments on commit 76b5ef0

Please sign in to comment.