Browse Source

feat: Adding debug adapter for VSCode

Matthias Ladkau 3 years ago
parent
commit
d0295ce844

+ 64 - 6
cli/tool/debug.go

@@ -12,6 +12,8 @@ package tool
 
 import (
 	"bufio"
+	"bytes"
+	"encoding/base64"
 	"encoding/json"
 	"flag"
 	"fmt"
@@ -19,6 +21,7 @@ import (
 	"strings"
 	"time"
 
+	"devt.de/krotik/common/errorutil"
 	"devt.de/krotik/common/stringutil"
 	"devt.de/krotik/ecal/interpreter"
 	"devt.de/krotik/ecal/util"
@@ -34,6 +37,7 @@ type CLIDebugInterpreter struct {
 
 	DebugServerAddr *string // Debug server address
 	RunDebugServer  *bool   // Run a debug server
+	EchoDebugServer *bool   // Echo all input and output of the debug server
 	Interactive     *bool   // Flag if the interpreter should open a console in the current tty.
 }
 
@@ -41,7 +45,7 @@ type CLIDebugInterpreter struct {
 NewCLIDebugInterpreter wraps an existing CLIInterpreter object and adds capabilities.
 */
 func NewCLIDebugInterpreter(i *CLIInterpreter) *CLIDebugInterpreter {
-	return &CLIDebugInterpreter{i, nil, nil, nil}
+	return &CLIDebugInterpreter{i, nil, nil, nil, nil}
 }
 
 /*
@@ -55,6 +59,7 @@ func (i *CLIDebugInterpreter) ParseArgs() bool {
 
 	i.DebugServerAddr = flag.String("serveraddr", "localhost:33274", "Debug server address") // Think BERTA
 	i.RunDebugServer = flag.Bool("server", false, "Run a debug server")
+	i.EchoDebugServer = flag.Bool("echo", false, "Echo all i/o of the debug server")
 	i.Interactive = flag.Bool("interactive", true, "Run interactive console")
 
 	return i.CLIInterpreter.ParseArgs()
@@ -92,7 +97,7 @@ func (i *CLIDebugInterpreter) Interpret() error {
 
 		if *i.RunDebugServer {
 			debugServer := &debugTelnetServer{*i.DebugServerAddr, "ECALDebugServer: ",
-				nil, true, i, i.RuntimeProvider.Logger}
+				nil, true, *i.EchoDebugServer, i, i.RuntimeProvider.Logger}
 			go debugServer.Run()
 			time.Sleep(500 * time.Millisecond) // Too lazy to do proper signalling
 			defer func() {
@@ -141,6 +146,7 @@ func (i *CLIDebugInterpreter) Handle(ot OutputTerminal, line string) {
 			ot.WriteString(stringutil.PrintGraphicStringTable(tabData, 2, 1,
 				stringutil.SingleDoubleLineTable))
 		}
+		ot.WriteString(fmt.Sprintln(fmt.Sprintln()))
 
 	} else {
 		res, err := i.RuntimeProvider.Debugger.HandleInput(strings.TrimSpace(line[2:]))
@@ -149,12 +155,17 @@ func (i *CLIDebugInterpreter) Handle(ot OutputTerminal, line string) {
 			var outBytes []byte
 			outBytes, err = json.MarshalIndent(res, "", "  ")
 			if err == nil {
-				ot.WriteString(fmt.Sprintln(string(outBytes)))
+				ot.WriteString(fmt.Sprintln(fmt.Sprintln(string(outBytes))))
 			}
 		}
 
 		if err != nil {
-			ot.WriteString(fmt.Sprintf("Debugger Error: %v", err.Error()))
+			var outBytes []byte
+			outBytes, err = json.MarshalIndent(map[string]interface{}{
+				"DebuggerError": err.Error(),
+			}, "", "  ")
+			errorutil.AssertOk(err)
+			ot.WriteString(fmt.Sprintln(fmt.Sprintln(string(outBytes))))
 		}
 	}
 }
@@ -167,6 +178,7 @@ type debugTelnetServer struct {
 	logPrefix   string
 	listener    *net.TCPListener
 	listen      bool
+	echo        bool
 	interpreter *CLIDebugInterpreter
 	logger      util.Logger
 }
@@ -210,26 +222,67 @@ HandleConnection handles an incoming connection.
 func (s *debugTelnetServer) HandleConnection(conn net.Conn) {
 	tid := s.interpreter.RuntimeProvider.NewThreadID()
 	inputReader := bufio.NewReader(conn)
-	outputTerminal := OutputTerminal(&bufioWriterShim{bufio.NewWriter(conn)})
+	outputTerminal := OutputTerminal(&bufioWriterShim{fmt.Sprint(conn.RemoteAddr()), bufio.NewWriter(conn), s.echo})
 
 	line := ""
 
 	s.logger.LogDebug(s.logPrefix, "Connect ", conn.RemoteAddr())
+	if s.echo {
+		fmt.Println(fmt.Sprintf("%v : Connected", conn.RemoteAddr()))
+	}
 
 	for {
+		var outBytes []byte
 		var err error
 
 		if line, err = inputReader.ReadString('\n'); err == nil {
 			line = strings.TrimSpace(line)
 
+			if s.echo {
+				fmt.Println(fmt.Sprintf("%v > %v", conn.RemoteAddr(), line))
+			}
+
 			if line == "exit" || line == "q" || line == "quit" || line == "bye" || line == "\x04" {
 				break
 			}
 
-			s.interpreter.HandleInput(outputTerminal, line, tid)
+			isHelpTable := strings.HasPrefix(line, "@")
+
+			if !s.interpreter.CanHandle(line) || isHelpTable {
+				buffer := bytes.NewBuffer(nil)
+
+				s.interpreter.HandleInput(&bufioWriterShim{"tmpbuffer", bufio.NewWriter(buffer), false}, line, tid)
+
+				if isHelpTable {
+
+					// Special case we have tables which should be transformed
+
+					r := strings.NewReplacer("═", "*", "│", "*", "╪", "*", "╒", "*",
+						"╕", "*", "╘", "*", "╛", "*", "╤", "*", "╞", "*", "╡", "*", "╧", "*")
+
+					outBytes = []byte(r.Replace(buffer.String()))
+
+				} else {
+
+					outBytes = buffer.Bytes()
+				}
+
+				outBytes, err = json.MarshalIndent(map[string]interface{}{
+					"EncodedOutput": base64.StdEncoding.EncodeToString(outBytes),
+				}, "", "  ")
+				errorutil.AssertOk(err)
+				outputTerminal.WriteString(fmt.Sprintln(fmt.Sprintln(string(outBytes))))
+
+			} else {
+
+				s.interpreter.HandleInput(outputTerminal, line, tid)
+			}
 		}
 
 		if err != nil {
+			if s.echo {
+				fmt.Println(fmt.Sprintf("%v : Disconnected", conn.RemoteAddr()))
+			}
 			s.logger.LogDebug(s.logPrefix, "Disconnect ", conn.RemoteAddr(), " - ", err)
 			break
 		}
@@ -242,13 +295,18 @@ func (s *debugTelnetServer) HandleConnection(conn net.Conn) {
 bufioWriterShim is a shim to allow a bufio.Writer to be used as an OutputTerminal.
 */
 type bufioWriterShim struct {
+	id     string
 	writer *bufio.Writer
+	echo   bool
 }
 
 /*
 WriteString write a string to the writer.
 */
 func (shim *bufioWriterShim) WriteString(s string) {
+	if shim.echo {
+		fmt.Println(fmt.Sprintf("%v < %v", shim.id, s))
+	}
 	shim.writer.WriteString(s)
 	shim.writer.Flush()
 }

+ 1 - 1
cli/tool/interpret.go

@@ -282,7 +282,7 @@ func (i *CLIInterpreter) HandleInput(ot OutputTerminal, line string, tid uint64)
 			if ierr = ast.Runtime.Validate(); ierr == nil {
 
 				if res, ierr = ast.Runtime.Eval(i.GlobalVS, make(map[string]interface{}), tid); ierr == nil && res != nil {
-					ot.WriteString(fmt.Sprintln(res))
+					ot.WriteString(fmt.Sprintln(stringutil.ConvertToString(res)))
 				}
 			}
 		}

+ 5 - 0
ecal-support/.eslintrc.js

@@ -15,5 +15,10 @@ module.exports = {
     '@typescript-eslint'
   ],
   rules: {
+    "eqeqeq": [2, "allow-null"],
+    "indent": [2, 4],
+    "quotes": [2, "single"],
+    "semi": [2, "always"],
+    "no-console": 0
   }
 }

+ 1 - 1
ecal-support/.vscode/launch.json

@@ -3,7 +3,7 @@
     "version": "0.2.0",
     "configurations": [
         {
-            "name": "Extension",
+            "name": "ECAL Extension",
             "type": "extensionHost",
             "request": "launch",
             "args": [

+ 17 - 7
ecal-support/package.json

@@ -24,11 +24,14 @@
         "compile": "tsc",
         "watch": "tsc -w",
         "package": "vsce package",
-        "pretty": "eslint 'src/**/*.{js,ts,tsx}' --quiet --fix"
+        "lint": "eslint 'src/**/*.{js,ts,tsx}' --quiet --fix"
     },
     "dependencies": {
         "@jpwilliams/waitgroup": "1.0.1",
-        "vscode-debugadapter": "^1.42.1"
+        "vscode-debugadapter": "^1.42.1",
+        "promise-socket": "^7.0.0",
+        "readline-promise": "^1.0.4",
+        "async-lock": "^1.2.4"
     },
     "devDependencies": {
         "@types/node": "^14.14.2",
@@ -83,13 +86,19 @@
                 "configurationAttributes": {
                     "launch": {
                         "required": [
-                            "serverURL",
+                            "host",
+                            "port",
                             "dir"
                         ],
                         "properties": {
-                            "serverURL": {
+                            "host": {
                                 "type": "string",
-                                "description": "URL of the ECAL debug server.",
+                                "description": "Host of the ECAL debug server.",
+                                "default": "localhost:43806"
+                            },
+                            "port": {
+                                "type": "number",
+                                "description": "Port of the ECAL debug server.",
                                 "default": "localhost:43806"
                             },
                             "dir": {
@@ -99,7 +108,7 @@
                             },
                             "executeOnEntry": {
                                 "type": "boolean",
-                                "description": "Execute the ECAL script on entry. If this is set to false then code needs to be manually started from the ECAL debug server console.",
+                                "description": "Execute the current edited ECAL script on entry. If this is set to false then code needs to be manually started from the ECAL debug server console.",
                                 "default": true
                             },
                             "trace": {
@@ -115,7 +124,8 @@
                         "type": "ecaldebug",
                         "request": "launch",
                         "name": "Debug ECAL script with ECAL Debug Server",
-                        "serverURL": "localhost:43806",
+                        "host": "localhost",
+                        "port": 33274,
                         "dir": "${workspaceFolder}",
                         "executeOnEntry": true,
                         "trace": false

+ 234 - 130
ecal-support/src/ecalDebugAdapter.ts

@@ -5,9 +5,14 @@
  * https://code.visualstudio.com/api/extension-guides/debugger-extension
  */
 
-import { logger, Logger, LoggingDebugSession, InitializedEvent, Thread } from 'vscode-debugadapter'
-import { DebugProtocol } from 'vscode-debugprotocol'
-import { WaitGroup } from '@jpwilliams/waitgroup'
+import {
+    logger, Logger, LoggingDebugSession, InitializedEvent,
+    Thread, Breakpoint
+} from 'vscode-debugadapter';
+import { DebugProtocol } from 'vscode-debugprotocol';
+import { WaitGroup } from '@jpwilliams/waitgroup';
+import { ECALDebugClient } from './ecalDebugClient';
+import * as vscode from 'vscode';
 
 /**
  * ECALDebugArguments are the arguments which VSCode can pass to the debug adapter.
@@ -15,9 +20,9 @@ import { WaitGroup } from '@jpwilliams/waitgroup'
  * debug adapter from a lauch configuration ('.vscode/launch.json') in a project folder.
  */
 interface ECALDebugArguments extends DebugProtocol.LaunchRequestArguments {
-
+    host: string; // Host of the ECAL debug server
+    port: number; // Port of the ECAL debug server
     dir: string; // Root directory for ECAL interpreter
-    serverURL: string // URL of the ECAL debug server
     executeOnEntry?: boolean; // Flag if the debugged script should be executed when the debug session is started
     trace?: boolean; // Flag to enable verbose logging of the adapter protocol
 }
@@ -36,18 +41,33 @@ export class ECALDebugSession extends LoggingDebugSession {
      */
     private wgConfig = new WaitGroup();
 
+    private client: ECALDebugClient;
+
+    private extout : vscode.OutputChannel = vscode.window.createOutputChannel('ECAL Debug Session');
+
+    private config :ECALDebugArguments = {} as ECALDebugArguments;
+
     /**
      * Create a new debug adapter which is used for one debug session.
      */
     public constructor () {
-      super('mock-debug.txt')
-      console.error('##### constructor')
-      // Lines and columns start at 1
-      this.setDebuggerLinesStartAt1(true)
-      this.setDebuggerColumnsStartAt1(true)
-
-      // Increment the config WaitGroup counter for configurationDoneRequest()
-      this.wgConfig.add(1)
+        super('mock-debug.txt');
+
+        this.extout.appendLine('Creating Debug Session');
+        this.client = new ECALDebugClient(new LogChannelAdapter(this.extout));
+
+        // Add event handlers
+
+        this.client.on('pauseOnBreakpoint', e => {
+            console.log("event:", e)
+        })
+
+        // Lines and columns start at 1
+        this.setDebuggerLinesStartAt1(true);
+        this.setDebuggerColumnsStartAt1(true);
+
+        // Increment the config WaitGroup counter for configurationDoneRequest()
+        this.wgConfig.add(1);
     }
 
     /**
@@ -55,16 +75,16 @@ export class ECALDebugSession extends LoggingDebugSession {
      * interrogates the debug adapter on the features which it provides.
      */
     protected initializeRequest (response: DebugProtocol.InitializeResponse, args: DebugProtocol.InitializeRequestArguments): void {
-      console.log('##### initializeRequest:', args)
+        console.log('##### initializeRequest:', args);
 
-      response.body = response.body || {}
+        response.body = response.body || {};
 
-      // The adapter implements the configurationDoneRequest.
-      response.body.supportsConfigurationDoneRequest = true
+        // The adapter implements the configurationDoneRequest.
+        response.body.supportsConfigurationDoneRequest = true;
 
-      this.sendResponse(response)
+        this.sendResponse(response);
 
-      this.sendEvent(new InitializedEvent())
+        this.sendEvent(new InitializedEvent());
     }
 
     /**
@@ -72,191 +92,275 @@ export class ECALDebugSession extends LoggingDebugSession {
      * finished the initialization of the debug adapter.
      */
     protected configurationDoneRequest (response: DebugProtocol.ConfigurationDoneResponse, args: DebugProtocol.ConfigurationDoneArguments): void {
-      console.error('##### configurationDoneRequest')
+        console.log('##### configurationDoneRequest');
 
-      super.configurationDoneRequest(response, args)
-      this.wgConfig.done()
+        super.configurationDoneRequest(response, args);
+        this.wgConfig.done();
     }
 
     /**
      * The client (e.g. VSCode) asks the debug adapter to start the debuggee communication.
      */
     protected async launchRequest (response: DebugProtocol.LaunchResponse, args: ECALDebugArguments) {
-      console.error('##### launchRequest:', args)
+        console.log('##### launchRequest:', args);
 
-      // Setup logging either verbose or just on errors
+        this.config = args; // Store the configuration
 
-      logger.setup(args.trace ? Logger.LogLevel.Verbose : Logger.LogLevel.Error, false)
+        // Setup logging either verbose or just on errors
 
-      await this.wgConfig.wait() // Wait for configuration sequence to finish
+        logger.setup(args.trace ? Logger.LogLevel.Verbose : Logger.LogLevel.Error, false);
 
-      this.sendResponse(response)
+        await this.wgConfig.wait(); // Wait for configuration sequence to finish
+
+        this.extout.appendLine(`Configuration loaded: ${JSON.stringify(args)}`);
+
+        await this.client.conect(args.host, args.port);
+
+        this.sendResponse(response);
     }
 
     protected async setBreakPointsRequest (response: DebugProtocol.SetBreakpointsResponse, args: DebugProtocol.SetBreakpointsArguments): Promise<void> {
-      console.error('##### setBreakPointsRequest:', args)
+        console.error('##### setBreakPointsRequest:', args);
+
+        const breakpoints:DebugProtocol.Breakpoint[] = [];
+
+        if (args.source.path?.indexOf(this.config.dir) === 0) {
+            const source = args.source.path.slice(this.config.dir.length + 1);
+
+            // Clear all breakpoints of the file
+
+            await this.client.clearBreakpoints(source);
+
+            // Set all breakpoints
 
-      response.body = {
-        breakpoints: []
-      }
-      this.sendResponse(response)
+            for (const line of args.lines || []) {
+                await this.client.setBreakpoint(`${source}:${line}`);
+            }
+
+            // Confirm that the breakpoints have been set
+
+            const status = await this.client.status();
+            if (status) {
+                for (const [k, v] of Object.entries(status.breakpoints)) {
+                    if (v) {
+                        const line = parseInt(k.split(':')[1]);
+                        this.extout.appendLine(`Setting breakpoint for ${args.source.name}: ${line}`);
+                        breakpoints.push(new Breakpoint(true, line));
+                    }
+                }
+            }
+        }
+
+        response.body = {
+            breakpoints
+        };
+
+        this.sendResponse(response);
     }
 
-    protected threadsRequest (response: DebugProtocol.ThreadsResponse): void {
-      console.error('##### threadsRequest')
+    protected async threadsRequest (response: DebugProtocol.ThreadsResponse): Promise<void> {
+        console.log('##### threadsRequest');
+
+        const status = await this.client.status();
+        const threads = [];
 
-      // runtime supports no threads so just return a default thread.
-      response.body = {
-        threads: [
-          new Thread(1, 'thread 1')
-        ]
-      }
-      this.sendResponse(response)
+        if (status) {
+            for (const tid in Object.keys(status.threads)) {
+                threads.push(new Thread(parseInt(tid), `Thread ${tid}`));
+            }
+        } else {
+            threads.push(new Thread(1, 'Thread 1'));
+        }
+
+        response.body = {
+            threads
+        };
+
+        this.sendResponse(response);
     }
 
     protected stackTraceRequest (response: DebugProtocol.StackTraceResponse, args: DebugProtocol.StackTraceArguments): void {
-      console.error('##### stackTraceRequest:', args)
+        console.error('##### stackTraceRequest:', args);
 
-      response.body = {
-        stackFrames: []
-      }
-      this.sendResponse(response)
+        response.body = {
+            stackFrames: []
+        };
+        this.sendResponse(response);
     }
 
     protected scopesRequest (response: DebugProtocol.ScopesResponse, args: DebugProtocol.ScopesArguments): void {
-      console.error('##### scopesRequest:', args)
+        console.error('##### scopesRequest:', args);
 
-      response.body = {
-        scopes: []
-      }
-      this.sendResponse(response)
+        response.body = {
+            scopes: []
+        };
+        this.sendResponse(response);
     }
 
     protected async variablesRequest (response: DebugProtocol.VariablesResponse, args: DebugProtocol.VariablesArguments, request?: DebugProtocol.Request) {
-      console.error('##### variablesRequest', args, request)
+        console.error('##### variablesRequest', args, request);
 
-      response.body = {
-        variables: []
-      }
-      this.sendResponse(response)
+        response.body = {
+            variables: []
+        };
+        this.sendResponse(response);
     }
 
     protected continueRequest (response: DebugProtocol.ContinueResponse, args: DebugProtocol.ContinueArguments): void {
-      console.error('##### continueRequest', args)
-      this.sendResponse(response)
+        console.error('##### continueRequest', args);
+        this.sendResponse(response);
     }
 
     protected reverseContinueRequest (response: DebugProtocol.ReverseContinueResponse, args: DebugProtocol.ReverseContinueArguments): void {
-      console.error('##### reverseContinueRequest', args)
-      this.sendResponse(response)
+        console.error('##### reverseContinueRequest', args);
+        this.sendResponse(response);
     }
 
     protected nextRequest (response: DebugProtocol.NextResponse, args: DebugProtocol.NextArguments): void {
-      console.error('##### nextRequest', args)
-      this.sendResponse(response)
+        console.error('##### nextRequest', args);
+        this.sendResponse(response);
     }
 
     protected stepBackRequest (response: DebugProtocol.StepBackResponse, args: DebugProtocol.StepBackArguments): void {
-      console.error('##### stepBackRequest', args)
-      this.sendResponse(response)
+        console.error('##### stepBackRequest', args);
+        this.sendResponse(response);
     }
 
     protected stepInTargetsRequest (response: DebugProtocol.StepInTargetsResponse, args: DebugProtocol.StepInTargetsArguments) {
-      console.error('##### stepInTargetsRequest', args)
-      response.body = {
-        targets: []
-      }
-      this.sendResponse(response)
+        console.error('##### stepInTargetsRequest', args);
+        response.body = {
+            targets: []
+        };
+        this.sendResponse(response);
     }
 
     protected stepInRequest (response: DebugProtocol.StepInResponse, args: DebugProtocol.StepInArguments): void {
-      console.error('##### stepInRequest', args)
-      this.sendResponse(response)
+        console.error('##### stepInRequest', args);
+        this.sendResponse(response);
     }
 
     protected stepOutRequest (response: DebugProtocol.StepOutResponse, args: DebugProtocol.StepOutArguments): void {
-      console.error('##### stepOutRequest', args)
-      this.sendResponse(response)
+        console.error('##### stepOutRequest', args);
+        this.sendResponse(response);
     }
 
     protected async evaluateRequest (response: DebugProtocol.EvaluateResponse, args: DebugProtocol.EvaluateArguments): Promise<void> {
-      console.error('##### evaluateRequest', args)
+        let result: any;
+
+        try {
+            result = await this.client.sendCommandString(`${args.expression}\r\n`);
+
+            if (typeof (result) !== 'string') {
+                result = JSON.stringify(result, null, '  ');
+            }
+        } catch (e) {
+            result = String(e);
+        }
 
-      response.body = {
-        result: 'evaluate',
-        variablesReference: 0
-      }
-      this.sendResponse(response)
+        response.body = {
+            result,
+            variablesReference: 0
+        };
+
+        this.sendResponse(response);
     }
 
     protected dataBreakpointInfoRequest (response: DebugProtocol.DataBreakpointInfoResponse, args: DebugProtocol.DataBreakpointInfoArguments): void {
-      console.error('##### dataBreakpointInfoRequest', args)
+        console.error('##### dataBreakpointInfoRequest', args);
 
-      response.body = {
-        dataId: null,
-        description: 'cannot break on data access',
-        accessTypes: undefined,
-        canPersist: false
-      }
+        response.body = {
+            dataId: null,
+            description: 'cannot break on data access',
+            accessTypes: undefined,
+            canPersist: false
+        };
 
-      this.sendResponse(response)
+        this.sendResponse(response);
     }
 
     protected setDataBreakpointsRequest (response: DebugProtocol.SetDataBreakpointsResponse, args: DebugProtocol.SetDataBreakpointsArguments): void {
-      console.error('##### setDataBreakpointsRequest', args)
+        console.error('##### setDataBreakpointsRequest', args);
 
-      response.body = {
-        breakpoints: []
-      }
+        response.body = {
+            breakpoints: []
+        };
 
-      this.sendResponse(response)
+        this.sendResponse(response);
     }
 
     protected completionsRequest (response: DebugProtocol.CompletionsResponse, args: DebugProtocol.CompletionsArguments): void {
-      console.error('##### completionsRequest', args)
-
-      response.body = {
-        targets: [
-          {
-            label: 'item 10',
-            sortText: '10'
-          },
-          {
-            label: 'item 1',
-            sortText: '01'
-          },
-          {
-            label: 'item 2',
-            sortText: '02'
-          },
-          {
-            label: 'array[]',
-            selectionStart: 6,
-            sortText: '03'
-          },
-          {
-            label: 'func(arg)',
-            selectionStart: 5,
-            selectionLength: 3,
-            sortText: '04'
-          }
-        ]
-      }
-      this.sendResponse(response)
+        console.error('##### completionsRequest', args);
+
+        response.body = {
+            targets: [
+                {
+                    label: 'item 10',
+                    sortText: '10'
+                },
+                {
+                    label: 'item 1',
+                    sortText: '01'
+                },
+                {
+                    label: 'item 2',
+                    sortText: '02'
+                },
+                {
+                    label: 'array[]',
+                    selectionStart: 6,
+                    sortText: '03'
+                },
+                {
+                    label: 'func(arg)',
+                    selectionStart: 5,
+                    selectionLength: 3,
+                    sortText: '04'
+                }
+            ]
+        };
+        this.sendResponse(response);
     }
 
     protected cancelRequest (response: DebugProtocol.CancelResponse, args: DebugProtocol.CancelArguments) {
-      console.error('##### cancelRequest', args)
-      this.sendResponse(response)
+        console.error('##### cancelRequest', args);
+        this.sendResponse(response);
     }
 
     protected customRequest (command: string, response: DebugProtocol.Response, args: any) {
-      console.error('##### customRequest', args)
+        console.error('##### customRequest', args);
+
+        if (command === 'toggleFormatting') {
+            this.sendResponse(response);
+        } else {
+            super.customRequest(command, response, args);
+        }
+    }
 
-      if (command === 'toggleFormatting') {
-        this.sendResponse(response)
-      } else {
-        super.customRequest(command, response, args)
-      }
+    public shutdown () {
+        console.log('#### Shutdown');
+        this.client?.shutdown().then(() => {
+            this.extout.appendLine('Debug Session has finished');
+        }).catch(e => {
+            this.extout.appendLine(`Debug Session has finished with an error: ${e}`);
+        });
     }
 }
+
+class LogChannelAdapter {
+  private out: vscode.OutputChannel
+
+  constructor (out: vscode.OutputChannel) {
+      this.out = out;
+  }
+
+  log (value: string): void {
+      this.out.appendLine(value);
+  }
+
+  error (value: string): void {
+      this.out.appendLine(`Error: ${value}`);
+      setTimeout(() => {
+          this.out.show(true);
+      }, 500);
+  }
+}

+ 184 - 0
ecal-support/src/ecalDebugClient.ts

@@ -0,0 +1,184 @@
+/**
+ * Debug client implementation for the ECAL debugger.
+ */
+
+import * as net from 'net';
+import { EventEmitter } from 'events';
+import PromiseSocket from 'promise-socket';
+import { LogOutputStream, DebugStatus, ThreadInspection } from './types';
+
+interface BacklogCommand {
+  cmd:string,
+  args? :string[],
+}
+
+/**
+ * Debug client for ECAL debug server.
+ */
+export class ECALDebugClient extends EventEmitter {
+  private socket : PromiseSocket<net.Socket>;
+  private socketLock : any;
+  private connected : boolean = false
+  private backlog : BacklogCommand[] = []
+  private threadInspection : Record<string, ThreadInspection> = {}
+
+  /**
+     * Create a new debug client.
+     */
+  public constructor (
+        protected out: LogOutputStream
+  ) {
+    super()
+    this.socket = new PromiseSocket(new net.Socket());
+
+      const AsyncLock = require('async-lock');
+      this.socketLock = new AsyncLock();
+  }
+
+  public async conect (host: string, port: number) {
+      try {
+          this.out.log(`Connecting to: ${host}:${port}`);
+          await this.socket.connect({ port, host });
+          // this.socket.setTimeout(2000);
+          this.connected = true;
+          this.pollEvents(); // Start emitting events
+      } catch (e) {
+          this.out.error(`Could not connect to debug server: ${e}`);
+      }
+  }
+
+  public async status () : Promise<DebugStatus | null> {
+      try {
+          return await this.sendCommand('status') as DebugStatus;
+      } catch (e) {
+          this.out.error(`Could not query for status: ${e}`);
+          return null;
+      }
+  }
+
+  public async inspect (tid:string) : Promise<ThreadInspection | null> {
+      try {
+          return await this.sendCommand('inspect', [tid]) as ThreadInspection;
+      } catch (e) {
+          this.out.error(`Could not inspect thread ${tid}: ${e}`);
+          return null;
+      }
+  }
+
+  public async setBreakpoint (breakpoint: string) {
+      try {
+          await this.sendCommand(`break ${breakpoint}`) as DebugStatus;
+      } catch (e) {
+          this.out.error(`Could not set breakpoint ${breakpoint}: ${e}`);
+      }
+  }
+
+  public async clearBreakpoints (source: string) {
+      try {
+          await this.sendCommand('rmbreak', [source]) as DebugStatus;
+      } catch (e) {
+          this.out.error(`Could not remove breakpoints for ${source}: ${e}`);
+      }
+  }
+
+  public async shutdown () {
+      this.connected = false;
+      await this.socket.destroy();
+  }
+
+  /**
+   * PollEvents is the polling loop for debug events.
+   */
+  private async pollEvents () {
+      let nextLoop = 1000;
+      try {
+          const status = await this.status();
+
+          for (const [tid, thread] of Object.entries(status?.threads || [])) {
+              if (thread.threadRunning === false && !this.threadInspection[tid]) {
+
+                console.log("#### Thread was stopped!!")
+
+                // A thread was stopped inspect it
+
+                let inspection : ThreadInspection = {
+                    callstack: [],
+                    threadRunning: false
+                }
+
+                try {
+                    inspection = await this.sendCommand('describe', [tid]) as ThreadInspection;
+                } catch (e) {
+                    this.out.error(`Could not get description for ${tid}: ${e}`);
+                }
+
+                this.threadInspection[tid] = inspection;
+
+                console.log("#### Description result:", inspection)
+
+                this.emit('pauseOnBreakpoint', tid);
+            }
+          }
+
+      } catch (e) {
+          this.out.error(`Error during event loop: ${e}`);
+          nextLoop = 5000;
+      }
+
+      if (this.connected) {
+          setTimeout(this.pollEvents.bind(this), nextLoop);
+      } else {
+          this.out.log('Stop emitting events' + nextLoop);
+      }
+  }
+
+  public async sendCommand (cmd:string, args? :string[]): Promise<any> {
+      // Create or process the backlog depending on the connection status
+
+      if (!this.connected) {
+          this.backlog.push({
+              cmd,
+              args
+          });
+          return null;
+      } else if (this.backlog.length > 0) {
+          const backlog = this.backlog;
+          this.backlog = [];
+          for (const item of backlog) {
+              await this.sendCommand(item.cmd, item.args);
+          }
+      }
+
+      return await this.sendCommandString(`##${cmd} ${args ? args.join(' ') : ''}\r\n`);
+  }
+
+  public async sendCommandString (cmdString:string): Promise<any> {
+      // Socket needs to be locked. Reading and writing to the socket is seen
+      // by the interpreter as async (i/o bound) code. Separate calls to
+      // sendCommand will be executed in different event loops. Without the lock
+      // the different sendCommand calls would mix their responses.
+
+      return await this.socketLock.acquire('socket', async () => {
+          await this.socket.write(cmdString, 'utf8');
+
+          let text = '';
+          while (!text.endsWith('\n\n')) {
+              text += await this.socket.read(1);
+          }
+
+          let res : any = {};
+          try {
+              res = JSON.parse(text);
+          } catch (e) {
+              throw (new Error(`Could not parse response: ${text} - error:${e}`));
+          }
+          if (res?.DebuggerError) {
+              throw (new Error(`Unexpected internal error for command "${cmdString}": ${res.DebuggerError}`));
+          }
+          if (res?.EncodedOutput !== undefined) {
+              res = Buffer.from(res.EncodedOutput, 'base64').toString('utf8');
+          }
+          return res;
+      });
+  }
+}

+ 10 - 10
ecal-support/src/extension.ts

@@ -1,19 +1,19 @@
-import * as vscode from 'vscode'
-import { ProviderResult } from 'vscode'
-import { ECALDebugSession } from './ecalDebugAdapter'
+import * as vscode from 'vscode';
+import { ProviderResult } from 'vscode';
+import { ECALDebugSession } from './ecalDebugAdapter';
 
 export function activate (context: vscode.ExtensionContext) {
-  context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory(
-    'ecaldebug', new InlineDebugAdapterFactory()))
+    context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory(
+        'ecaldebug', new InlineDebugAdapterFactory()));
 }
 
 export function deactivate () {
 }
 
 class InlineDebugAdapterFactory implements vscode.DebugAdapterDescriptorFactory {
-  createDebugAdapterDescriptor (_session: vscode.DebugSession): ProviderResult<vscode.DebugAdapterDescriptor> {
-    // Declare the ECALDebugSession as an DebugAdapterInlineImplementation so extention and adapter can
-    // run in-process (making it possible to easily debug the adapter)
-    return new vscode.DebugAdapterInlineImplementation(new ECALDebugSession())
-  }
+    createDebugAdapterDescriptor (_session: vscode.DebugSession): ProviderResult<vscode.DebugAdapterDescriptor> {
+        // Declare the ECALDebugSession as an DebugAdapterInlineImplementation so extention and adapter can
+        // run in-process (making it possible to easily debug the adapter)
+        return new vscode.DebugAdapterInlineImplementation(new ECALDebugSession());
+    }
 }

+ 24 - 0
ecal-support/src/types.ts

@@ -0,0 +1,24 @@
+export interface ThreadInspection {
+    callstack: string[]
+    threadRunning: boolean
+}
+
+export interface ThreadStatus {
+    callstack: string[]
+    threadRunning?: boolean
+}
+
+export interface DebugStatus {
+    breakonstart: boolean,
+    breakpoints: any,
+    sources: string[],
+    threads: Record<number, ThreadStatus>
+}
+
+/**
+ * Log output stream for this client.
+ */
+export interface LogOutputStream {
+    log(value: string): void;
+    error(value: string): void;
+}

+ 15 - 0
examples/fib/.vscode/launch.json

@@ -0,0 +1,15 @@
+{
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "type": "ecaldebug",
+            "request": "launch",
+            "name": "Debug ECAL script with ECAL Debug Server",
+            "host": "localhost",
+            "port": 33274,
+            "dir": "${workspaceFolder}",
+            "executeOnEntry": true,
+            "trace": true
+        }
+    ]
+}

+ 2 - 0
examples/fib/debug.sh

@@ -0,0 +1,2 @@
+#!/bin/sh
+../../ecal debug -server -echo fib.ecal

+ 4 - 4
examples/fib/lib.ecal

@@ -1,11 +1,11 @@
 # Library for fib
 
-/* 
+/*
 fib calculates the fibonacci series using recursion.
 */
-func fib(n) { 
+func fib(n) {
     if (n <= 1) {
         return n
-    } 
+    }
     return fib(n-1) + fib(n-2)
-} 
+}

+ 0 - 13
examples/game_of_life/.vscode/launch.json

@@ -1,13 +0,0 @@
-{
-    "version": "0.2.0",
-    "configurations": [		{
-        "type": "ecaldebug",
-        "request": "launch",
-        "name": "Debug ECAL script with ECAL Debug Server",
-
-        "serverURL": "localhost:43806",
-        "dir": "${workspaceFolder}",
-        "executeOnEntry": true,
-        "trace": true,
-    }]
-}

+ 9 - 1
interpreter/debug.go

@@ -317,7 +317,15 @@ RemoveBreakPoint removes a break point.
 func (ed *ecalDebugger) RemoveBreakPoint(source string, line int) {
 	ed.lock.Lock()
 	defer ed.lock.Unlock()
-	delete(ed.breakPoints, fmt.Sprintf("%v:%v", source, line))
+	if line > 0 {
+		delete(ed.breakPoints, fmt.Sprintf("%v:%v", source, line))
+	} else {
+		for k := range ed.breakPoints {
+			if ksource := strings.Split(k, ":")[0]; ksource == source {
+				delete(ed.breakPoints, k)
+			}
+		}
+	}
 }
 
 /*

+ 6 - 2
interpreter/debug_cmd.go

@@ -140,7 +140,7 @@ convert the output data into a JSON string.
 */
 func (c *rmBreakpointCommand) Run(debugger util.ECALDebugger, args []string) (interface{}, error) {
 	if len(args) == 0 {
-		return nil, fmt.Errorf("Need a break target (<source>:<line>) as first parameter")
+		return nil, fmt.Errorf("Need a break target (<source>[:<line>]) as first parameter")
 	}
 
 	targetSplit := strings.Split(args[0], ":")
@@ -153,9 +153,13 @@ func (c *rmBreakpointCommand) Run(debugger util.ECALDebugger, args []string) (in
 
 			return nil, nil
 		}
+
+	} else {
+
+		debugger.RemoveBreakPoint(args[0], -1)
 	}
 
-	return nil, fmt.Errorf("Invalid break target - should be <source>:<line>")
+	return nil, nil
 }
 
 /*

+ 32 - 7
interpreter/debug_test.go

@@ -172,6 +172,37 @@ test3`[1:] {
       "callStack": []
     }
   }
+}` {
+		t.Error("Unexpected result:", outString, err)
+		return
+	}
+
+	if _, err = testDebugger.HandleInput("break ECALEvalTest:4"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	if _, err = testDebugger.HandleInput("rmbreak ECALEvalTest"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	out, err = testDebugger.HandleInput(fmt.Sprintf("status"))
+
+	outBytes, _ = json.MarshalIndent(out, "", "  ")
+	outString = string(outBytes)
+
+	if err != nil || outString != `{
+  "breakonstart": false,
+  "breakpoints": {},
+  "sources": [
+    "ECALEvalTest"
+  ],
+  "threads": {
+    "1": {
+      "callStack": []
+    }
+  }
 }` {
 		t.Error("Unexpected result:", outString, err)
 		return
@@ -1076,13 +1107,7 @@ func TestDebuggingErrorInput(t *testing.T) {
 	}
 
 	if _, err = testDebugger.HandleInput("rmbreak"); err == nil ||
-		err.Error() != `Need a break target (<source>:<line>) as first parameter` {
-		t.Error("Unexpected result:", err)
-		return
-	}
-
-	if _, err = testDebugger.HandleInput("rmbreak foo"); err == nil ||
-		err.Error() != `Invalid break target - should be <source>:<line>` {
+		err.Error() != `Need a break target (<source>[:<line>]) as first parameter` {
 		t.Error("Unexpected result:", err)
 		return
 	}