feat: llgo monitor -target target -port port
This commit is contained in:
303
internal/monitor/monitor.go
Normal file
303
internal/monitor/monitor.go
Normal file
@@ -0,0 +1,303 @@
|
||||
package monitor
|
||||
|
||||
import (
|
||||
"debug/dwarf"
|
||||
"debug/elf"
|
||||
"debug/macho"
|
||||
"debug/pe"
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/token"
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/goplus/llgo/internal/crosscompile"
|
||||
"github.com/mattn/go-tty"
|
||||
"go.bug.st/serial"
|
||||
)
|
||||
|
||||
// MonitorConfig contains configuration for the monitor
|
||||
type MonitorConfig struct {
|
||||
Port string // Serial port device
|
||||
Target string // Target name for crosscompile config
|
||||
BaudRate int // Baudrate of serial monitor
|
||||
Executable string // Optional path to executable for debug info
|
||||
WaitTime int // Wait time for port connection (ms)
|
||||
}
|
||||
|
||||
// Monitor starts serial monitoring with the given configuration
|
||||
func Monitor(config MonitorConfig, verbose bool) error {
|
||||
// Set defaults
|
||||
if config.BaudRate == 0 {
|
||||
config.BaudRate = 115200
|
||||
}
|
||||
if config.WaitTime == 0 {
|
||||
config.WaitTime = 300
|
||||
}
|
||||
|
||||
// If target is specified, try to get port from crosscompile config
|
||||
if config.Target != "" && config.Port == "" {
|
||||
port, err := getPortFromTarget(config.Target)
|
||||
if err != nil && verbose {
|
||||
fmt.Fprintf(os.Stderr, "Warning: could not get port from target: %v\n", err)
|
||||
}
|
||||
if port != "" {
|
||||
config.Port = port
|
||||
}
|
||||
}
|
||||
|
||||
if config.Port == "" {
|
||||
return fmt.Errorf("port not specified and could not determine from target")
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Fprintf(os.Stderr, "Connecting to %s at %d baud\n", config.Port, config.BaudRate)
|
||||
}
|
||||
|
||||
// Open serial port with retry
|
||||
var serialConn serial.Port
|
||||
var err error
|
||||
for i := 0; i <= config.WaitTime; i++ {
|
||||
serialConn, err = serial.Open(config.Port, &serial.Mode{BaudRate: config.BaudRate})
|
||||
if err != nil {
|
||||
if i < config.WaitTime {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("failed to open port %s: %w", config.Port, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
defer serialConn.Close()
|
||||
|
||||
// Open TTY for input
|
||||
tty, err := tty.Open()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to open TTY: %w", err)
|
||||
}
|
||||
defer tty.Close()
|
||||
|
||||
// Setup signal handling
|
||||
sig := make(chan os.Signal, 1)
|
||||
signal.Notify(sig, os.Interrupt)
|
||||
defer signal.Stop(sig)
|
||||
|
||||
// Create output writer with optional debug info
|
||||
var writer *outputWriter
|
||||
if config.Executable != "" {
|
||||
writer = newOutputWriter(os.Stdout, config.Executable)
|
||||
} else {
|
||||
writer = newOutputWriter(os.Stdout, "")
|
||||
}
|
||||
|
||||
fmt.Printf("Connected to %s. Press Ctrl-C to exit.\n", config.Port)
|
||||
|
||||
errCh := make(chan error, 2)
|
||||
|
||||
// Goroutine for reading from serial port
|
||||
go func() {
|
||||
buf := make([]byte, 100*1024)
|
||||
for {
|
||||
n, err := serialConn.Read(buf)
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("serial read error: %w", err)
|
||||
return
|
||||
}
|
||||
writer.Write(buf[:n])
|
||||
}
|
||||
}()
|
||||
|
||||
// Goroutine for reading from TTY and writing to serial port
|
||||
go func() {
|
||||
for {
|
||||
r, err := tty.ReadRune()
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("TTY read error: %w", err)
|
||||
return
|
||||
}
|
||||
if r == 0 {
|
||||
continue
|
||||
}
|
||||
serialConn.Write([]byte(string(r)))
|
||||
}
|
||||
}()
|
||||
|
||||
// Wait for signal or error
|
||||
select {
|
||||
case <-sig:
|
||||
if verbose {
|
||||
fmt.Fprintf(os.Stderr, "\nDisconnected from %s\n", config.Port)
|
||||
}
|
||||
return nil
|
||||
case err := <-errCh:
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// getPortFromTarget tries to get serial port from target configuration
|
||||
func getPortFromTarget(target string) (string, error) {
|
||||
export, err := crosscompile.Use("", "", false, target)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Try to get port from serial port list
|
||||
if len(export.Flash.SerialPort) > 0 {
|
||||
return export.Flash.SerialPort[0], nil
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no serial port found in target configuration")
|
||||
}
|
||||
|
||||
var addressMatch = regexp.MustCompile(`^panic: runtime error at 0x([0-9a-f]+): `)
|
||||
|
||||
// Extract the address from the "panic: runtime error at" message.
|
||||
func extractPanicAddress(line []byte) uint64 {
|
||||
matches := addressMatch.FindSubmatch(line)
|
||||
if matches != nil {
|
||||
address, err := strconv.ParseUint(string(matches[1]), 16, 64)
|
||||
if err == nil {
|
||||
return address
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// Convert an address in the binary to a source address location.
|
||||
func addressToLine(executable string, address uint64) (token.Position, error) {
|
||||
data, err := readDWARF(executable)
|
||||
if err != nil {
|
||||
return token.Position{}, err
|
||||
}
|
||||
r := data.Reader()
|
||||
|
||||
for {
|
||||
e, err := r.Next()
|
||||
if err != nil {
|
||||
return token.Position{}, err
|
||||
}
|
||||
if e == nil {
|
||||
break
|
||||
}
|
||||
switch e.Tag {
|
||||
case dwarf.TagCompileUnit:
|
||||
r.SkipChildren()
|
||||
lr, err := data.LineReader(e)
|
||||
if err != nil {
|
||||
return token.Position{}, err
|
||||
}
|
||||
var lineEntry = dwarf.LineEntry{
|
||||
EndSequence: true,
|
||||
}
|
||||
for {
|
||||
// Read the next .debug_line entry.
|
||||
prevLineEntry := lineEntry
|
||||
err := lr.Next(&lineEntry)
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
return token.Position{}, err
|
||||
}
|
||||
|
||||
if prevLineEntry.EndSequence && lineEntry.Address == 0 {
|
||||
// Tombstone value. This symbol has been removed, for
|
||||
// example by the --gc-sections linker flag. It is still
|
||||
// here in the debug information because the linker can't
|
||||
// just remove this reference.
|
||||
// Read until the next EndSequence so that this sequence is
|
||||
// skipped.
|
||||
// For more details, see (among others):
|
||||
// https://reviews.llvm.org/D84825
|
||||
for {
|
||||
err := lr.Next(&lineEntry)
|
||||
if err != nil {
|
||||
return token.Position{}, err
|
||||
}
|
||||
if lineEntry.EndSequence {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !prevLineEntry.EndSequence {
|
||||
// The chunk describes the code from prevLineEntry to
|
||||
// lineEntry.
|
||||
if prevLineEntry.Address <= address && lineEntry.Address > address {
|
||||
return token.Position{
|
||||
Filename: prevLineEntry.File.Name,
|
||||
Line: prevLineEntry.Line,
|
||||
Column: prevLineEntry.Column,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return token.Position{}, nil // location not found
|
||||
}
|
||||
|
||||
// Read the DWARF debug information from a given file (in various formats).
|
||||
func readDWARF(executable string) (*dwarf.Data, error) {
|
||||
f, err := os.Open(executable)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
if file, err := elf.NewFile(f); err == nil {
|
||||
return file.DWARF()
|
||||
} else if file, err := macho.NewFile(f); err == nil {
|
||||
return file.DWARF()
|
||||
} else if file, err := pe.NewFile(f); err == nil {
|
||||
return file.DWARF()
|
||||
} else {
|
||||
return nil, errors.New("unknown binary format")
|
||||
}
|
||||
}
|
||||
|
||||
type outputWriter struct {
|
||||
out io.Writer
|
||||
executable string
|
||||
line []byte
|
||||
}
|
||||
|
||||
// newOutputWriter returns an io.Writer that will intercept panic addresses and
|
||||
// will try to insert a source location in the output if the source location can
|
||||
// be found in the executable.
|
||||
func newOutputWriter(out io.Writer, executable string) *outputWriter {
|
||||
return &outputWriter{
|
||||
out: out,
|
||||
executable: executable,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *outputWriter) Write(p []byte) (n int, err error) {
|
||||
start := 0
|
||||
for i, c := range p {
|
||||
if c == '\n' {
|
||||
w.out.Write(p[start : i+1])
|
||||
start = i + 1
|
||||
if w.executable != "" {
|
||||
address := extractPanicAddress(w.line)
|
||||
if address != 0 {
|
||||
loc, err := addressToLine(w.executable, address)
|
||||
if err == nil && loc.Filename != "" {
|
||||
fmt.Printf("[llgo: panic at %s]\n", loc.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
w.line = w.line[:0]
|
||||
} else {
|
||||
w.line = append(w.line, c)
|
||||
}
|
||||
}
|
||||
w.out.Write(p[start:])
|
||||
n = len(p)
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user