Browse Source

feat: Adding reload support

Matthias Ladkau 3 years ago
parent
commit
a9c9f2eb92

+ 1 - 3
cli/ecal.go

@@ -22,12 +22,10 @@ import (
 /*
 /*
 TODO:
 TODO:
 - create executable binary (pack into single binary)
 - create executable binary (pack into single binary)
-- debug server support (vscode)
-- debug support break on start
 - debug support errors
 - debug support errors
 - pretty printer
 - pretty printer
 - local variable definition (let)
 - local variable definition (let)
-- watch files
+- reload on start for debugger adapter
 */
 */
 
 
 func main() {
 func main() {

+ 16 - 1
cli/tool/debug.go

@@ -39,13 +39,14 @@ type CLIDebugInterpreter struct {
 	RunDebugServer  *bool   // Run a debug server
 	RunDebugServer  *bool   // Run a debug server
 	EchoDebugServer *bool   // Echo all input and output of the 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.
 	Interactive     *bool   // Flag if the interpreter should open a console in the current tty.
+	BreakOnStart    *bool   // Flag if the debugger should stop the execution on start
 }
 }
 
 
 /*
 /*
 NewCLIDebugInterpreter wraps an existing CLIInterpreter object and adds capabilities.
 NewCLIDebugInterpreter wraps an existing CLIInterpreter object and adds capabilities.
 */
 */
 func NewCLIDebugInterpreter(i *CLIInterpreter) *CLIDebugInterpreter {
 func NewCLIDebugInterpreter(i *CLIInterpreter) *CLIDebugInterpreter {
-	return &CLIDebugInterpreter{i, nil, nil, nil, nil}
+	return &CLIDebugInterpreter{i, nil, nil, nil, nil, nil}
 }
 }
 
 
 /*
 /*
@@ -61,6 +62,7 @@ func (i *CLIDebugInterpreter) ParseArgs() bool {
 	i.RunDebugServer = flag.Bool("server", false, "Run a debug server")
 	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.EchoDebugServer = flag.Bool("echo", false, "Echo all i/o of the debug server")
 	i.Interactive = flag.Bool("interactive", true, "Run interactive console")
 	i.Interactive = flag.Bool("interactive", true, "Run interactive console")
+	i.BreakOnStart = flag.Bool("breakonstart", false, "Stop the execution on start")
 
 
 	return i.CLIInterpreter.ParseArgs()
 	return i.CLIInterpreter.ParseArgs()
 }
 }
@@ -90,12 +92,16 @@ func (i *CLIDebugInterpreter) Interpret() error {
 		// Set debug object on the runtime provider
 		// Set debug object on the runtime provider
 
 
 		i.RuntimeProvider.Debugger = interpreter.NewECALDebugger(i.GlobalVS)
 		i.RuntimeProvider.Debugger = interpreter.NewECALDebugger(i.GlobalVS)
+		i.RuntimeProvider.Debugger.BreakOnStart(*i.BreakOnStart)
 
 
 		// Set this object as a custom handler to deal with input.
 		// Set this object as a custom handler to deal with input.
 
 
 		i.CustomHandler = i
 		i.CustomHandler = i
 
 
 		if *i.RunDebugServer {
 		if *i.RunDebugServer {
+
+			// Start the debug server
+
 			debugServer := &debugTelnetServer{*i.DebugServerAddr, "ECALDebugServer: ",
 			debugServer := &debugTelnetServer{*i.DebugServerAddr, "ECALDebugServer: ",
 				nil, true, *i.EchoDebugServer, i, i.RuntimeProvider.Logger}
 				nil, true, *i.EchoDebugServer, i, i.RuntimeProvider.Logger}
 			go debugServer.Run()
 			go debugServer.Run()
@@ -114,6 +120,15 @@ func (i *CLIDebugInterpreter) Interpret() error {
 	return err
 	return err
 }
 }
 
 
+/*
+LoadInitialFile clears the global scope and reloads the initial file.
+*/
+func (i *CLIDebugInterpreter) LoadInitialFile(tid uint64) error {
+	i.RuntimeProvider.Debugger.StopThreads(500 * time.Millisecond)
+	i.RuntimeProvider.Debugger.BreakOnStart(*i.BreakOnStart)
+	return nil
+}
+
 /*
 /*
 CanHandle checks if a given string can be handled by this handler.
 CanHandle checks if a given string can be handled by this handler.
 */
 */

+ 48 - 16
cli/tool/interpret.go

@@ -29,6 +29,18 @@ import (
 	"devt.de/krotik/ecal/util"
 	"devt.de/krotik/ecal/util"
 )
 )
 
 
+/*
+CLICustomHandler is a handler for custom operations.
+*/
+type CLICustomHandler interface {
+	CLIInputHandler
+
+	/*
+	   LoadInitialFile clears the global scope and reloads the initial file.
+	*/
+	LoadInitialFile(tid uint64) error
+}
+
 /*
 /*
 CLIInterpreter is a commandline interpreter for ECAL.
 CLIInterpreter is a commandline interpreter for ECAL.
 */
 */
@@ -38,7 +50,7 @@ type CLIInterpreter struct {
 
 
 	// Customizations of output and input handling
 	// Customizations of output and input handling
 
 
-	CustomHandler        CLIInputHandler
+	CustomHandler        CLICustomHandler
 	CustomWelcomeMessage string
 	CustomWelcomeMessage string
 	CustomHelpString     string
 	CustomHelpString     string
 
 
@@ -140,6 +152,33 @@ func (i *CLIInterpreter) CreateRuntimeProvider(name string) error {
 	return err
 	return err
 }
 }
 
 
+/*
+LoadInitialFile clears the global scope and reloads the initial file.
+*/
+func (i *CLIInterpreter) LoadInitialFile(tid uint64) error {
+	var err error
+
+	i.CustomHandler.LoadInitialFile(tid)
+
+	i.GlobalVS.Clear()
+
+	if cargs := flag.Args(); len(cargs) > 0 {
+		var ast *parser.ASTNode
+		var initFile []byte
+
+		initFileName := flag.Arg(0)
+		initFile, err = ioutil.ReadFile(initFileName)
+
+		if ast, err = parser.ParseWithRuntime(initFileName, string(initFile), i.RuntimeProvider); err == nil {
+			if err = ast.Runtime.Validate(); err == nil {
+				_, err = ast.Runtime.Eval(i.GlobalVS, make(map[string]interface{}), tid)
+			}
+		}
+	}
+
+	return err
+}
+
 /*
 /*
 Interpret starts the ECAL code interpreter. Starts an interactive console in
 Interpret starts the ECAL code interpreter. Starts an interactive console in
 the current tty if the interactive flag is set.
 the current tty if the interactive flag is set.
@@ -178,21 +217,7 @@ func (i *CLIInterpreter) Interpret(interactive bool) error {
 
 
 			// Execute file if given
 			// Execute file if given
 
 
-			if cargs := flag.Args(); len(cargs) > 0 {
-				var ast *parser.ASTNode
-				var initFile []byte
-
-				initFileName := flag.Arg(0)
-				initFile, err = ioutil.ReadFile(initFileName)
-
-				if ast, err = parser.ParseWithRuntime(initFileName, string(initFile), i.RuntimeProvider); err == nil {
-					if err = ast.Runtime.Validate(); err == nil {
-						_, err = ast.Runtime.Eval(i.GlobalVS, make(map[string]interface{}), tid)
-					}
-				}
-			}
-
-			if err == nil {
+			if err = i.LoadInitialFile(tid); err == nil {
 
 
 				if interactive {
 				if interactive {
 
 
@@ -255,6 +280,7 @@ func (i *CLIInterpreter) HandleInput(ot OutputTerminal, line string, tid uint64)
 		ot.WriteString(fmt.Sprint("\n"))
 		ot.WriteString(fmt.Sprint("\n"))
 		ot.WriteString(fmt.Sprint("Console supports all normal ECAL statements and the following special commands:\n"))
 		ot.WriteString(fmt.Sprint("Console supports all normal ECAL statements and the following special commands:\n"))
 		ot.WriteString(fmt.Sprint("\n"))
 		ot.WriteString(fmt.Sprint("\n"))
+		ot.WriteString(fmt.Sprint("    @reload - Clear the interpreter and reload the initial file if it was given.\n"))
 		ot.WriteString(fmt.Sprint("    @sym [glob] - List all available inbuild functions and available stdlib packages of ECAL.\n"))
 		ot.WriteString(fmt.Sprint("    @sym [glob] - List all available inbuild functions and available stdlib packages of ECAL.\n"))
 		ot.WriteString(fmt.Sprint("    @std <package> [glob] - List all available constants and functions of a stdlib package.\n"))
 		ot.WriteString(fmt.Sprint("    @std <package> [glob] - List all available constants and functions of a stdlib package.\n"))
 		if i.CustomHelpString != "" {
 		if i.CustomHelpString != "" {
@@ -263,6 +289,12 @@ func (i *CLIInterpreter) HandleInput(ot OutputTerminal, line string, tid uint64)
 		ot.WriteString(fmt.Sprint("\n"))
 		ot.WriteString(fmt.Sprint("\n"))
 		ot.WriteString(fmt.Sprint("Add an argument after a list command to do a full text search. The search string should be in glob format.\n"))
 		ot.WriteString(fmt.Sprint("Add an argument after a list command to do a full text search. The search string should be in glob format.\n"))
 
 
+	} else if strings.HasPrefix(line, "@reload") {
+
+		// Reload happens in a separate thread as it may be suspended on start
+
+		go i.LoadInitialFile(i.RuntimeProvider.NewThreadID())
+
 	} else if strings.HasPrefix(line, "@sym") {
 	} else if strings.HasPrefix(line, "@sym") {
 		i.displaySymbols(ot, strings.Split(line, " ")[1:])
 		i.displaySymbols(ot, strings.Split(line, " ")[1:])
 
 

+ 9 - 7
engine/pool/threadpool.go

@@ -473,6 +473,14 @@ run lets this worker run tasks.
 */
 */
 func (w *ThreadPoolWorker) run() {
 func (w *ThreadPoolWorker) run() {
 
 
+	defer func() {
+		// Remove worker from workerMap
+
+		w.pool.workerMapLock.Lock()
+		delete(w.pool.workerMap, w.id)
+		w.pool.workerMapLock.Unlock()
+	}()
+
 	for true {
 	for true {
 
 
 		// Try to get the next task
 		// Try to get the next task
@@ -508,12 +516,6 @@ func (w *ThreadPoolWorker) run() {
 			w.pool.workerMapLock.Unlock()
 			w.pool.workerMapLock.Unlock()
 		}
 		}
 	}
 	}
-
-	// Remove worker from workerMap
-
-	w.pool.workerMapLock.Lock()
-	delete(w.pool.workerMap, w.id)
-	w.pool.workerMapLock.Unlock()
 }
 }
 
 
 /*
 /*
@@ -528,8 +530,8 @@ Run the idle task.
 */
 */
 func (t *idleTask) Run(tid uint64) error {
 func (t *idleTask) Run(tid uint64) error {
 	t.tp.newTaskCond.L.Lock()
 	t.tp.newTaskCond.L.Lock()
+	defer t.tp.newTaskCond.L.Unlock()
 	t.tp.newTaskCond.Wait()
 	t.tp.newTaskCond.Wait()
-	t.tp.newTaskCond.L.Unlock()
 	return nil
 	return nil
 }
 }
 
 

