From b02bb004a87713a932a4dc64b8960c6c0fe922b8 Mon Sep 17 00:00:00 2001 From: Stuart Douglas Date: Tue, 17 Sep 2024 07:44:06 +1000 Subject: [PATCH] feat: VSCode support --- bin/.dlv-1.23.0.pkg | 1 + bin/delve-1.23.0 | 1 + go-runtime/compile/build.go | 4 +- internal/buildengine/discover_test.go | 17 ++-- internal/buildengine/engine.go | 2 +- internal/localdebug/local_ide_debug.go | 126 ++++++++++++++++++++++++- internal/moduleconfig/moduleconfig.go | 2 +- 7 files changed, 138 insertions(+), 15 deletions(-) create mode 120000 bin/.dlv-1.23.0.pkg create mode 120000 bin/delve-1.23.0 diff --git a/bin/.dlv-1.23.0.pkg b/bin/.dlv-1.23.0.pkg new file mode 120000 index 000000000..383f4511d --- /dev/null +++ b/bin/.dlv-1.23.0.pkg @@ -0,0 +1 @@ +hermit \ No newline at end of file diff --git a/bin/delve-1.23.0 b/bin/delve-1.23.0 new file mode 120000 index 000000000..d30074516 --- /dev/null +++ b/bin/delve-1.23.0 @@ -0,0 +1 @@ +.dlv-1.23.0.pkg \ No newline at end of file diff --git a/go-runtime/compile/build.go b/go-runtime/compile/build.go index 1e223715a..283af5656 100644 --- a/go-runtime/compile/build.go +++ b/go-runtime/compile/build.go @@ -255,14 +255,14 @@ func Build(ctx context.Context, projectRootDir, moduleDir string, sch *schema.Sc return fmt.Errorf("failed to compile: %w", err) } err = os.WriteFile(filepath.Join(mainDir, "../../launch"), []byte(`#!/bin/bash - if [ -n "$FTL_DEBUG_PORT" ]; then + if [ -n "$FTL_DEBUG_PORT" ] && command -v dlv &> /dev/null ; then dlv --listen=localhost:$FTL_DEBUG_PORT --headless=true --api-version=2 --accept-multiclient --allow-non-terminal-interactive --log exec --continue ./main else exec ./main fi `), 0750) if err != nil { - return err + return fmt.Errorf("failed to write launch script: %w", err) } return nil } diff --git a/internal/buildengine/discover_test.go b/internal/buildengine/discover_test.go index fbef18b03..9b6c6ad88 100644 --- a/internal/buildengine/discover_test.go +++ b/internal/buildengine/discover_test.go @@ -21,7 +21,7 @@ func TestDiscoverModules(t *testing.T) { Language: "go", Realm: "home", Module: "alpha", - Deploy: []string{"main"}, + Deploy: []string{"main", "launch"}, DeployDir: ".ftl", Schema: "schema.pb", Errors: "errors.pb", @@ -34,7 +34,7 @@ func TestDiscoverModules(t *testing.T) { Language: "go", Realm: "home", Module: "another", - Deploy: []string{"main"}, + Deploy: []string{"main", "launch"}, DeployDir: ".ftl", Schema: "schema.pb", Errors: "errors.pb", @@ -47,7 +47,7 @@ func TestDiscoverModules(t *testing.T) { Language: "go", Realm: "home", Module: "depcycle1", - Deploy: []string{"main"}, + Deploy: []string{"main", "launch"}, DeployDir: ".ftl", Schema: "schema.pb", Errors: "errors.pb", @@ -60,7 +60,7 @@ func TestDiscoverModules(t *testing.T) { Language: "go", Realm: "home", Module: "depcycle2", - Deploy: []string{"main"}, + Deploy: []string{"main", "launch"}, DeployDir: ".ftl", Schema: "schema.pb", Errors: "errors.pb", @@ -75,8 +75,8 @@ func TestDiscoverModules(t *testing.T) { Module: "echo", Build: "mvn -B package", Deploy: []string{ - "main", "quarkus-app", + "launch", }, DeployDir: "target", GeneratedSchemaDir: "src/main/ftl-module-schema", @@ -101,6 +101,7 @@ func TestDiscoverModules(t *testing.T) { Build: "", Deploy: []string{ "main", + "launch", }, DeployDir: ".ftl", Schema: "schema.pb", @@ -120,8 +121,8 @@ func TestDiscoverModules(t *testing.T) { Module: "externalkotlin", Build: "mvn -B package", Deploy: []string{ - "main", "quarkus-app", + "launch", }, DeployDir: "target", GeneratedSchemaDir: "src/main/ftl-module-schema", @@ -143,7 +144,7 @@ func TestDiscoverModules(t *testing.T) { Language: "go", Realm: "home", Module: "integer", - Deploy: []string{"main"}, + Deploy: []string{"main", "launch"}, DeployDir: ".ftl", Schema: "schema.pb", Errors: "errors.pb", @@ -156,7 +157,7 @@ func TestDiscoverModules(t *testing.T) { Language: "go", Realm: "home", Module: "other", - Deploy: []string{"main"}, + Deploy: []string{"main", "launch"}, DeployDir: ".ftl", Schema: "schema.pb", Errors: "errors.pb", diff --git a/internal/buildengine/engine.go b/internal/buildengine/engine.go index 37ff61422..1355570d0 100644 --- a/internal/buildengine/engine.go +++ b/internal/buildengine/engine.go @@ -100,7 +100,7 @@ func WithListener(listener Listener) Option { } } -// WithListener sets the event listener for the Engine. +// WithDevMode sets the engine to dev mode. func WithDevMode(devMode bool) Option { return func(o *Engine) { o.devMode = devMode diff --git a/internal/localdebug/local_ide_debug.go b/internal/localdebug/local_ide_debug.go index 000cea43c..a7a2027bd 100644 --- a/internal/localdebug/local_ide_debug.go +++ b/internal/localdebug/local_ide_debug.go @@ -2,6 +2,7 @@ package localdebug import ( "context" + "encoding/json" "fmt" "os" "os/signal" @@ -56,6 +57,7 @@ func init() { var lock = sync.Mutex{} var paths = map[string]bool{} +// SyncIDEDebugIntegrations will sync the local IDE debug configurations for the given project path. // This is a bit of a hack to prove out the concept of local debugging. func SyncIDEDebugIntegrations(cxt context.Context, projectPath string, ports map[string]*DebugInfo) { lock.Lock() @@ -64,6 +66,7 @@ func SyncIDEDebugIntegrations(cxt context.Context, projectPath string, ports map return } handleIntellij(cxt, projectPath, ports) + handleVSCode(cxt, projectPath, ports) } func handleIntellij(cxt context.Context, path string, ports map[string]*DebugInfo) { @@ -86,9 +89,7 @@ func handleIntellij(cxt context.Context, path string, ports map[string]*DebugInf for _, entry := range entries { if strings.HasPrefix(entry.Name(), "FTL.") && strings.HasSuffix(entry.Name(), ".xml") { debugInfo := ports[entry.Name()[4:(len(entry.Name())-4)]] - if debugInfo != nil { - continue - } else { + if debugInfo == nil { // old FTL entry, remove it path := filepath.Join(runConfig, entry.Name()) _ = os.Remove(path) @@ -137,6 +138,125 @@ func findIdeaFolder(startPath string) string { return "" } +func handleVSCode(cxt context.Context, path string, ports map[string]*DebugInfo) { + logger := log.FromContext(cxt) + launchJSON := findLaunchJSON(path) + if launchJSON == "" { + return + } + + contents := map[string]interface{}{} + existing := map[string]int{} + var configurations []interface{} + if _, err := os.Stat(launchJSON); err == nil { + file, err := os.ReadFile(launchJSON) + if err != nil { + logger.Errorf(err, "could not read launch.json") + return + } + err = json.Unmarshal(file, &contents) + if err != nil { + logger.Errorf(err, "could not read launch.json") + return + } + configurations = contents["configurations"].([]interface{}) //nolint:forcetypeassert + if configurations == nil { + configurations = []interface{}{} + } + } else { + contents["version"] = "0.2.0" + configurations = []interface{}{} + } + for i, config := range configurations { + existing[config.(map[string]interface{})["name"].(string)] = i //nolint:forcetypeassert + } + + for k, v := range ports { + if v.Language == "java" || v.Language == "kotlin" { + name := "FTš¯¯ŗ JVM - " + k + pos, ok := existing[name] + if ok { + // Update the port + configurations[pos].(map[string]interface{})["port"] = v.Port //nolint:forcetypeassert + continue + } + entry := map[string]interface{}{ + "name": name, + "type": "java", + "request": "attach", + "hostName": "127.0.0.1", + "port": v.Port, + } + configurations = append(configurations, entry) + + } else if v.Language == "go" { + name := "FTš¯¯ŗ GO - " + k + pos, ok := existing[name] + if ok { + // Update the port + configurations[pos].(map[string]interface{})["port"] = v.Port //nolint:forcetypeassert + continue + } + entry := map[string]interface{}{ + "name": name, + "type": "go", + "request": "attach", + "mode": "remote", + "apiVersion": 2, + "host": "127.0.0.1", + "port": v.Port, + } + configurations = append(configurations, entry) + } + } + contents["configurations"] = configurations + data, err := json.Marshal(contents) + if err != nil { + logger.Errorf(err, "could not marshal launch.json") + return + } + err = os.WriteFile(launchJSON, data, 0644) + if err != nil { + logger.Errorf(err, "could not write launch.json") + return + } +} + +// findLaunchJSON recurses up the directory tree to find a .vscode folder +// If it can't find one it creates one next to project.toml +// It then returns the path to the launch.json file +func findLaunchJSON(startPath string) string { + currentPath := startPath + projectPath := "" + for { + vscodePath := filepath.Join(currentPath, ".vscode") + if _, err := os.Stat(vscodePath); err == nil { + return filepath.Join(vscodePath, "launch.json") + } + if projectPath == "" { + tmp := filepath.Join(currentPath, "project.toml") + if _, err := os.Stat(tmp); err == nil { + projectPath = currentPath + } + } + parentPath := filepath.Dir(currentPath) + if parentPath == currentPath { + // Reached the root directory + break + } + currentPath = parentPath + } + if projectPath != "" { + vscodePath := filepath.Join(projectPath, ".vscode") + err := os.MkdirAll(vscodePath, 0750) + if err != nil { + return "" + } + return filepath.Join(vscodePath, "launch.json") + } + return "" +} + type DebugInfo struct { Port int Language string diff --git a/internal/moduleconfig/moduleconfig.go b/internal/moduleconfig/moduleconfig.go index 4bda38263..35a40f4e0 100644 --- a/internal/moduleconfig/moduleconfig.go +++ b/internal/moduleconfig/moduleconfig.go @@ -167,7 +167,7 @@ func setConfigDefaults(moduleDir string, config *ModuleConfig) error { config.GeneratedSchemaDir = "src/main/ftl-module-schema" } if len(config.Deploy) == 0 { - config.Deploy = []string{"main", "quarkus-app"} + config.Deploy = []string{"quarkus-app", "launch"} } case "go": if config.DeployDir == "" {