Skip to content

Commit

Permalink
feat: add an interactive debugger to GnoVM (#1563)
Browse files Browse the repository at this point in the history
![gno-debug](https://github.com/gnolang/gno/assets/5792239/96c50686-df6c-4dd8-a1d1-63f787a9328d)

We provide here an embedded interactive debugger to let the user control
and inspect its program at symbolic level, with the same features and
commands as classical debuggers: gdb, lldb or delve.

The debugger is enabled by setting the `-debug` flag in `gno run`
command, which loads the target program and immediately shows a debugger
prompt on the console:

	$ gno run -debug /tmp/my-program.gno
	Welcome to the Gnovm debugger. Type 'help' for list of commands.
	dbg>

Providing `-debug-addr` flag allows to start a remote debugging session,
and not interfer with the program stdin and stdout. For example, in a
first terminal:

	$ gno run -debug-addr :4000 /tmp/my-program.gno
	Waiting for debugger client to connect at :4000

And in a second terminal, using a netcat like nc(1):

	$ nc localhost 4000
	Welcome to the Gnovm debugger. Type 'help' for list of commands.
	dbg>

The debugger works by intercepting each execution step at virtual
machine level (each iteration within `Machine.Run` loop) to a callback,
which in turns can provide a debugger command REPL or check if the
execution can proceed to the next step, etc.

The general logic and structure is there. It is possible to `continue`,
`stepi`, `detach`, `print`, `stack`, etc and get a general feedback of
the user experience and the impact on the code.

Efforts are made to make this feature minimally intrusive in the actual
VM, and not interfering when the debugger is not used.

It is planned shortly after this PR is integrated to add the capacity to
attach to an already running program, and to taylor the data format for
existing debugging environments such as VScode, etc, as demand arises.

Resolves gnolang/hackerspace#54

<!-- please provide a detailed description of the changes made in this
pull request. -->

<details><summary>Contributors' checklist...</summary>

- [x] Added new tests, or not needed, or not feasible
- [x] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [x] Updated the official documentation or not needed
- [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [x] Added references to related issues and PRs
- [x] Provided any useful hints for running manual tests
- [ ] Added new benchmarks to [generated
graphs](https://gnoland.github.io/benchmarks), if any. More info
[here](https://github.com/gnolang/gno/blob/master/.benchmarks/README.md).
</details>

---------

Co-authored-by: Morgan <[email protected]>
  • Loading branch information
mvertes and thehowl authored May 14, 2024
1 parent 8afb1a4 commit a901e79
Show file tree
Hide file tree
Showing 8 changed files with 1,063 additions and 5 deletions.
4 changes: 2 additions & 2 deletions gnovm/cmd/gno/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func testMainCaseRun(t *testing.T, tc []testMainCase) {
if r := recover(); r != nil {
output := fmt.Sprintf("%v", r)
t.Log("recover", output)
require.False(t, recoverShouldBeEmpty, "should panic")
require.False(t, recoverShouldBeEmpty, "should not panic")
require.True(t, errShouldBeEmpty, "should not return an error")
if test.recoverShouldContain != "" {
require.Regexpf(t, test.recoverShouldContain, output, "recover should contain")
Expand All @@ -100,7 +100,7 @@ func testMainCaseRun(t *testing.T, tc []testMainCase) {
}
checkOutputs(t)
} else {
require.True(t, recoverShouldBeEmpty, "should not panic")
require.True(t, recoverShouldBeEmpty, "should panic")
}
}()

Expand Down
31 changes: 28 additions & 3 deletions gnovm/cmd/gno/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ import (
)

type runCfg struct {
verbose bool
rootDir string
expr string
verbose bool
rootDir string
expr string
debug bool
debugAddr string
}

func newRunCmd(io commands.IO) *commands.Command {
Expand Down Expand Up @@ -58,6 +60,20 @@ func (c *runCfg) RegisterFlags(fs *flag.FlagSet) {
"main()",
"value of expression to evaluate. Defaults to executing function main() with no args",
)

fs.BoolVar(
&c.debug,
"debug",
false,
"enable interactive debugger using stdin and stdout",
)

fs.StringVar(
&c.debugAddr,
"debug-addr",
"",
"enable interactive debugger using tcp address in the form [host]:port",
)
}

func execRun(cfg *runCfg, args []string, io commands.IO) error {
Expand Down Expand Up @@ -97,12 +113,21 @@ func execRun(cfg *runCfg, args []string, io commands.IO) error {

m := gno.NewMachineWithOptions(gno.MachineOptions{
PkgPath: string(files[0].PkgName),
Input: stdin,
Output: stdout,
Store: testStore,
Debug: cfg.debug || cfg.debugAddr != "",
})

defer m.Release()

// If the debug address is set, the debugger waits for a remote client to connect to it.
if cfg.debugAddr != "" {
if err := m.Debugger.Serve(cfg.debugAddr); err != nil {
return err
}
}

// run files
m.RunFiles(files...)
runExpr(m, cfg.expr)
Expand Down
8 changes: 8 additions & 0 deletions gnovm/cmd/gno/run_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ func TestRunApp(t *testing.T) {
args: []string{"run", "../../tests/integ/undefined_variable_test/undefined_variables_test.gno"},
recoverShouldContain: "--- preprocess stack ---", // should contain preprocess debug stack trace
},
{
args: []string{"run", "-debug", "../../tests/integ/debugger/sample.gno"},
stdoutShouldContain: "Welcome to the Gnovm debugger",
},
{
args: []string{"run", "-debug-addr", "invalidhost:17538", "../../tests/integ/debugger/sample.gno"},
errShouldContain: "listen tcp: lookup invalidhost",
},
// TODO: a test file
// TODO: args
// TODO: nativeLibs VS stdlibs
Expand Down
Loading

0 comments on commit a901e79

Please sign in to comment.