adb-toolkit/backend_logcatpatterns.go

169 lines
5.2 KiB
Go

package main
import (
"regexp"
"strings"
)
// Relationship extraction for the Logcat visual map — ported to Go so the mining
// heuristics live in native (compiled) code rather than shipped JavaScript.
//
// A log line is just text, but Android's framework + system-event logs encode
// real relationships: who started whom, who crashed, who got killed, who sent a
// signal to which pid. We mine those so the map can draw meaningful edges on top
// of the ambient co-occurrence web. Runs in the existing per-line log pipeline
// (parseLogcatLine), so the result ships attached to each LogcatLine — no extra IPC.
// LogRef is one relationship a log line implies. JSON shape matches the frontend.
type LogRef struct {
Kind string `json:"kind"` // activity|spawn|death|crash|anr|signal|gfx|mention
Target string `json:"target"` // package, component, or pid payload
TargetKind string `json:"targetKind"` // package|component|pid
}
var (
lcpPkg = regexp.MustCompile(`\b([a-z][a-z0-9_]*(?:\.[a-z0-9_]+){2,})\b`)
lcpPkgAnchor = regexp.MustCompile(`^[a-z][a-z0-9_]*(?:\.[a-z0-9_]+){2,}$`)
lcpComponent = regexp.MustCompile(`(?i)([a-z][a-z0-9_.]+)/([a-z0-9_.$]+)`)
lcpEventCSV = regexp.MustCompile(`\[([^\]]*)\]`)
lcpSig = regexp.MustCompile(`Sending signal\.\s*PID:\s*(\d+)`)
lcpProcess = regexp.MustCompile(`(?i)Process:\s*([a-z][a-z0-9_.]+)`)
lcpANRin = regexp.MustCompile(`\bANR in\b`)
lcpStartProc = regexp.MustCompile(`\bStart proc\b`)
lcpKilling = regexp.MustCompile(`\bKilling\b|\bhas died\b|\bdied\b`)
lcpStartAct = regexp.MustCompile(`\bSTART u\d+|\bDisplayed\b|\bmoveTaskTo`)
lcpFatal = regexp.MustCompile(`FATAL EXCEPTION`)
lcpGfxTag = regexp.MustCompile(`^(SurfaceFlinger|WindowManager|ViewRootImpl|Choreographer|gralloc|OpenGLRenderer)`)
lcpMentSkip = regexp.MustCompile(`^(java|javax|sun|kotlin|android|androidx|dalvik)\.`)
)
func lcpFirstPackage(s string) string {
if m := lcpPkg.FindStringSubmatch(s); m != nil {
return m[1]
}
return ""
}
// Pull the package field out of an event-log CSV payload (first dotted token).
func lcpEventPackage(msg string) string {
csv := lcpEventCSV.FindStringSubmatch(msg)
if csv == nil {
return lcpFirstPackage(msg)
}
for _, f := range strings.Split(csv[1], ",") {
t := strings.TrimSpace(f)
if lcpPkgAnchor.MatchString(t) {
return t
}
}
return lcpFirstPackage(msg)
}
// lcpExtractRefs mines the relationships a single log line implies. Returns an
// empty slice for the vast majority of lines.
func lcpExtractRefs(tag, msg string) []LogRef {
refs := []LogRef{}
push := func(kind, target, targetKind string) {
if target != "" {
refs = append(refs, LogRef{Kind: kind, Target: target, TargetKind: targetKind})
}
}
// binary event-log tags (events buffer)
switch tag {
case "am_proc_start", "am_proc_bound":
push("spawn", lcpEventPackage(msg), "package")
return refs
case "am_proc_died", "am_kill", "am_low_memory":
push("death", lcpEventPackage(msg), "package")
return refs
case "am_crash":
push("crash", lcpEventPackage(msg), "package")
return refs
case "am_anr":
push("anr", lcpEventPackage(msg), "package")
return refs
case "am_activity_launch_time", "am_focused_activity", "am_resume_activity", "am_pause_activity", "wm_focused_window":
if c := lcpComponent.FindStringSubmatch(msg); c != nil {
push("activity", c[1]+"/"+c[2], "component")
} else {
push("activity", lcpEventPackage(msg), "package")
}
return refs
}
// framework text logs (main/system buffers)
if tag == "ActivityManager" || tag == "ActivityTaskManager" {
if lcpANRin.MatchString(msg) {
push("anr", lcpFirstPackage(msg), "package")
}
if lcpStartProc.MatchString(msg) {
push("spawn", lcpFirstPackage(msg), "package")
}
if lcpKilling.MatchString(msg) {
push("death", lcpFirstPackage(msg), "package")
}
if sig := lcpSig.FindStringSubmatch(msg); sig != nil {
push("signal", sig[1], "pid")
}
if lcpStartAct.MatchString(msg) {
if c := lcpComponent.FindStringSubmatch(msg); c != nil {
push("activity", c[1]+"/"+c[2], "component")
} else {
push("activity", lcpFirstPackage(msg), "package")
}
}
if len(refs) > 0 {
return refs
}
}
if tag == "AndroidRuntime" || lcpFatal.MatchString(msg) {
if p := lcpProcess.FindStringSubmatch(msg); p != nil {
push("crash", p[1], "package")
} else {
push("crash", lcpFirstPackage(msg), "package")
}
if len(refs) > 0 {
return refs
}
}
if tag == "lowmemorykiller" || tag == "lmkd" {
push("death", lcpFirstPackage(msg), "package")
if len(refs) > 0 {
return refs
}
}
if lcpGfxTag.MatchString(tag) {
if c := lcpComponent.FindStringSubmatch(msg); c != nil {
push("gfx", c[1]+"/"+c[2], "component")
return refs
}
}
return refs
}
// lcpExtractMentions: generic fallback — up to `max` package-looking tokens
// (used when the map's "parsed mentions" toggle is on).
func lcpExtractMentions(msg string, max int) []LogRef {
out := []LogRef{}
seen := map[string]bool{}
for _, m := range lcpPkg.FindAllStringSubmatch(msg, -1) {
if len(out) >= max {
break
}
t := m[1]
if seen[t] {
continue
}
seen[t] = true
if lcpMentSkip.MatchString(t) {
continue
}
out = append(out, LogRef{Kind: "mention", Target: t, TargetKind: "package"})
}
return out
}