Skip to content

Commit

Permalink
Add battery IORegistry and sysmontap cpuusage (#475)
Browse files Browse the repository at this point in the history
* Add battery ioregistry stats

* Add sysmontap cpu usage stats

* Add doc

* fix: safe type assertions

* fix: add system monitor wrapper

* fix: simplify to just channel

* Dispatch to dedicated Dispatcher in Connection

* fix: close channel

* fix: deliver CPU sample continuosly

* Specify sampling rate

---------

Authored-by: fish-sauce <[email protected]>
Co-authored-by: dmissmann <[email protected]>
  • Loading branch information
kvs-coder and dmissmann authored Nov 5, 2024
1 parent 6fa8b80 commit 1789ded
Show file tree
Hide file tree
Showing 8 changed files with 356 additions and 25 deletions.
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

0 comments on commit 1789ded

Please sign in to comment.