Skip to content

Commit 1789ded

Browse files
Victordmissmann
andauthored
Add battery IORegistry and sysmontap cpuusage (#475)
* 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]>
1 parent 6fa8b80 commit 1789ded

File tree

8 files changed

+356
-25
lines changed

8 files changed

+356
-25
lines changed

ios/diagnostics/diagnostics.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,27 @@ func Reboot(device ios.DeviceEntry) error {
3333
return service.Close()
3434
}
3535

36+
// Battery extracts the battery ioregistry stats like Temperature, Voltage, CurrentCapacity
37+
func (diagnosticsConn *Connection) Battery() (IORegistry, error) {
38+
req := newIORegistryRequest()
39+
req.addClass("IOPMPowerSource")
40+
41+
reader := diagnosticsConn.deviceConn.Reader()
42+
encoded, err := req.encoded()
43+
if err != nil {
44+
return IORegistry{}, err
45+
}
46+
err = diagnosticsConn.deviceConn.Send(encoded)
47+
if err != nil {
48+
return IORegistry{}, err
49+
}
50+
response, err := diagnosticsConn.plistCodec.Decode(reader)
51+
if err != nil {
52+
return IORegistry{}, err
53+
}
54+
return diagnosticsfromBytes(response).Diagnostics.IORegistry, nil
55+
}
56+
3657
func (diagnosticsConn *Connection) Reboot() error {
3758
req := rebootRequest{Request: "Restart", WaitForDisconnect: true, DisplayFail: true, DisplayPass: true}
3859
reader := diagnosticsConn.deviceConn.Reader()

ios/diagnostics/ioregistry.go

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,32 @@ package diagnostics
22

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

5-
func ioregentryRequest(key string) []byte {
6-
requestMap := map[string]interface{}{
7-
"Request": "IORegistry",
8-
"EntryName": key,
9-
}
10-
bt, err := ios.PlistCodec{}.Encode(requestMap)
11-
if err != nil {
12-
panic("query request encoding should never fail")
13-
}
14-
return bt
5+
type ioregistryRequest struct {
6+
reqMap map[string]string
157
}
168

17-
func (diagnosticsConn *Connection) IORegEntryQuery(key string) (interface{}, error) {
18-
err := diagnosticsConn.deviceConn.Send(ioregentryRequest(key))
19-
if err != nil {
20-
return "", err
21-
}
22-
respBytes, err := diagnosticsConn.plistCodec.Decode(diagnosticsConn.deviceConn.Reader())
9+
func newIORegistryRequest() *ioregistryRequest {
10+
return &ioregistryRequest{map[string]string{
11+
"Request": "IORegistry",
12+
}}
13+
}
14+
15+
func (req *ioregistryRequest) addPlane(plane string) {
16+
req.reqMap["CurrentPlane"] = plane
17+
}
18+
19+
func (req *ioregistryRequest) addName(name string) {
20+
req.reqMap["EntryName"] = name
21+
}
22+
23+
func (req *ioregistryRequest) addClass(class string) {
24+
req.reqMap["EntryClass"] = class
25+
}
26+
27+
func (req *ioregistryRequest) encoded() ([]byte, error) {
28+
bt, err := ios.PlistCodec{}.Encode(req.reqMap)
2329
if err != nil {
24-
return "", err
30+
return nil, err
2531
}
26-
plist, err := ios.ParsePlist(respBytes)
27-
return plist, err
32+
return bt, nil
2833
}

ios/diagnostics/request.go

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,20 @@ type allDiagnosticsResponse struct {
3030
}
3131

3232
type Diagnostics struct {
33-
GasGauge GasGauge
34-
HDMI HDMI
35-
NAND NAND
36-
WiFi WiFi
33+
GasGauge GasGauge
34+
HDMI HDMI
35+
NAND NAND
36+
WiFi WiFi
37+
IORegistry IORegistry
38+
}
39+
40+
// IORegistry relates to the battery stats
41+
type IORegistry struct {
42+
InstantAmperage int
43+
Temperature int
44+
Voltage int
45+
IsCharging bool
46+
CurrentCapacity int
3747
}
3848

3949
type WiFi struct {

ios/dtx_codec/connection.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,14 @@ type Connection struct {
3030
mutex sync.Mutex
3131
requestChannelMessages chan Message
3232

33+
// MessageDispatcher use this prop to catch messages from GlobalDispatcher
34+
// and handle it accordingly in a custom dispatcher of the dedicated service
35+
//
36+
// Set this prop when creating a connection instance
37+
//
38+
// Refer to end-to-end example of `instruments/instruments_sysmontap.go`
39+
MessageDispatcher Dispatcher
40+
3341
closed chan struct{}
3442
err error
3543
closeOnce sync.Once
@@ -87,6 +95,18 @@ func NewGlobalDispatcher(requestChannelMessages chan Message, dtxConnection *Con
8795
return dispatcher
8896
}
8997

98+
// Dispatch to a MessageDispatcher of the Connection if set
99+
func (dtxConn *Connection) Dispatch(msg Message) {
100+
msgDispatcher := dtxConn.MessageDispatcher
101+
if msgDispatcher != nil {
102+
log.Debugf("msg dispatcher found: %T", msgDispatcher)
103+
msgDispatcher.Dispatch(msg)
104+
return
105+
}
106+
107+
log.Errorf("no connection dispatcher registered for global channel, msg: %v", msg)
108+
}
109+
90110
// Dispatch prints log messages and errors when they are received and also creates local Channels when requested by the device.
91111
func (g GlobalDispatcher) Dispatch(msg Message) {
92112
SendAckIfNeeded(g.dtxConnection, msg)
@@ -111,6 +131,9 @@ func (g GlobalDispatcher) Dispatch(msg Message) {
111131
if msg.HasError() {
112132
log.Error(msg.Payload[0])
113133
}
134+
if msg.PayloadHeader.MessageType == UnknownTypeOne {
135+
g.dtxConnection.Dispatch(msg)
136+
}
114137
}
115138

116139
func notifyOfPublishedCapabilities(msg Message) {

ios/instruments/helper.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package instruments
22

33
import (
44
"fmt"
5+
"reflect"
56

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

28+
func connectInstrumentsWithMsgDispatcher(device ios.DeviceEntry, dispatcher dtx.Dispatcher) (*dtx.Connection, error) {
29+
dtxConn, err := connectInstruments(device)
30+
if err != nil {
31+
return nil, err
32+
}
33+
dtxConn.MessageDispatcher = dispatcher
34+
log.Debugf("msg dispatcher: %v attached to instruments connection", reflect.TypeOf(dispatcher))
35+
36+
return dtxConn, nil
37+
}
38+
2739
func connectInstruments(device ios.DeviceEntry) (*dtx.Connection, error) {
2840
if device.SupportsRsd() {
2941
log.Debugf("Connecting to %s", serviceNameRsd)

ios/instruments/instruments_deviceinfo.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,24 @@ type ProcessInfo struct {
2020
StartDate time.Time
2121
}
2222

23+
// processAttributes returns the attributes list which can be used for monitoring
24+
func (d DeviceInfoService) processAttributes() ([]interface{}, error) {
25+
resp, err := d.channel.MethodCall("sysmonProcessAttributes")
26+
if err != nil {
27+
return nil, err
28+
}
29+
return resp.Payload[0].([]interface{}), nil
30+
}
31+
32+
// systemAttributes returns the attributes list which can be used for monitoring
33+
func (d DeviceInfoService) systemAttributes() ([]interface{}, error) {
34+
resp, err := d.channel.MethodCall("sysmonSystemAttributes")
35+
if err != nil {
36+
return nil, err
37+
}
38+
return resp.Payload[0].([]interface{}), nil
39+
}
40+
2341
// ProcessList returns a []ProcessInfo, one for each process running on the iOS device
2442
func (d DeviceInfoService) ProcessList() ([]ProcessInfo, error) {
2543
resp, err := d.channel.MethodCall("runningProcesses")
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package instruments
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/danielpaulus/go-ios/ios"
7+
dtx "github.com/danielpaulus/go-ios/ios/dtx_codec"
8+
log "github.com/sirupsen/logrus"
9+
)
10+
11+
type sysmontapMsgDispatcher struct {
12+
messages chan dtx.Message
13+
}
14+
15+
func newSysmontapMsgDispatcher() *sysmontapMsgDispatcher {
16+
return &sysmontapMsgDispatcher{make(chan dtx.Message)}
17+
}
18+
19+
func (p *sysmontapMsgDispatcher) Dispatch(m dtx.Message) {
20+
p.messages <- m
21+
}
22+
23+
const sysmontapName = "com.apple.instruments.server.services.sysmontap"
24+
25+
type sysmontapService struct {
26+
channel *dtx.Channel
27+
conn *dtx.Connection
28+
29+
deviceInfoService *DeviceInfoService
30+
msgDispatcher *sysmontapMsgDispatcher
31+
}
32+
33+
// NewSysmontapService creates a new sysmontapService
34+
// - samplingInterval is the rate how often to get samples, i.e Xcode's default is 10, which results in sampling output
35+
// each 1 second, with 500 the samples are retrieved every 15 seconds. It doesn't make any correlation between
36+
// the expected rate and the actual rate of samples delivery. We can only conclude, that the lower the rate in digits,
37+
// the faster the samples are delivered
38+
func NewSysmontapService(device ios.DeviceEntry, samplingInterval int) (*sysmontapService, error) {
39+
deviceInfoService, err := NewDeviceInfoService(device)
40+
if err != nil {
41+
return nil, err
42+
}
43+
44+
msgDispatcher := newSysmontapMsgDispatcher()
45+
dtxConn, err := connectInstrumentsWithMsgDispatcher(device, msgDispatcher)
46+
if err != nil {
47+
return nil, err
48+
}
49+
50+
processControlChannel := dtxConn.RequestChannelIdentifier(sysmontapName, loggingDispatcher{dtxConn})
51+
52+
sysAttrs, err := deviceInfoService.systemAttributes()
53+
if err != nil {
54+
return nil, err
55+
}
56+
57+
procAttrs, err := deviceInfoService.processAttributes()
58+
if err != nil {
59+
return nil, err
60+
}
61+
62+
config := map[string]interface{}{
63+
"ur": samplingInterval,
64+
"bm": 0,
65+
"procAttrs": procAttrs,
66+
"sysAttrs": sysAttrs,
67+
"cpuUsage": true,
68+
"physFootprint": true,
69+
"sampleInterval": 500000000,
70+
}
71+
_, err = processControlChannel.MethodCall("setConfig:", config)
72+
if err != nil {
73+
return nil, err
74+
}
75+
76+
err = processControlChannel.MethodCallAsync("start")
77+
if err != nil {
78+
return nil, err
79+
}
80+
81+
return &sysmontapService{processControlChannel, dtxConn, deviceInfoService, msgDispatcher}, nil
82+
}
83+
84+
// Close closes up the DTX connection, message dispatcher and dtx.Message channel
85+
func (s *sysmontapService) Close() error {
86+
close(s.msgDispatcher.messages)
87+
88+
s.deviceInfoService.Close()
89+
return s.conn.Close()
90+
}
91+
92+
// ReceiveCPUUsage returns a chan of SysmontapMessage with CPU Usage info
93+
// The method will close the result channel automatically as soon as sysmontapMsgDispatcher's
94+
// dtx.Message channel is closed.
95+
func (s *sysmontapService) ReceiveCPUUsage() chan SysmontapMessage {
96+
messages := make(chan SysmontapMessage)
97+
go func() {
98+
defer close(messages)
99+
100+
for msg := range s.msgDispatcher.messages {
101+
sysmontapMessage, err := mapToCPUUsage(msg)
102+
if err != nil {
103+
log.Debugf("expected `sysmontapMessage` from global channel, but received %v", msg)
104+
continue
105+
}
106+
107+
messages <- sysmontapMessage
108+
}
109+
110+
log.Infof("sysmontap message dispatcher channel closed")
111+
}()
112+
113+
return messages
114+
}
115+
116+
// SysmontapMessage is a wrapper struct for incoming CPU samples
117+
type SysmontapMessage struct {
118+
CPUCount uint64
119+
EnabledCPUs uint64
120+
EndMachAbsTime uint64
121+
Type uint64
122+
SystemCPUUsage CPUUsage
123+
}
124+
125+
type CPUUsage struct {
126+
CPU_TotalLoad float64
127+
}
128+
129+
func mapToCPUUsage(msg dtx.Message) (SysmontapMessage, error) {
130+
payload := msg.Payload
131+
if len(payload) != 1 {
132+
return SysmontapMessage{}, fmt.Errorf("payload of message should have only one element: %+v", msg)
133+
}
134+
135+
resultArray, ok := payload[0].([]interface{})
136+
if !ok {
137+
return SysmontapMessage{}, fmt.Errorf("expected resultArray of type []interface{}: %+v", payload[0])
138+
}
139+
resultMap, ok := resultArray[0].(map[string]interface{})
140+
if !ok {
141+
return SysmontapMessage{}, fmt.Errorf("expected resultMap of type map[string]interface{} as a single element of resultArray: %+v", resultArray[0])
142+
}
143+
cpuCount, ok := resultMap["CPUCount"].(uint64)
144+
if !ok {
145+
return SysmontapMessage{}, fmt.Errorf("expected CPUCount of type uint64 of resultMap: %+v", resultMap)
146+
}
147+
enabledCPUs, ok := resultMap["EnabledCPUs"].(uint64)
148+
if !ok {
149+
return SysmontapMessage{}, fmt.Errorf("expected EnabledCPUs of type uint64 of resultMap: %+v", resultMap)
150+
}
151+
endMachAbsTime, ok := resultMap["EndMachAbsTime"].(uint64)
152+
if !ok {
153+
return SysmontapMessage{}, fmt.Errorf("expected EndMachAbsTime of type uint64 of resultMap: %+v", resultMap)
154+
}
155+
typ, ok := resultMap["Type"].(uint64)
156+
if !ok {
157+
return SysmontapMessage{}, fmt.Errorf("expected Type of type uint64 of resultMap: %+v", resultMap)
158+
}
159+
sysmontapMessageMap, ok := resultMap["SystemCPUUsage"].(map[string]interface{})
160+
if !ok {
161+
return SysmontapMessage{}, fmt.Errorf("expected SystemCPUUsage of type map[string]interface{} of resultMap: %+v", resultMap)
162+
}
163+
cpuTotalLoad, ok := sysmontapMessageMap["CPU_TotalLoad"].(float64)
164+
if !ok {
165+
return SysmontapMessage{}, fmt.Errorf("expected CPU_TotalLoad of type uint64 of sysmontapMessageMap: %+v", sysmontapMessageMap)
166+
}
167+
cpuUsage := CPUUsage{CPU_TotalLoad: cpuTotalLoad}
168+
169+
sysmontapMessage := SysmontapMessage{
170+
cpuCount,
171+
enabledCPUs,
172+
endMachAbsTime,
173+
typ,
174+
cpuUsage,
175+
}
176+
return sysmontapMessage, nil
177+
}

0 commit comments

Comments
 (0)