Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add battery IORegistry and sysmontap cpuusage #475

Merged
merged 11 commits into from
Nov 5, 2024
21 changes: 21 additions & 0 deletions ios/diagnostics/diagnostics.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,27 @@ func Reboot(device ios.DeviceEntry) error {
return service.Close()
}

// Battery extracts the battery ioregistry stats like Temperature, Voltage, CurrentCapacity
func (diagnosticsConn *Connection) Battery() (IORegistry, error) {
req := newIORegistryRequest()
req.addClass("IOPMPowerSource")

reader := diagnosticsConn.deviceConn.Reader()
encoded, err := req.encoded()
if err != nil {
return IORegistry{}, err
}
err = diagnosticsConn.deviceConn.Send(encoded)
if err != nil {
return IORegistry{}, err
}
response, err := diagnosticsConn.plistCodec.Decode(reader)
if err != nil {
return IORegistry{}, err
}
return diagnosticsfromBytes(response).Diagnostics.IORegistry, nil
}

func (diagnosticsConn *Connection) Reboot() error {
req := rebootRequest{Request: "Restart", WaitForDisconnect: true, DisplayFail: true, DisplayPass: true}
reader := diagnosticsConn.deviceConn.Reader()
Expand Down
43 changes: 24 additions & 19 deletions ios/diagnostics/ioregistry.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,32 @@ package diagnostics

import ios "github.com/danielpaulus/go-ios/ios"

func ioregentryRequest(key string) []byte {
requestMap := map[string]interface{}{
"Request": "IORegistry",
"EntryName": key,
}
bt, err := ios.PlistCodec{}.Encode(requestMap)
if err != nil {
panic("query request encoding should never fail")
}
return bt
type ioregistryRequest struct {
reqMap map[string]string
}

func (diagnosticsConn *Connection) IORegEntryQuery(key string) (interface{}, error) {
err := diagnosticsConn.deviceConn.Send(ioregentryRequest(key))
if err != nil {
return "", err
}
respBytes, err := diagnosticsConn.plistCodec.Decode(diagnosticsConn.deviceConn.Reader())
func newIORegistryRequest() *ioregistryRequest {
return &ioregistryRequest{map[string]string{
"Request": "IORegistry",
}}
}

func (req *ioregistryRequest) addPlane(plane string) {
req.reqMap["CurrentPlane"] = plane
}

func (req *ioregistryRequest) addName(name string) {
req.reqMap["EntryName"] = name
}

func (req *ioregistryRequest) addClass(class string) {
req.reqMap["EntryClass"] = class
}

func (req *ioregistryRequest) encoded() ([]byte, error) {
bt, err := ios.PlistCodec{}.Encode(req.reqMap)
if err != nil {
return "", err
return nil, err
}
plist, err := ios.ParsePlist(respBytes)
return plist, err
return bt, nil
}
18 changes: 14 additions & 4 deletions ios/diagnostics/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,20 @@ type allDiagnosticsResponse struct {
}

type Diagnostics struct {
GasGauge GasGauge
HDMI HDMI
NAND NAND
WiFi WiFi
GasGauge GasGauge
HDMI HDMI
NAND NAND
WiFi WiFi
IORegistry IORegistry
}

// IORegistry relates to the battery stats
type IORegistry struct {
InstantAmperage int
Temperature int
Voltage int
IsCharging bool
CurrentCapacity int
}

type WiFi struct {
Expand Down
23 changes: 23 additions & 0 deletions ios/dtx_codec/connection.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,14 @@ type Connection struct {
mutex sync.Mutex
requestChannelMessages chan Message

// MessageDispatcher use this prop to catch messages from GlobalDispatcher
// and handle it accordingly in a custom dispatcher of the dedicated service
//
// Set this prop when creating a connection instance
//
// Refer to end-to-end example of `instruments/instruments_sysmontap.go`
MessageDispatcher Dispatcher

closed chan struct{}
err error
closeOnce sync.Once
Expand Down Expand Up @@ -87,6 +95,18 @@ func NewGlobalDispatcher(requestChannelMessages chan Message, dtxConnection *Con
return dispatcher
}

// Dispatch to a MessageDispatcher of the Connection if set
func (dtxConn *Connection) Dispatch(msg Message) {
msgDispatcher := dtxConn.MessageDispatcher
if msgDispatcher != nil {
log.Debugf("msg dispatcher found: %T", msgDispatcher)
msgDispatcher.Dispatch(msg)
return
}

log.Errorf("no connection dispatcher registered for global channel, msg: %v", msg)
}

// Dispatch prints log messages and errors when they are received and also creates local Channels when requested by the device.
func (g GlobalDispatcher) Dispatch(msg Message) {
SendAckIfNeeded(g.dtxConnection, msg)
Expand All @@ -111,6 +131,9 @@ func (g GlobalDispatcher) Dispatch(msg Message) {
if msg.HasError() {
log.Error(msg.Payload[0])
}
if msg.PayloadHeader.MessageType == UnknownTypeOne {
g.dtxConnection.Dispatch(msg)
}
}

func notifyOfPublishedCapabilities(msg Message) {
Expand Down
12 changes: 12 additions & 0 deletions ios/instruments/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package instruments

import (
"fmt"
"reflect"

"github.com/danielpaulus/go-ios/ios"
dtx "github.com/danielpaulus/go-ios/ios/dtx_codec"
Expand All @@ -24,6 +25,17 @@ func (p loggingDispatcher) Dispatch(m dtx.Message) {
log.Debug(m)
}

func connectInstrumentsWithMsgDispatcher(device ios.DeviceEntry, dispatcher dtx.Dispatcher) (*dtx.Connection, error) {
dtxConn, err := connectInstruments(device)
if err != nil {
return nil, err
}
dtxConn.MessageDispatcher = dispatcher
log.Debugf("msg dispatcher: %v attached to instruments connection", reflect.TypeOf(dispatcher))

return dtxConn, nil
}

func connectInstruments(device ios.DeviceEntry) (*dtx.Connection, error) {
if device.SupportsRsd() {
log.Debugf("Connecting to %s", serviceNameRsd)
Expand Down
18 changes: 18 additions & 0 deletions ios/instruments/instruments_deviceinfo.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,24 @@ type ProcessInfo struct {
StartDate time.Time
}

// processAttributes returns the attributes list which can be used for monitoring
func (d DeviceInfoService) processAttributes() ([]interface{}, error) {
resp, err := d.channel.MethodCall("sysmonProcessAttributes")
if err != nil {
return nil, err
}
return resp.Payload[0].([]interface{}), nil
}

// systemAttributes returns the attributes list which can be used for monitoring
func (d DeviceInfoService) systemAttributes() ([]interface{}, error) {
resp, err := d.channel.MethodCall("sysmonSystemAttributes")
if err != nil {
return nil, err
}
return resp.Payload[0].([]interface{}), nil
}

// ProcessList returns a []ProcessInfo, one for each process running on the iOS device
func (d DeviceInfoService) ProcessList() ([]ProcessInfo, error) {
resp, err := d.channel.MethodCall("runningProcesses")
Expand Down
177 changes: 177 additions & 0 deletions ios/instruments/instruments_sysmontap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
package instruments

import (
"fmt"

"github.com/danielpaulus/go-ios/ios"
dtx "github.com/danielpaulus/go-ios/ios/dtx_codec"
log "github.com/sirupsen/logrus"
)

type sysmontapMsgDispatcher struct {
messages chan dtx.Message
}

func newSysmontapMsgDispatcher() *sysmontapMsgDispatcher {
return &sysmontapMsgDispatcher{make(chan dtx.Message)}
}

func (p *sysmontapMsgDispatcher) Dispatch(m dtx.Message) {
p.messages <- m
}

const sysmontapName = "com.apple.instruments.server.services.sysmontap"

type sysmontapService struct {
channel *dtx.Channel
conn *dtx.Connection

deviceInfoService *DeviceInfoService
msgDispatcher *sysmontapMsgDispatcher
}

// NewSysmontapService creates a new sysmontapService
// - samplingInterval is the rate how often to get samples, i.e Xcode's default is 10, which results in sampling output
// each 1 second, with 500 the samples are retrieved every 15 seconds. It doesn't make any correlation between
// the expected rate and the actual rate of samples delivery. We can only conclude, that the lower the rate in digits,
// the faster the samples are delivered
func NewSysmontapService(device ios.DeviceEntry, samplingInterval int) (*sysmontapService, error) {
deviceInfoService, err := NewDeviceInfoService(device)
if err != nil {
return nil, err
}

msgDispatcher := newSysmontapMsgDispatcher()
dtxConn, err := connectInstrumentsWithMsgDispatcher(device, msgDispatcher)
if err != nil {
return nil, err
}

processControlChannel := dtxConn.RequestChannelIdentifier(sysmontapName, loggingDispatcher{dtxConn})

sysAttrs, err := deviceInfoService.systemAttributes()
if err != nil {
return nil, err
}

procAttrs, err := deviceInfoService.processAttributes()
if err != nil {
return nil, err
}

config := map[string]interface{}{
"ur": samplingInterval,
"bm": 0,
"procAttrs": procAttrs,
"sysAttrs": sysAttrs,
"cpuUsage": true,
"physFootprint": true,
"sampleInterval": 500000000,
}
_, err = processControlChannel.MethodCall("setConfig:", config)
if err != nil {
return nil, err
}

err = processControlChannel.MethodCallAsync("start")
if err != nil {
return nil, err
}

return &sysmontapService{processControlChannel, dtxConn, deviceInfoService, msgDispatcher}, nil
}

// Close closes up the DTX connection, message dispatcher and dtx.Message channel
func (s *sysmontapService) Close() error {
close(s.msgDispatcher.messages)

s.deviceInfoService.Close()
return s.conn.Close()
}

// ReceiveCPUUsage returns a chan of SysmontapMessage with CPU Usage info
// The method will close the result channel automatically as soon as sysmontapMsgDispatcher's
// dtx.Message channel is closed.
func (s *sysmontapService) ReceiveCPUUsage() chan SysmontapMessage {
messages := make(chan SysmontapMessage)
go func() {
defer close(messages)

for msg := range s.msgDispatcher.messages {
sysmontapMessage, err := mapToCPUUsage(msg)
if err != nil {
log.Debugf("expected `sysmontapMessage` from global channel, but received %v", msg)
continue
}

messages <- sysmontapMessage
}

log.Infof("sysmontap message dispatcher channel closed")
}()

return messages
}

// SysmontapMessage is a wrapper struct for incoming CPU samples
type SysmontapMessage struct {
CPUCount uint64
EnabledCPUs uint64
EndMachAbsTime uint64
Type uint64
SystemCPUUsage CPUUsage
}

type CPUUsage struct {
CPU_TotalLoad float64
}

func mapToCPUUsage(msg dtx.Message) (SysmontapMessage, error) {
payload := msg.Payload
if len(payload) != 1 {
return SysmontapMessage{}, fmt.Errorf("payload of message should have only one element: %+v", msg)
}

resultArray, ok := payload[0].([]interface{})
if !ok {
return SysmontapMessage{}, fmt.Errorf("expected resultArray of type []interface{}: %+v", payload[0])
}
resultMap, ok := resultArray[0].(map[string]interface{})
if !ok {
return SysmontapMessage{}, fmt.Errorf("expected resultMap of type map[string]interface{} as a single element of resultArray: %+v", resultArray[0])
}
cpuCount, ok := resultMap["CPUCount"].(uint64)
if !ok {
return SysmontapMessage{}, fmt.Errorf("expected CPUCount of type uint64 of resultMap: %+v", resultMap)
}
enabledCPUs, ok := resultMap["EnabledCPUs"].(uint64)
if !ok {
return SysmontapMessage{}, fmt.Errorf("expected EnabledCPUs of type uint64 of resultMap: %+v", resultMap)
}
endMachAbsTime, ok := resultMap["EndMachAbsTime"].(uint64)
if !ok {
return SysmontapMessage{}, fmt.Errorf("expected EndMachAbsTime of type uint64 of resultMap: %+v", resultMap)
}
typ, ok := resultMap["Type"].(uint64)
if !ok {
return SysmontapMessage{}, fmt.Errorf("expected Type of type uint64 of resultMap: %+v", resultMap)
}
sysmontapMessageMap, ok := resultMap["SystemCPUUsage"].(map[string]interface{})
if !ok {
return SysmontapMessage{}, fmt.Errorf("expected SystemCPUUsage of type map[string]interface{} of resultMap: %+v", resultMap)
}
cpuTotalLoad, ok := sysmontapMessageMap["CPU_TotalLoad"].(float64)
if !ok {
return SysmontapMessage{}, fmt.Errorf("expected CPU_TotalLoad of type uint64 of sysmontapMessageMap: %+v", sysmontapMessageMap)
}
cpuUsage := CPUUsage{CPU_TotalLoad: cpuTotalLoad}

sysmontapMessage := SysmontapMessage{
cpuCount,
enabledCPUs,
endMachAbsTime,
typ,
cpuUsage,
}
return sysmontapMessage, nil
}
Loading
Loading