package web import ( "strconv" "strings" ) // Step is one parsed [luci] step marker pair from a child log. type Step struct { N, Total int Command string Seconds int64 Done bool Failed bool } const stepPrefix = "[luci] step " // ParseSteps extracts step markers with their wall time from a log. A step // without an end marker reports Done=false (still running or cut short). func ParseSteps(log string) []Step { var steps []Step byIndex := map[int]int{} for _, line := range strings.Split(log, "\n") { line = strings.TrimRight(line, "\r") if !strings.HasPrefix(line, stepPrefix) { continue } rest := line[len(stepPrefix):] slash := strings.Index(rest, "/") if slash < 1 { continue } n, err := strconv.Atoi(rest[:slash]) if err != nil { continue } rest = rest[slash+1:] if total, command, isStart := stepStart(rest); isStart { if _, seen := byIndex[n]; seen { continue } byIndex[n] = len(steps) steps = append(steps, Step{N: n, Total: total, Command: command}) continue } if seconds, failed, ended := stepEnd(rest); ended { if at, ok := byIndex[n]; ok { steps[at].Seconds, steps[at].Done, steps[at].Failed = seconds, true, failed } } } return steps } func stepStart(rest string) (int, string, bool) { colon := strings.Index(rest, ": ") if colon < 1 { return 0, "", false } total, err := strconv.Atoi(rest[:colon]) if err != nil { return 0, "", false } return total, rest[colon+2:], true } func stepEnd(rest string) (seconds int64, failed, ended bool) { for _, form := range []struct { marker string failed bool }{ {" done in ", false}, {" failed after ", true}, } { if i := strings.Index(rest, form.marker); i >= 0 { tail := rest[i+len(form.marker):] end := strings.IndexByte(tail, 's') if end < 1 { return 0, form.failed, true } value, err := strconv.ParseInt(tail[:end], 10, 64) if err != nil { return 0, form.failed, true } return value, form.failed, true } } return 0, false, false }