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:
 - create executable binary (pack into single binary)
-- debug server support (vscode)
-- debug support break on start
 - debug support errors
 - pretty printer
 - local variable definition (let)
-- watch files
+- reload on start for debugger adapter
 */
 
 func main() {

+ 16 - 1
cli/tool/debug.go

@@ -39,13 +39,14 @@ type CLIDebugInterpreter struct {
 	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.
+	BreakOnStart    *bool   // Flag if the debugger should stop the execution on start
 }
 
 /*
 NewCLIDebugInterpreter wraps an existing CLIInterpreter object and adds capabilities.
 */
 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.EchoDebugServer = flag.Bool("echo", false, "Echo all i/o of the debug server")
 	i.Interactive = flag.Bool("interactive", true, "Run interactive console")
+	i.BreakOnStart = flag.Bool("breakonstart", false, "Stop the execution on start")
 
 	return i.CLIInterpreter.ParseArgs()
 }
@@ -90,12 +92,16 @@ func (i *CLIDebugInterpreter) Interpret() error {
 		// Set debug object on the runtime provider
 
 		i.RuntimeProvider.Debugger = interpreter.NewECALDebugger(i.GlobalVS)
+		i.RuntimeProvider.Debugger.BreakOnStart(*i.BreakOnStart)
 
 		// Set this object as a custom handler to deal with input.
 
 		i.CustomHandler = i
 
 		if *i.RunDebugServer {
+
+			// Start the debug server
+
 			debugServer := &debugTelnetServer{*i.DebugServerAddr, "ECALDebugServer: ",
 				nil, true, *i.EchoDebugServer, i, i.RuntimeProvider.Logger}
 			go debugServer.Run()
@@ -114,6 +120,15 @@ func (i *CLIDebugInterpreter) Interpret() error {
 	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.
 */

+ 48 - 16
cli/tool/interpret.go

@@ -29,6 +29,18 @@ import (
 	"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.
 */
@@ -38,7 +50,7 @@ type CLIInterpreter struct {
 
 	// Customizations of output and input handling
 
-	CustomHandler        CLIInputHandler
+	CustomHandler        CLICustomHandler
 	CustomWelcomeMessage string
 	CustomHelpString     string
 
@@ -140,6 +152,33 @@ func (i *CLIInterpreter) CreateRuntimeProvider(name string) error {
 	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
 the current tty if the interactive flag is set.
@@ -178,21 +217,7 @@ func (i *CLIInterpreter) Interpret(interactive bool) error {
 
 			// 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 {
 
@@ -255,6 +280,7 @@ func (i *CLIInterpreter) HandleInput(ot OutputTerminal, line string, tid uint64)
 		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("\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("    @std <package> [glob] - List all available constants and functions of a stdlib package.\n"))
 		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("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") {
 		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() {
 
+	defer func() {
+		// Remove worker from workerMap
+
+		w.pool.workerMapLock.Lock()
+		delete(w.pool.workerMap, w.id)
+		w.pool.workerMapLock.Unlock()
+	}()
+
 	for true {
 
 		// Try to get the next task
@@ -508,12 +516,6 @@ func (w *ThreadPoolWorker) run() {
 			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 {
 	t.tp.newTaskCond.L.Lock()
+	defer t.tp.newTaskCond.L.Unlock()
 	t.tp.newTaskCond.Wait()
-	t.tp.newTaskCond.L.Unlock()
 	return nil
 }
 

+ 41 - 2
interpreter/debug.go

@@ -15,8 +15,10 @@ package interpreter
 
 import (
 	"fmt"
+	"runtime"
 	"strings"
 	"sync"
+	"time"
 
 	"devt.de/krotik/common/datautil"
 	"devt.de/krotik/common/errorutil"
@@ -38,7 +40,7 @@ type ecalDebugger struct {
 	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
 	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
 	StepOver                         // Step over the next function
 	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,
 		globalScope:                globalVS,
 		lock:                       &sync.RWMutex{},
+		lastVisit:                  0,
 	}
 }
 
@@ -124,6 +128,36 @@ func (ed *ecalDebugger) HandleInput(input string) (interface{}, error) {
 	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.
 */
@@ -140,6 +174,7 @@ func (ed *ecalDebugger) VisitState(node *parser.ASTNode, vs parser.Scope, tid ui
 
 	ed.lock.RLock()
 	_, ok := ed.callStacks[tid]
+	ed.lastVisit = time.Now().UnixNano()
 	ed.lock.RUnlock()
 
 	if !ok {
@@ -170,7 +205,7 @@ func (ed *ecalDebugger) VisitState(node *parser.ASTNode, vs parser.Scope, tid ui
 			// The thread is being interrogated
 
 			switch is.cmd {
-			case Resume:
+			case Resume, Kill:
 				if is.node.Token.Lline != node.Token.Lline {
 
 					// 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)
 					ed.lock.Unlock()
 
+					if is.cmd == Kill {
+						runtime.Goexit()
+					}
+
 					return ed.VisitState(node, vs, tid)
 				}
 			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) {
 	var err error
 

+ 6 - 0
parser/runtime.go

@@ -59,6 +59,12 @@ type Scope interface {
 	*/
 	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.
 	*/

+ 9 - 0
scope/varsscope.go

@@ -100,6 +100,15 @@ func (s *varsScope) Name() string {
 	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.
 */

+ 14 - 0
scope/varsscope_test.go

@@ -357,6 +357,20 @@ func TestVarScopeDump(t *testing.T) {
     sink: 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)
 		return

+ 9 - 0
util/types.go

@@ -11,6 +11,8 @@
 package util
 
 import (
+	"time"
+
 	"devt.de/krotik/ecal/parser"
 )
 
@@ -99,6 +101,13 @@ type ECALDebugger interface {
 	*/
 	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.
 	*/