feat(debug): headless script mode with timeout watchdog

This commit is contained in:
2026-08-29 14:16:04 +02:00
parent e44162a749
commit cb398d9498
2 changed files with 40 additions and 6 deletions
+35 -2
View File
@@ -7,9 +7,11 @@ package main
import (
"fmt"
"io"
"os"
"sort"
"strings"
"time"
"sourcedock.dev/petrbalvin/gasm-devkit/debug"
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
@@ -34,6 +36,8 @@ REPL commands:
funcName := fs.String("func", "", "function to debug")
argsFile := fs.String("args", "", "file containing the ABI0 argument block")
bufSpec := fs.String("buf", "", "buffer specification: name:size:pattern[,name:size:pattern...] where pattern is zero, ones, seq, or hex")
script := fs.String("script", "", "run REPL commands from a file (one per line) and exit; '-' reads stdin")
timeout := fs.Duration("timeout", 0, "kill the debuggee after this duration (e.g. 30s); for headless --script runs")
fs.Parse(args)
// --- Debuggee mode (internal, spawned by the debugger) ---
@@ -57,6 +61,18 @@ REPL commands:
}
path := fs.Arg(0)
// The watchdog is armed before anything can block: ptrace attach and a
// continued kernel loop both hang the run when the environment forbids
// tracing or the kernel loops forever, and neither is interruptible from
// the inside.
if *timeout > 0 {
go func() {
time.Sleep(*timeout)
fmt.Fprintf(os.Stderr, "gasm debug: timeout (%s) — killing the debuggee\n", *timeout)
os.Exit(3)
}()
}
// Load the kernel to extract function metadata and labels.
k, err := verify.Load(path)
if err != nil {
@@ -157,12 +173,29 @@ REPL commands:
bm := debug.NewBreakpoints(sess)
fmt.Printf("gasm debug: %s in %s (pid %d)\n", *funcName, path, sess.Pid())
// Convert the line table for the REPL.
// Convert the line table for the command loop.
var srcLines []debug.SourceLine
for _, le := range fl.Lines {
srcLines = append(srcLines, debug.SourceLine{Offset: le.Offset, Line: le.Line})
}
debug.REPL(sess, bm, sess.CodeBase(), fl.Offset, fl.Size, fl.Args, labels, srcLines)
// Headless mode: run the script through the normal command loop and
// exit. The watchdog armed above covers launch, continue and step.
var in io.Reader = os.Stdin
if *script != "" {
if *script == "-" {
in = os.Stdin
} else {
f, err := os.Open(*script)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm debug: %v\n", err)
return 1
}
defer f.Close()
in = f
}
}
debug.REPL(sess, bm, sess.CodeBase(), fl.Offset, fl.Size, fl.Args, labels, srcLines, in)
return 0
}
+5 -4
View File
@@ -8,7 +8,7 @@ package debug
import (
"bufio"
"fmt"
"os"
"io"
"sort"
"strconv"
"strings"
@@ -26,14 +26,15 @@ type SourceLine struct {
Line int
}
// REPL runs the interactive debugger loop.
func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, argsSize int, labels []Label, lines []SourceLine) {
// REPL runs the interactive debugger loop, reading commands from in (pass
// os.Stdin interactively, or a bytes.Reader/script file for headless runs).
func REPL(s *Session, bm *Breakpoints, codeBase uint64, funcOffset, funcSize, argsSize int, labels []Label, lines []SourceLine, in io.Reader) {
entryAddr := codeBase + uint64(funcOffset)
fmt.Printf("stopped at function entry: %#x (%d bytes)\n", entryAddr, funcSize)
fmt.Println("commands: break <label|addr> | step [n] | continue | disas [n] | regs | where | x <addr> [len] | w <addr> <val...> | labels | quit")
scanner := bufio.NewScanner(os.Stdin)
scanner := bufio.NewScanner(in)
for {
fmt.Print("(gasm) ")