package infra import ( "context" "encoding/json" "os" "os/exec" "time" ) // batteryStatus modela el JSON que imprime `termux-battery-status` (binario del // paquete termux-api en Android/Termux). Solo se declaran los campos que // consumimos como metricas. type batteryStatus struct { Health string `json:"health"` Percentage float64 `json:"percentage"` Plugged string `json:"plugged"` Status string `json:"status"` Temperature float64 `json:"temperature"` Current float64 `json:"current"` } // CollectBatteryMetrics recolecta metricas de bateria de un dispositivo // Android via el comando `termux-battery-status` (paquete termux-api) y las // devuelve como slice de PromSample con nombres estilo node_exporter. // // Es best-effort y diseñada para correr en cualquier nodo de la flota, // incluidos Linux normales donde `termux-battery-status` NO existe: en ese // caso (binario no encontrado, comando fallido o JSON invalido) devuelve un // slice vacio y error nil — NO es un fallo, simplemente no hay bateria que // reportar. Solo emite samples cuando el comando existe y responde JSON valido. // // El comando se ejecuta con un timeout de 5s via context para no colgar el // agente de monitorizacion si termux-api se queda sin responder. func CollectBatteryMetrics() ([]PromSample, error) { // Localizamos el binario por ruta absoluta con os.Stat en vez de // exec.LookPath: en Android el syscall faccessat2 que usa LookPath esta // bloqueado por seccomp y mata el proceso con SIGSYS. Si no esta presente // (Linux normal), no hay bateria que reportar (no-op). bin := findTermuxBattery() if bin == "" { return []PromSample{}, nil } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() out, err := exec.CommandContext(ctx, bin).Output() if err != nil { // Comando presente pero fallido (timeout, sin permisos, etc.): no-op. return []PromSample{}, nil } samples, err := parseBatteryJSON(out) if err != nil { // JSON inesperado o invalido: no-op, no abortamos al caller. return []PromSample{}, nil } return samples, nil } // findTermuxBattery devuelve la ruta absoluta del binario termux-battery-status // si existe, o "" si no. Usa os.Stat (permitido por seccomp en Android) en vez // de exec.LookPath (que invoca faccessat2 y crashea con SIGSYS en Android). func findTermuxBattery() string { candidates := []string{} if prefix := os.Getenv("PREFIX"); prefix != "" { candidates = append(candidates, prefix+"/bin/termux-battery-status") } candidates = append(candidates, "/data/data/com.termux/files/usr/bin/termux-battery-status") for _, c := range candidates { if _, err := os.Stat(c); err == nil { return c } } return "" } // BatterySamplesFromJSON parsea la salida JSON de `termux-battery-status` y // produce los PromSample de bateria. Es pura y exportada para que un caller que // ya tenga el JSON (por ejemplo leido de un fichero, util en Android donde el // agente no puede ejecutar subprocesos) lo convierta sin volver a ejecutar el // comando. func BatterySamplesFromJSON(data []byte) ([]PromSample, error) { return parseBatteryJSON(data) } // parseBatteryJSON parsea la salida JSON de `termux-battery-status` y produce // los PromSample de bateria. Es pura: no ejecuta comandos ni toca el entorno, // lo que la hace testeable con un JSON fijo. Devuelve error solo si el JSON no // es valido o no tiene la forma esperada. func parseBatteryJSON(data []byte) ([]PromSample, error) { var bs batteryStatus if err := json.Unmarshal(data, &bs); err != nil { return nil, err } // charging = 1 si el status indica carga/lleno o si esta enchufado. var charging float64 if bs.Status == "CHARGING" || bs.Status == "FULL" || bs.Plugged != "UNPLUGGED" { charging = 1 } samples := []PromSample{ {Name: "node_battery_percent", Value: bs.Percentage}, {Name: "node_battery_temp_celsius", Value: bs.Temperature}, {Name: "node_battery_charging", Value: charging}, {Name: "node_battery_current_ua", Value: bs.Current}, { Name: "node_battery_health_info", Labels: map[string]string{ "health": bs.Health, "status": bs.Status, "plugged": bs.Plugged, }, Value: 1, }, } return samples, nil }