package diagnostic import ( "bufio" "bytes" "fmt" "io" "os/exec" ) type DecodeLineFunc func(text string) (*Hop, error) func decodeNetworkOutputToFile(command *exec.Cmd, decodeLine DecodeLineFunc) ([]*Hop, string, error) { stdout, err := command.StdoutPipe() if err != nil { return nil, "", fmt.Errorf("error piping traceroute's output: %w", err) } if err := command.Start(); err != nil { return nil, "", fmt.Errorf("error starting traceroute: %w", err) } // Tee the output to a string to have the raw information // in case the decode call fails // This error is handled only after the Wait call below returns // otherwise the process can become a zombie buf := bytes.NewBuffer([]byte{}) tee := io.TeeReader(stdout, buf) hops, err := Decode(tee, decodeLine) // regardless of success of the decoding // consume all output to have available in buf _, _ = io.ReadAll(tee) if werr := command.Wait(); werr != nil { return nil, "", fmt.Errorf("error finishing traceroute: %w", werr) } if err != nil { return nil, buf.String(), err } return hops, buf.String(), nil } func Decode(reader io.Reader, decodeLine DecodeLineFunc) ([]*Hop, error) { scanner := bufio.NewScanner(reader) scanner.Split(bufio.ScanLines) var hops []*Hop for scanner.Scan() { text := scanner.Text() if text == "" { continue } hop, err := decodeLine(text) if err != nil { // This continue is here on the error case because there are lines at the start and end // that may not be parsable. (check windows tracert output) // The skip is here because aside from the start and end lines the other lines should // always be parsable without errors. continue } hops = append(hops, hop) } if scanner.Err() != nil { return nil, fmt.Errorf("scanner reported an error: %w", scanner.Err()) } return hops, nil }