adb-toolkit/backend_logcat.go

194 lines
4.9 KiB
Go

package main
import (
"bufio"
"context"
"fmt"
"os/exec"
"regexp"
"strings"
"sync"
"github.com/wailsapp/wails/v2/pkg/runtime"
)
type LogcatLine struct {
Raw string `json:"raw"`
Level string `json:"level"`
Tag string `json:"tag"`
Message string `json:"message"`
PID string `json:"pid"`
TID string `json:"tid"`
Time string `json:"time"`
Refs []LogRef `json:"refs"` // mined relationships (native, for the visual map)
Mentions []LogRef `json:"mentions"` // generic package mentions (optional/noisy)
}
var (
logcatCancel context.CancelFunc
logcatMu sync.Mutex
)
// StartLogcat begins streaming logcat to the frontend via events.
// filter: optional tag filter e.g. "ActivityManager:I *:S"
// buffer: "main", "radio", "events", "crash", or "all"
func (a *App) StartLogcat(filter string, buffer string) error {
logcatMu.Lock()
defer logcatMu.Unlock()
// Stop any existing logcat
if logcatCancel != nil {
logcatCancel()
logcatCancel = nil
}
adbPath, err := a.getBinaryPath("adb")
if err != nil {
return err
}
args := []string{"logcat", "-v", "threadtime"}
if buffer != "" && buffer != "main" {
if buffer == "all" {
args = append(args, "-b", "all")
} else {
args = append(args, "-b", buffer)
}
}
if filter != "" {
args = append(args, strings.Fields(filter)...)
}
ctx, cancel := context.WithCancel(context.Background())
logcatCancel = cancel
cmd := exec.CommandContext(ctx, adbPath, args...)
setCommandSysProcAttr(cmd)
stdout, err := cmd.StdoutPipe()
if err != nil {
cancel()
return fmt.Errorf("failed to get stdout pipe: %w", err)
}
if err := cmd.Start(); err != nil {
cancel()
return fmt.Errorf("failed to start logcat: %w", err)
}
go func() {
defer cancel()
scanner := bufio.NewScanner(stdout)
scanner.Buffer(make([]byte, 1024*1024), 1024*1024)
for scanner.Scan() {
line := scanner.Text()
parsed := parseLogcatLine(line)
runtime.EventsEmit(a.ctx, "logcat:line", parsed)
}
runtime.EventsEmit(a.ctx, "logcat:stopped", nil)
cmd.Wait()
}()
return nil
}
// StopLogcat stops the running logcat stream.
func (a *App) StopLogcat() {
logcatMu.Lock()
defer logcatMu.Unlock()
if logcatCancel != nil {
logcatCancel()
logcatCancel = nil
}
}
// ClearLogcat clears all logcat buffers.
func (a *App) ClearLogcat() error {
_, err := a.runCommand("adb", "logcat", "-c")
return err
}
// LogcatProcessNames returns a best-effort PID -> process/package name map so the
// visual map can label process nodes with real names (e.g. com.android.systemui)
// instead of bare PIDs. Parsed from `ps -A`; the NAME column is the last field
// and the PID is the second. Best-effort: a failure just yields an empty map and
// the UI falls back to PIDs.
func (a *App) LogcatProcessNames() (map[string]string, error) {
out, err := a.runAdbShell("ps", "-A", "-o", "PID,NAME")
if err != nil || strings.TrimSpace(out) == "" {
// Older toybox builds reject -o; fall back to the full table.
out, err = a.runAdbShell("ps", "-A")
if err != nil {
return map[string]string{}, nil
}
}
names := make(map[string]string)
for i, line := range strings.Split(out, "\n") {
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
// Skip a header row ("PID NAME" or the ps -A column header).
if i == 0 && !isAllDigits(fields[0]) && !isAllDigits(fields[1]) {
continue
}
var pid, name string
if isAllDigits(fields[0]) {
// `ps -A -o PID,NAME` → "PID NAME"
pid, name = fields[0], fields[len(fields)-1]
} else if isAllDigits(fields[1]) {
// full `ps -A` table → "USER PID PPID ... NAME"
pid, name = fields[1], fields[len(fields)-1]
} else {
continue
}
if pid != "" && name != "" {
names[pid] = name
}
}
return names, nil
}
func isAllDigits(s string) bool {
if s == "" {
return false
}
for _, r := range s {
if r < '0' || r > '9' {
return false
}
}
return true
}
// threadtime format: "MM-DD HH:MM:SS.mmm PID TID L TAG: message"
// PID/TID are right-aligned with variable padding, so a naive split-on-space
// mis-assigns fields. This regex tolerates arbitrary whitespace runs.
var logcatRe = regexp.MustCompile(`^(\d{2}-\d{2} \d{2}:\d{2}:\d{2}\.\d{3})\s+(\d+)\s+(\d+)\s+([A-Za-z])\s+(.*?):\s?(.*)$`)
// parseLogcatLine parses a threadtime logcat line. Non-matching lines (e.g.
// "--------- beginning of main") keep their raw text as the message.
func parseLogcatLine(line string) LogcatLine {
result := LogcatLine{Raw: line}
m := logcatRe.FindStringSubmatch(line)
if m == nil {
result.Message = strings.TrimSpace(line)
result.Refs = []LogRef{}
result.Mentions = []LogRef{}
return result
}
result.Time = m[1]
result.PID = m[2]
result.TID = m[3]
result.Level = m[4]
result.Tag = strings.TrimSpace(m[5])
result.Message = m[6]
result.Refs = lcpExtractRefs(result.Tag, result.Message)
result.Mentions = lcpExtractMentions(result.Message, 3)
return result
}