github.com/tinygo-org/tinygo@v0.31.3-0.20240404173401-90b0bf646c27/monitor.go (about)

     1  package main
     2  
     3  import (
     4  	"bufio"
     5  	"debug/dwarf"
     6  	"debug/elf"
     7  	"debug/macho"
     8  	"debug/pe"
     9  	"errors"
    10  	"fmt"
    11  	"go/token"
    12  	"io"
    13  	"net"
    14  	"os"
    15  	"os/signal"
    16  	"regexp"
    17  	"strconv"
    18  	"strings"
    19  	"time"
    20  
    21  	"github.com/mattn/go-tty"
    22  	"github.com/tinygo-org/tinygo/compileopts"
    23  
    24  	"go.bug.st/serial"
    25  	"go.bug.st/serial/enumerator"
    26  )
    27  
    28  // Monitor connects to the given port and reads/writes the serial port.
    29  func Monitor(executable, port string, config *compileopts.Config) error {
    30  	const timeout = time.Second * 3
    31  	var exit func() // function to be called before exiting
    32  	var serialConn io.ReadWriter
    33  
    34  	if config.Options.Serial == "rtt" {
    35  		// Use the RTT interface, which is documented (in part) here:
    36  		// https://wiki.segger.com/RTT
    37  
    38  		// Try to find the "machine.rttSerialInstance" symbol, which is the RTT
    39  		// control block.
    40  		file, err := elf.Open(executable)
    41  		if err != nil {
    42  			return fmt.Errorf("could not open ELF file to determine RTT control block: %w", err)
    43  		}
    44  		defer file.Close()
    45  		symbols, err := file.Symbols()
    46  		if err != nil {
    47  			return fmt.Errorf("could not read ELF symbol table to determine RTT control block: %w", err)
    48  		}
    49  		var address uint64
    50  		for _, symbol := range symbols {
    51  			if symbol.Name == "machine.rttSerialInstance" {
    52  				address = symbol.Value
    53  				break
    54  			}
    55  		}
    56  		if address == 0 {
    57  			return fmt.Errorf("could not find RTT control block in ELF file")
    58  		}
    59  
    60  		// Start an openocd process in the background.
    61  		args, err := config.OpenOCDConfiguration()
    62  		if err != nil {
    63  			return err
    64  		}
    65  		args = append(args,
    66  			"-c", fmt.Sprintf("rtt setup 0x%x 16 \"SEGGER RTT\"", address),
    67  			"-c", "init",
    68  			"-c", "rtt server start 0 0")
    69  		cmd := executeCommand(config.Options, "openocd", args...)
    70  		stderr, err := cmd.StderrPipe()
    71  		if err != nil {
    72  			return err
    73  		}
    74  		cmd.Stdout = os.Stdout
    75  		err = cmd.Start()
    76  		if err != nil {
    77  			return err
    78  		}
    79  		defer cmd.Process.Kill()
    80  		exit = func() {
    81  			// Make sure the openocd process is terminated at exit.
    82  			// This does not happen through the defer above when exiting through
    83  			// os.Exit.
    84  			cmd.Process.Kill()
    85  		}
    86  
    87  		// Read the stderr, which logs various important messages we need.
    88  		r := bufio.NewReader(stderr)
    89  		var telnet net.Conn
    90  		var timeoutAt time.Time
    91  		for {
    92  			// Read the next line from the openocd process.
    93  			lineBytes, err := r.ReadBytes('\n')
    94  			if err != nil {
    95  				return err
    96  			}
    97  			line := string(lineBytes)
    98  
    99  			if line == "Info : rtt: No control block found\n" {
   100  				// Message that is sent back when OpenOCD can't find the control
   101  				// block after a 'rtt start' message.
   102  				if time.Now().After(timeoutAt) {
   103  					return fmt.Errorf("RTT timeout (could not locate RTT control block at 0x%08x)", address)
   104  				}
   105  				time.Sleep(time.Millisecond * 100)
   106  				telnet.Write([]byte("rtt start\r\n"))
   107  			} else if strings.HasPrefix(line, "Info : Listening on port") {
   108  				// We need two different ports for controlling OpenOCD
   109  				// (typically port 4444) and the RTT channel 0 socket (arbitrary
   110  				// port).
   111  				var port int
   112  				var protocol string
   113  				fmt.Sscanf(line, "Info : Listening on port %d for %s connections\n", &port, &protocol)
   114  				if protocol == "telnet" && telnet == nil {
   115  					// Connect to the "telnet" command line interface.
   116  					telnet, err = net.Dial("tcp4", fmt.Sprintf("localhost:%d", port))
   117  					if err != nil {
   118  						return err
   119  					}
   120  					// Tell OpenOCD to start scanning for the RTT control block.
   121  					telnet.Write([]byte("rtt start\r\n"))
   122  					// Also make sure we will time out if the control block just
   123  					// can't be found.
   124  					timeoutAt = time.Now().Add(timeout)
   125  				} else if protocol == "rtt" {
   126  					// Connect to the RTT channel, for both stdin and stdout.
   127  					conn, err := net.Dial("tcp4", fmt.Sprintf("localhost:%d", port))
   128  					if err != nil {
   129  						return err
   130  					}
   131  					serialConn = conn
   132  				}
   133  			} else if strings.HasPrefix(line, "Info : rtt: Control block found at") {
   134  				// Connection established!
   135  				break
   136  			}
   137  		}
   138  	} else { // -serial=uart or -serial=usb
   139  		var err error
   140  		wait := 300
   141  		for i := 0; i <= wait; i++ {
   142  			port, err = getDefaultPort(port, config.Target.SerialPort)
   143  			if err != nil {
   144  				if i < wait {
   145  					time.Sleep(10 * time.Millisecond)
   146  					continue
   147  				}
   148  				return err
   149  			}
   150  			break
   151  		}
   152  
   153  		br := config.Options.BaudRate
   154  		if br <= 0 {
   155  			br = 115200
   156  		}
   157  
   158  		wait = 300
   159  		var p serial.Port
   160  		for i := 0; i <= wait; i++ {
   161  			p, err = serial.Open(port, &serial.Mode{BaudRate: br})
   162  			if err != nil {
   163  				if i < wait {
   164  					time.Sleep(10 * time.Millisecond)
   165  					continue
   166  				}
   167  				return err
   168  			}
   169  			serialConn = p
   170  			break
   171  		}
   172  		defer p.Close()
   173  	}
   174  
   175  	tty, err := tty.Open()
   176  	if err != nil {
   177  		return err
   178  	}
   179  	defer tty.Close()
   180  
   181  	sig := make(chan os.Signal, 1)
   182  	signal.Notify(sig, os.Interrupt)
   183  	defer signal.Stop(sig)
   184  
   185  	go func() {
   186  		<-sig
   187  		tty.Close()
   188  		if exit != nil {
   189  			exit()
   190  		}
   191  		os.Exit(0)
   192  	}()
   193  
   194  	fmt.Printf("Connected to %s. Press Ctrl-C to exit.\n", port)
   195  
   196  	errCh := make(chan error, 1)
   197  
   198  	go func() {
   199  		buf := make([]byte, 100*1024)
   200  		var line []byte
   201  		for {
   202  			n, err := serialConn.Read(buf)
   203  			if err != nil {
   204  				errCh <- fmt.Errorf("read error: %w", err)
   205  				return
   206  			}
   207  			start := 0
   208  			for i, c := range buf[:n] {
   209  				if c == '\n' {
   210  					os.Stdout.Write(buf[start : i+1])
   211  					start = i + 1
   212  					address := extractPanicAddress(line)
   213  					if address != 0 {
   214  						loc, err := addressToLine(executable, address)
   215  						if err == nil && loc.IsValid() {
   216  							fmt.Printf("[tinygo: panic at %s]\n", loc.String())
   217  						}
   218  					}
   219  					line = line[:0]
   220  				} else {
   221  					line = append(line, c)
   222  				}
   223  			}
   224  			os.Stdout.Write(buf[start:n])
   225  		}
   226  	}()
   227  
   228  	go func() {
   229  		for {
   230  			r, err := tty.ReadRune()
   231  			if err != nil {
   232  				errCh <- err
   233  				return
   234  			}
   235  			if r == 0 {
   236  				continue
   237  			}
   238  			serialConn.Write([]byte(string(r)))
   239  		}
   240  	}()
   241  
   242  	return <-errCh
   243  }
   244  
   245  // SerialPortInfo is a structure that holds information about the port and its
   246  // associated TargetSpec.
   247  type SerialPortInfo struct {
   248  	Name   string
   249  	IsUSB  bool
   250  	VID    string
   251  	PID    string
   252  	Target string
   253  	Spec   *compileopts.TargetSpec
   254  }
   255  
   256  // ListSerialPort returns serial port information and any detected TinyGo
   257  // target.
   258  func ListSerialPorts() ([]SerialPortInfo, error) {
   259  	maps, err := compileopts.GetTargetSpecs()
   260  	if err != nil {
   261  		return nil, err
   262  	}
   263  
   264  	portsList, err := enumerator.GetDetailedPortsList()
   265  	if err != nil {
   266  		return nil, err
   267  	}
   268  
   269  	serialPortInfo := []SerialPortInfo{}
   270  	for _, p := range portsList {
   271  		info := SerialPortInfo{
   272  			Name:  p.Name,
   273  			IsUSB: p.IsUSB,
   274  			VID:   p.VID,
   275  			PID:   p.PID,
   276  		}
   277  		vid := strings.ToLower(p.VID)
   278  		pid := strings.ToLower(p.PID)
   279  		for k, v := range maps {
   280  			usbInterfaces := v.SerialPort
   281  			for _, s := range usbInterfaces {
   282  				parts := strings.Split(s, ":")
   283  				if len(parts) != 2 {
   284  					continue
   285  				}
   286  				if vid == strings.ToLower(parts[0]) && pid == strings.ToLower(parts[1]) {
   287  					info.Target = k
   288  					info.Spec = v
   289  				}
   290  			}
   291  		}
   292  		serialPortInfo = append(serialPortInfo, info)
   293  	}
   294  
   295  	return serialPortInfo, nil
   296  }
   297  
   298  var addressMatch = regexp.MustCompile(`^panic: runtime error at 0x([0-9a-f]+): `)
   299  
   300  // Extract the address from the "panic: runtime error at" message.
   301  func extractPanicAddress(line []byte) uint64 {
   302  	matches := addressMatch.FindSubmatch(line)
   303  	if matches != nil {
   304  		address, err := strconv.ParseUint(string(matches[1]), 16, 64)
   305  		if err == nil {
   306  			return address
   307  		}
   308  	}
   309  	return 0
   310  }
   311  
   312  // Convert an address in the binary to a source address location.
   313  func addressToLine(executable string, address uint64) (token.Position, error) {
   314  	data, err := readDWARF(executable)
   315  	if err != nil {
   316  		return token.Position{}, err
   317  	}
   318  	r := data.Reader()
   319  
   320  	for {
   321  		e, err := r.Next()
   322  		if err != nil {
   323  			return token.Position{}, err
   324  		}
   325  		if e == nil {
   326  			break
   327  		}
   328  		switch e.Tag {
   329  		case dwarf.TagCompileUnit:
   330  			r.SkipChildren()
   331  			lr, err := data.LineReader(e)
   332  			if err != nil {
   333  				return token.Position{}, err
   334  			}
   335  			var lineEntry = dwarf.LineEntry{
   336  				EndSequence: true,
   337  			}
   338  			for {
   339  				// Read the next .debug_line entry.
   340  				prevLineEntry := lineEntry
   341  				err := lr.Next(&lineEntry)
   342  				if err != nil {
   343  					if err == io.EOF {
   344  						break
   345  					}
   346  					return token.Position{}, err
   347  				}
   348  
   349  				if prevLineEntry.EndSequence && lineEntry.Address == 0 {
   350  					// Tombstone value. This symbol has been removed, for
   351  					// example by the --gc-sections linker flag. It is still
   352  					// here in the debug information because the linker can't
   353  					// just remove this reference.
   354  					// Read until the next EndSequence so that this sequence is
   355  					// skipped.
   356  					// For more details, see (among others):
   357  					// https://reviews.llvm.org/D84825
   358  					for {
   359  						err := lr.Next(&lineEntry)
   360  						if err != nil {
   361  							return token.Position{}, err
   362  						}
   363  						if lineEntry.EndSequence {
   364  							break
   365  						}
   366  					}
   367  				}
   368  
   369  				if !prevLineEntry.EndSequence {
   370  					// The chunk describes the code from prevLineEntry to
   371  					// lineEntry.
   372  					if prevLineEntry.Address <= address && lineEntry.Address > address {
   373  						return token.Position{
   374  							Filename: prevLineEntry.File.Name,
   375  							Line:     prevLineEntry.Line,
   376  							Column:   prevLineEntry.Column,
   377  						}, nil
   378  					}
   379  				}
   380  			}
   381  		}
   382  	}
   383  
   384  	return token.Position{}, nil // location not found
   385  }
   386  
   387  // Read the DWARF debug information from a given file (in various formats).
   388  func readDWARF(executable string) (*dwarf.Data, error) {
   389  	f, err := os.Open(executable)
   390  	if err != nil {
   391  		return nil, err
   392  	}
   393  	if file, err := elf.NewFile(f); err == nil {
   394  		return file.DWARF()
   395  	} else if file, err := macho.NewFile(f); err == nil {
   396  		return file.DWARF()
   397  	} else if file, err := pe.NewFile(f); err == nil {
   398  		return file.DWARF()
   399  	} else {
   400  		return nil, errors.New("unknown binary format")
   401  	}
   402  }