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 }