+ 41 - 2
interpreter/debug.go

@@ -15,8 +15,10 @@ package interpreter
 
 
 import (
 import (
 	"fmt"
 	"fmt"
+	"runtime"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
+	"time"
 
 
 	"devt.de/krotik/common/datautil"
 	"devt.de/krotik/common/datautil"
 	"devt.de/krotik/common/errorutil"
 	"devt.de/krotik/common/errorutil"
@@ -38,7 +40,7 @@ type ecalDebugger struct {
 	breakOnStart               bool                                // Flag to stop at the start of the next execution
 	breakOnStart               bool                                // Flag to stop at the start of the next execution
 	globalScope                parser.Scope                        // Global variable scope which can be used to transfer data
 	globalScope                parser.Scope                        // Global variable scope which can be used to transfer data
 	lock                       *sync.RWMutex                       // Lock for this debugger
 	lock                       *sync.RWMutex                       // Lock for this debugger
-
+	lastVisit                  int64                               // Last time the debugger had a state visit
 }
 }
 
 
 /*
 /*
@@ -67,6 +69,7 @@ const (
 	StepOut                          // Step out of the current function
 	StepOut                          // Step out of the current function
 	StepOver                         // Step over the next function
 	StepOver                         // Step over the next function
 	Resume                           // Resume execution - do not break again on the same line
 	Resume                           // Resume execution - do not break again on the same line
+	Kill                             // Resume execution - and kill the thread on the next state change
 )
 )
 
 
 /*
 /*
@@ -97,6 +100,7 @@ func NewECALDebugger(globalVS parser.Scope) util.ECALDebugger {
 		breakOnStart:               false,
 		breakOnStart:               false,
 		globalScope:                globalVS,
 		globalScope:                globalVS,
 		lock:                       &sync.RWMutex{},
 		lock:                       &sync.RWMutex{},
+		lastVisit:                  0,
 	}
 	}
 }
 }
 
 
@@ -124,6 +128,36 @@ func (ed *ecalDebugger) HandleInput(input string) (interface{}, error) {
 	return res, err
 	return res, err
 }
 }
 
 
+/*
+StopThreads will continue all suspended threads and set them to be killed.
+Returns true if a waiting thread was resumed. Can wait for threads to end
+by ensuring that for at least d time no state change occured.
+*/
+func (ed *ecalDebugger) StopThreads(d time.Duration) bool {
+	var ret = false
+
+	for _, is := range ed.interrogationStates {
+		if is.running == false {
+			ret = true
+			is.cmd = Kill
+			is.running = true
+			is.cond.L.Lock()
+			is.cond.Broadcast()
+			is.cond.L.Unlock()
+		}
+	}
+
+	if ret && d > 0 {
+		var lastVisit int64 = -1
+		for lastVisit != ed.lastVisit {
+			lastVisit = ed.lastVisit
+			time.Sleep(d)
+		}
+	}
+
+	return ret
+}
+
 /*
 /*
 Break on the start of the next execution.
 Break on the start of the next execution.
 */
 */
@@ -140,6 +174,7 @@ func (ed *ecalDebugger) VisitState(node *parser.ASTNode, vs parser.Scope, tid ui
 
 
 	ed.lock.RLock()
 	ed.lock.RLock()
 	_, ok := ed.callStacks[tid]
 	_, ok := ed.callStacks[tid]
+	ed.lastVisit = time.Now().UnixNano()
 	ed.lock.RUnlock()
 	ed.lock.RUnlock()
 
 
 	if !ok {
 	if !ok {
@@ -170,7 +205,7 @@ func (ed *ecalDebugger) VisitState(node *parser.ASTNode, vs parser.Scope, tid ui
 			// The thread is being interrogated
 			// The thread is being interrogated
 
 
 			switch is.cmd {
 			switch is.cmd {
-			case Resume:
+			case Resume, Kill:
 				if is.node.Token.Lline != node.Token.Lline {
 				if is.node.Token.Lline != node.Token.Lline {
 
 
 					// Remove the resume command once we are on a different line
 					// Remove the resume command once we are on a different line
@@ -179,6 +214,10 @@ func (ed *ecalDebugger) VisitState(node *parser.ASTNode, vs parser.Scope, tid ui
 					delete(ed.interrogationStates, tid)
 					delete(ed.interrogationStates, tid)
 					ed.lock.Unlock()
 					ed.lock.Unlock()
 
 
+					if is.cmd == Kill {
+						runtime.Goexit()
+					}
+
 					return ed.VisitState(node, vs, tid)
 					return ed.VisitState(node, vs, tid)
 				}
 				}
 			case Stop, StepIn, StepOver:
 			case Stop, StepIn, StepOver:

+ 68 - 0
interpreter/debug_test.go

@@ -213,6 +213,74 @@ test3`[1:] {
 	}
 	}
 }
 }
 
 
+func TestDebugReset(t *testing.T) {
+	var err error
+
+	defer func() {
+		testDebugger = nil
+	}()
+
+	testDebugger = NewECALDebugger(nil)
+
+	if _, err = testDebugger.HandleInput("break ECALEvalTest:3"); err != nil {
+		t.Error("Unexpected result:", err)
+		return
+	}
+
+	wg := &sync.WaitGroup{}
+	wg.Add(1)
+
+	go func() {
+		defer wg.Done()
+
+		_, err = UnitTestEval(`
+log("test1")
+log("test2")
+log("test3")
+`, nil)
+		if err != nil {
+			t.Error(err)
+		}
+	}()
+
+	waitForThreadSuspension(t)
+
+	out, err := testDebugger.HandleInput(fmt.Sprintf("status"))
+
+	outBytes, _ := json.MarshalIndent(out, "", "  ")
+	outString := string(outBytes)
+
+	if err != nil || outString != `{
+  "breakonstart": false,
+  "breakpoints": {
+    "ECALEvalTest:3": true
+  },
+  "sources": [
+    "ECALEvalTest"
+  ],
+  "threads": {
+    "1": {
+      "callStack": [],
+      "threadRunning": false
+    }
+  }
+}` {
+		t.Error("Unexpected result:", outString, err)
+		return
+	}
+
+	testDebugger.StopThreads(100 * time.Millisecond)
+
+	wg.Wait()
+
+	if err != nil || testlogger.String() != `
+test1
+test2`[1:] {
+		t.Error("Unexpected result:", testlogger.String(), err)
+		return
+	}
+}
+
 func TestConcurrentDebugging(t *testing.T) {
 func TestConcurrentDebugging(t *testing.T) {
 	var err error
 	var err error
 
 

+ 6 - 0
parser/runtime.go

@@ -59,6 +59,12 @@ type Scope interface {
 	*/
 	*/
 	NewChild(name string) Scope
 	NewChild(name string) Scope
 
 
+	/*
+		Clear clears this scope of all stored values. This will clear children scopes
+		but not remove parent scopes.
+	*/
+	Clear()
+
 	/*
 	/*
 	   Parent returns the parent scope or nil.
 	   Parent returns the parent scope or nil.
 	*/
 	*/

+ 9 - 0
scope/varsscope.go

@@ -100,6 +100,15 @@ func (s *varsScope) Name() string {
 	return s.name
 	return s.name
 }
 }
 
 
+/*
+Clear clears this scope of all stored values. This will clear children scopes
+but not remove parent scopes.
+*/
+func (s *varsScope) Clear() {
+	s.children = nil
+	s.storage = make(map[string]interface{})
+}
+
 /*
 /*
 Parent returns the parent scope or nil.
 Parent returns the parent scope or nil.
 */
 */

+ 14 - 0
scope/varsscope_test.go

@@ -357,6 +357,20 @@ func TestVarScopeDump(t *testing.T) {
     sink: 2 {
     sink: 2 {
         g (int) : 2
         g (int) : 2
     }
     }
+}` {
+		t.Error("Unexpected result:", res)
+		return
+	}
+
+	sinkVs1.Clear()
+
+	if res := sinkVs1.String(); res != `global {
+    0 (int) : 0
+    global2 {
+        a (int) : 1
+        sink: 1 {
+        }
+    }
 }` {
 }` {
 		t.Error("Unexpected result:", res)
 		t.Error("Unexpected result:", res)
 		return
 		return

+ 9 - 0
util/types.go

@@ -11,6 +11,8 @@
 package util
 package util
 
 
 import (
 import (
+	"time"
+
 	"devt.de/krotik/ecal/parser"
 	"devt.de/krotik/ecal/parser"
 )
 )
 
 
@@ -99,6 +101,13 @@ type ECALDebugger interface {
 	*/
 	*/
 	HandleInput(input string) (interface{}, error)
 	HandleInput(input string) (interface{}, error)
 
 
+	/*
+	   StopThreads will continue all suspended threads and set them to be killed.
+	   Returns true if a waiting thread was resumed. Can wait for threads to end
+	   by ensuring that for at least d time no state change occured.
+	*/
+	StopThreads(d time.Duration) bool
+
 	/*
 	/*
 	   Break on the start of the next execution.
 	   Break on the start of the next execution.
 	*/
 	*/