//go:build darwin || linux package diagnostic import ( "context" "fmt" "os/exec" "strconv" "strings" "time" ) type NetworkCollectorImpl struct{} func (tracer *NetworkCollectorImpl) Collect(ctx context.Context, options TraceOptions) ([]*Hop, string, error) { args := []string{ "-I", "-w", strconv.FormatInt(int64(options.timeout.Seconds()), 10), "-m", strconv.FormatUint(options.ttl, 10), options.address, } var command string switch options.useV4 { case false: command = "traceroute6" default: command = "traceroute" } process := exec.CommandContext(ctx, command, args...) return decodeNetworkOutputToFile(process, DecodeLine) } func DecodeLine(text string) (*Hop, error) { fields := strings.Fields(text) parts := []string{} filter := func(s string) bool { return s != "*" && s != "ms" } for _, field := range fields { if filter(field) { parts = append(parts, field) } } index, err := strconv.ParseUint(parts[0], 10, 8) if err != nil { return nil, fmt.Errorf("couldn't parse index from timeout hop: %w", err) } if len(parts) == 1 { return NewTimeoutHop(uint8(index)), nil } domain := "" rtts := []time.Duration{} for _, part := range parts[1:] { rtt, err := strconv.ParseFloat(part, 64) if err != nil { domain += part + " " } else { rtts = append(rtts, time.Duration(rtt*MicrosecondsFactor)) } } domain, _ = strings.CutSuffix(domain, " ") return NewHop(uint8(index), domain, rtts), nil }