mirror of https://github.com/tinygo-org/tinygo.git
wasmstm32webassemblymicrocontrollerarmavrspiwasiadafruitarduinocircuitplayground-expressgpioi2cllvmmicrobitnrf51nrf52nrf52840samd21tinygo
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
424 lines
10 KiB
424 lines
10 KiB
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"debug/dwarf"
|
|
"debug/elf"
|
|
"debug/macho"
|
|
"debug/pe"
|
|
"errors"
|
|
"fmt"
|
|
"go/token"
|
|
"io"
|
|
"net"
|
|
"os"
|
|
"os/signal"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/mattn/go-tty"
|
|
"github.com/tinygo-org/tinygo/compileopts"
|
|
|
|
"go.bug.st/serial"
|
|
"go.bug.st/serial/enumerator"
|
|
)
|
|
|
|
// Monitor connects to the given port and reads/writes the serial port.
|
|
func Monitor(executable, port string, config *compileopts.Config) error {
|
|
const timeout = time.Second * 3
|
|
var exit func() // function to be called before exiting
|
|
var serialConn io.ReadWriter
|
|
|
|
if config.Options.Serial == "rtt" {
|
|
// Use the RTT interface, which is documented (in part) here:
|
|
// https://wiki.segger.com/RTT
|
|
|
|
// Try to find the "machine.rttSerialInstance" symbol, which is the RTT
|
|
// control block.
|
|
file, err := elf.Open(executable)
|
|
if err != nil {
|
|
return fmt.Errorf("could not open ELF file to determine RTT control block: %w", err)
|
|
}
|
|
defer file.Close()
|
|
symbols, err := file.Symbols()
|
|
if err != nil {
|
|
return fmt.Errorf("could not read ELF symbol table to determine RTT control block: %w", err)
|
|
}
|
|
var address uint64
|
|
for _, symbol := range symbols {
|
|
if symbol.Name == "machine.rttSerialInstance" {
|
|
address = symbol.Value
|
|
break
|
|
}
|
|
}
|
|
if address == 0 {
|
|
return fmt.Errorf("could not find RTT control block in ELF file")
|
|
}
|
|
|
|
// Start an openocd process in the background.
|
|
args, err := config.OpenOCDConfiguration()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
args = append(args,
|
|
"-c", fmt.Sprintf("rtt setup 0x%x 16 \"SEGGER RTT\"", address),
|
|
"-c", "init",
|
|
"-c", "rtt server start 0 0")
|
|
cmd := executeCommand(config.Options, "openocd", args...)
|
|
stderr, err := cmd.StderrPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cmd.Stdout = os.Stdout
|
|
err = cmd.Start()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer cmd.Process.Kill()
|
|
exit = func() {
|
|
// Make sure the openocd process is terminated at exit.
|
|
// This does not happen through the defer above when exiting through
|
|
// os.Exit.
|
|
cmd.Process.Kill()
|
|
}
|
|
|
|
// Read the stderr, which logs various important messages we need.
|
|
r := bufio.NewReader(stderr)
|
|
var telnet net.Conn
|
|
var timeoutAt time.Time
|
|
for {
|
|
// Read the next line from the openocd process.
|
|
lineBytes, err := r.ReadBytes('\n')
|
|
if err != nil {
|
|
return err
|
|
}
|
|
line := string(lineBytes)
|
|
|
|
if line == "Info : rtt: No control block found\n" {
|
|
// Message that is sent back when OpenOCD can't find the control
|
|
// block after a 'rtt start' message.
|
|
if time.Now().After(timeoutAt) {
|
|
return fmt.Errorf("RTT timeout (could not locate RTT control block at 0x%08x)", address)
|
|
}
|
|
time.Sleep(time.Millisecond * 100)
|
|
telnet.Write([]byte("rtt start\r\n"))
|
|
} else if strings.HasPrefix(line, "Info : Listening on port") {
|
|
// We need two different ports for controlling OpenOCD
|
|
// (typically port 4444) and the RTT channel 0 socket (arbitrary
|
|
// port).
|
|
var port int
|
|
var protocol string
|
|
fmt.Sscanf(line, "Info : Listening on port %d for %s connections\n", &port, &protocol)
|
|
if protocol == "telnet" && telnet == nil {
|
|
// Connect to the "telnet" command line interface.
|
|
telnet, err = net.Dial("tcp4", fmt.Sprintf("localhost:%d", port))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Tell OpenOCD to start scanning for the RTT control block.
|
|
telnet.Write([]byte("rtt start\r\n"))
|
|
// Also make sure we will time out if the control block just
|
|
// can't be found.
|
|
timeoutAt = time.Now().Add(timeout)
|
|
} else if protocol == "rtt" {
|
|
// Connect to the RTT channel, for both stdin and stdout.
|
|
conn, err := net.Dial("tcp4", fmt.Sprintf("localhost:%d", port))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
serialConn = conn
|
|
}
|
|
} else if strings.HasPrefix(line, "Info : rtt: Control block found at") {
|
|
// Connection established!
|
|
break
|
|
}
|
|
}
|
|
} else { // -serial=uart or -serial=usb
|
|
var err error
|
|
wait := 300
|
|
for i := 0; i <= wait; i++ {
|
|
port, err = getDefaultPort(port, config.Target.SerialPort)
|
|
if err != nil {
|
|
if i < wait {
|
|
time.Sleep(10 * time.Millisecond)
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
break
|
|
}
|
|
|
|
br := config.Options.BaudRate
|
|
if br <= 0 {
|
|
br = 115200
|
|
}
|
|
|
|
wait = 300
|
|
var p serial.Port
|
|
for i := 0; i <= wait; i++ {
|
|
p, err = serial.Open(port, &serial.Mode{BaudRate: br})
|
|
if err != nil {
|
|
if i < wait {
|
|
time.Sleep(10 * time.Millisecond)
|
|
continue
|
|
}
|
|
return err
|
|
}
|
|
serialConn = p
|
|
break
|
|
}
|
|
defer p.Close()
|
|
}
|
|
|
|
tty, err := tty.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tty.Close()
|
|
|
|
sig := make(chan os.Signal, 1)
|
|
signal.Notify(sig, os.Interrupt)
|
|
defer signal.Stop(sig)
|
|
|
|
go func() {
|
|
<-sig
|
|
tty.Close()
|
|
if exit != nil {
|
|
exit()
|
|
}
|
|
os.Exit(0)
|
|
}()
|
|
|
|
fmt.Printf("Connected to %s. Press Ctrl-C to exit.\n", port)
|
|
|
|
errCh := make(chan error, 1)
|
|
|
|
go func() {
|
|
buf := make([]byte, 100*1024)
|
|
writer := newOutputWriter(os.Stdout, executable)
|
|
for {
|
|
n, err := serialConn.Read(buf)
|
|
if err != nil {
|
|
errCh <- fmt.Errorf("read error: %w", err)
|
|
return
|
|
}
|
|
writer.Write(buf[:n])
|
|
}
|
|
}()
|
|
|
|
go func() {
|
|
for {
|
|
r, err := tty.ReadRune()
|
|
if err != nil {
|
|
errCh <- err
|
|
return
|
|
}
|
|
if r == 0 {
|
|
continue
|
|
}
|
|
serialConn.Write([]byte(string(r)))
|
|
}
|
|
}()
|
|
|
|
return <-errCh
|
|
}
|
|
|
|
// SerialPortInfo is a structure that holds information about the port and its
|
|
// associated TargetSpec.
|
|
type SerialPortInfo struct {
|
|
Name string
|
|
IsUSB bool
|
|
VID string
|
|
PID string
|
|
Target string
|
|
Spec *compileopts.TargetSpec
|
|
}
|
|
|
|
// ListSerialPort returns serial port information and any detected TinyGo
|
|
// target.
|
|
func ListSerialPorts() ([]SerialPortInfo, error) {
|
|
maps, err := compileopts.GetTargetSpecs()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
portsList, err := enumerator.GetDetailedPortsList()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
serialPortInfo := []SerialPortInfo{}
|
|
for _, p := range portsList {
|
|
info := SerialPortInfo{
|
|
Name: p.Name,
|
|
IsUSB: p.IsUSB,
|
|
VID: p.VID,
|
|
PID: p.PID,
|
|
}
|
|
vid := strings.ToLower(p.VID)
|
|
pid := strings.ToLower(p.PID)
|
|
for k, v := range maps {
|
|
usbInterfaces := v.SerialPort
|
|
for _, s := range usbInterfaces {
|
|
parts := strings.Split(s, ":")
|
|
if len(parts) != 2 {
|
|
continue
|
|
}
|
|
if vid == strings.ToLower(parts[0]) && pid == strings.ToLower(parts[1]) {
|
|
info.Target = k
|
|
info.Spec = v
|
|
}
|
|
}
|
|
}
|
|
serialPortInfo = append(serialPortInfo, info)
|
|
}
|
|
|
|
return serialPortInfo, nil
|
|
}
|
|
|
|
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
|
|
}
|
|
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
|
|
address := extractPanicAddress(w.line)
|
|
if address != 0 {
|
|
loc, err := addressToLine(w.executable, address)
|
|
if err == nil && loc.Filename != "" {
|
|
fmt.Printf("[tinygo: 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
|
|
}
|
|
|