diff --git a/ios/accessibility/utils.go b/ios/accessibility/utils.go index 1999ad98..2c20654e 100644 --- a/ios/accessibility/utils.go +++ b/ios/accessibility/utils.go @@ -1,6 +1,9 @@ package accessibility func convertToStringList(payload []interface{}) []string { + if len(payload) == 0 { + return make([]string, 0) + } list := payload[0].([]interface{}) result := make([]string, len(list)) for i, v := range list { diff --git a/ios/deviceconnection.go b/ios/deviceconnection.go index 33dd754d..11df5147 100755 --- a/ios/deviceconnection.go +++ b/ios/deviceconnection.go @@ -9,11 +9,13 @@ import ( "time" log "github.com/sirupsen/logrus" + plist "howett.net/plist" ) // DeviceConnectionInterface contains a physical network connection to a usbmuxd socket. type DeviceConnectionInterface interface { Close() error + SendAny(message any) error Send(message []byte) error Reader() io.Reader Writer() io.Writer @@ -89,6 +91,19 @@ func (conn *DeviceConnection) Send(bytes []byte) error { return nil } +func (conn *DeviceConnection) SendAny(req any) error { + data, err := plist.Marshal(req, plist.XMLFormat) + if err != nil { + return err + } + + if err := binary.Write(conn.c, binary.BigEndian, uint32(len(data))); err != nil { + return err + } + + return binary.Write(conn.c, binary.BigEndian, data) +} + // Reader exposes the underlying net.Conn as io.Reader func (conn *DeviceConnection) Reader() io.Reader { return conn.c diff --git a/ios/diagnostics/diagnostics_battery.go b/ios/diagnostics/diagnostics_battery.go new file mode 100644 index 00000000..c0a02f38 --- /dev/null +++ b/ios/diagnostics/diagnostics_battery.go @@ -0,0 +1,35 @@ +package diagnostics + +import ( + ios "github.com/danielpaulus/go-ios/ios" +) + +type Request struct { + Request string `plist:"Request"` +} +type IORegistryRequest struct { + Request + CurrentPlane string `plist:"CurrentPlane,omitempty"` + EntryName string `plist:"EntryName,omitempty"` + EntryClass string `plist:"EntryClass,omitempty"` +} + +func Battery(device ios.DeviceEntry) (interface{}, error) { + conn, _ := New(device) + req := &IORegistryRequest{ + Request: Request{"IORegistry"}, + CurrentPlane: "", + EntryName: "", + EntryClass: "IOPMPowerSource", + } + err := conn.deviceConn.SendAny(req) + if err != nil { + return "", err + } + respBytes, err := conn.plistCodec.RecvBytes(conn.deviceConn.Reader()) + if err != nil { + return "", err + } + plist, err := ios.ParsePlist(respBytes) + return plist, err +} diff --git a/ios/dtx_codec/connection.go b/ios/dtx_codec/connection.go index 81bd03a3..ca735666 100644 --- a/ios/dtx_codec/connection.go +++ b/ios/dtx_codec/connection.go @@ -1,6 +1,7 @@ package dtx import ( + "encoding/json" "io" "math" "strings" @@ -86,6 +87,42 @@ func (g GlobalDispatcher) Dispatch(msg Message) { return } } + // network + v := msg.Payload[0] + + b, _ := json.Marshal(v) + + str := string(b) + if str == "_notifyOfPublishedCapabilities:" { + return + } + + if s, ok := v.([]interface{}); ok && len(s) == 2 { + _type := s[0].(uint64) + if _type == 2 { + s1, _ := json.Marshal(s[1]) + println("network:" + string(s1)) + } + } + // sysmontap,cpu,meme + dataArray, ok := v.([]interface{}) + if ok && len(dataArray) > 0 { + for _, ele := range dataArray { + if m, ok := ele.(map[string]interface{}); ok { + if _, ok2 := m["SystemCPUUsage"]; ok2 { + s1, _ := json.Marshal(m) + println("sysmontap:" + string(s1)) + } + } + } + } + + if m, ok := v.(map[string]interface{}); ok { + if b, ok2 := m["CoreAnimationFramesPerSecond"]; ok2 { + println("fps:", b.(uint64)) + } + } + log.Tracef("Global Dispatcher Received: %s %s", msg.Payload, msg.Auxiliary) if msg.HasError() { log.Error(msg.Payload[0]) diff --git a/ios/instruments/instruments_channels.go b/ios/instruments/instruments_channels.go index b41a9c17..4606ade0 100644 --- a/ios/instruments/instruments_channels.go +++ b/ios/instruments/instruments_channels.go @@ -7,6 +7,9 @@ const ( procControlChannel = "com.apple.instruments.server.services.processcontrol" procControlPosixSpawnChannel = "com.apple.instruments.server.services.processcontrol.posixspawn" mobileNotificationsChannel = "com.apple.instruments.server.services.mobilenotifications" + mobileNetworkingChannel = "com.apple.instruments.server.services.networking" + SysmontapChannel = "com.apple.instruments.server.services.sysmontap" // 获取性能数据用 + GraphicsOpenGlChannel = "com.apple.instruments.server.services.graphics.opengl" ) const appListingChannel = "com.apple.instruments.server.services.device.applictionListing" diff --git a/ios/instruments/instruments_graphics_opengl.go b/ios/instruments/instruments_graphics_opengl.go new file mode 100644 index 00000000..dfaf2ee5 --- /dev/null +++ b/ios/instruments/instruments_graphics_opengl.go @@ -0,0 +1,28 @@ +package instruments + +import ( + "time" + + "github.com/danielpaulus/go-ios/ios" + log "github.com/sirupsen/logrus" +) + +func IterOpenglData(device ios.DeviceEntry) (func() (map[string]interface{}, error), func() error, error) { + conn, err := connectInstruments(device) + if err != nil { + return nil, nil, err + } + channel := conn.RequestChannelIdentifier(GraphicsOpenGlChannel, channelDispatcher{}) + + resp, err := channel.MethodCall("startSamplingAtTimeInterval:", 0) + if err != nil { + log.Errorf("resp:%+v", resp) + return nil, nil, err + } + time.Sleep(time.Duration(5) * time.Second) + channel.MethodCall("stopSampling:") + conn.Close() + // return dispatcher.Receive, dispatcher.Close, nil + return nil, nil, nil + +} diff --git a/ios/instruments/instruments_network.go b/ios/instruments/instruments_network.go new file mode 100644 index 00000000..ceda7768 --- /dev/null +++ b/ios/instruments/instruments_network.go @@ -0,0 +1,78 @@ +package instruments + +import ( + "time" + + "github.com/danielpaulus/go-ios/ios" + log "github.com/sirupsen/logrus" +) + +// type channelDispatcher2 struct { +// messageChannel chan dtx.Message +// closeChannel chan struct{} +// } + +// func (dispatcher channelDispatcher2) Receive() (map[string]interface{}, error) { +// log.Println("--->channelDispatcher2.Receive") +// for { +// fmt.Println("---1") +// select { +// case msg := <-dispatcher.messageChannel: +// fmt.Println("---2") +// selector, result, err := toMap(msg) +// if "applicationStateNotification:" == selector && err == nil { +// return result, nil +// } +// if err != nil { +// log.Debugf("error extracting message %+v, %v", msg, err) +// } +// case <-dispatcher.closeChannel: +// fmt.Println("---4") +// return map[string]interface{}{}, io.EOF +// } +// fmt.Println("---3") +// } +// } + +// func (dispatcher *channelDispatcher2) Close() error { +// select { +// case dispatcher.closeChannel <- struct{}{}: +// return nil +// case <-time.After(time.Second * 5): +// return fmt.Errorf("timeout") +// } +// } + +// func (dispatcher channelDispatcher2) Dispatch(msg dtx.Message) { +// fmt.Println("---2---Dispatch") +// dispatcher.messageChannel <- msg +// } + +func ListenNetwork(device ios.DeviceEntry) (func() (map[string]interface{}, error), func() error, error) { + conn, err := connectInstruments(device) + if err != nil { + return nil, nil, err + } + // dispatcher := channelDispatcher2{messageChannel: make(chan dtx.Message), closeChannel: make(chan struct{})} + // conn.AddDefaultChannelReceiver(dispatcher) + + // capabs := map[string]interface{}{ + // "com.apple.private.DTXBlockCompression": uint64(2), + // "com.apple.private.DTXConnection": uint64(1), + // } + + // conn.GlobalChannel().MethodCall("_notifyOfPublishedCapabilities:", capabs) + + channel := conn.RequestChannelIdentifier(mobileNetworkingChannel, channelDispatcher{}) + + resp, err := channel.MethodCall("startMonitoring") + if err != nil { + log.Errorf("resp:%+v", resp) + return nil, nil, err + } + time.Sleep(time.Duration(5) * time.Second) + conn.Close() + // return dispatcher.Receive, dispatcher.Close, nil + return nil, nil, nil + +} diff --git a/ios/instruments/instruments_sysmontap.go b/ios/instruments/instruments_sysmontap.go new file mode 100644 index 00000000..d44c68e1 --- /dev/null +++ b/ios/instruments/instruments_sysmontap.go @@ -0,0 +1,103 @@ +package instruments + +import ( + "fmt" + "time" + + "github.com/danielpaulus/go-ios/ios" +) + +type PerfOptions struct { + + // system + SysCPU bool `json:"sys_cpu,omitempty" yaml:"sys_cpu,omitempty"` + SysMem bool `json:"sys_mem,omitempty" yaml:"sys_mem,omitempty"` + SysDisk bool `json:"sys_disk,omitempty" yaml:"sys_disk,omitempty"` + SysNetwork bool `json:"sys_network,omitempty" yaml:"sys_network,omitempty"` + gpu bool + FPS bool `json:"fps,omitempty" yaml:"fps,omitempty"` + Network bool `json:"network,omitempty" yaml:"network,omitempty"` + // process + BundleID string `json:"bundle_id,omitempty" yaml:"bundle_id,omitempty"` + Pid int `json:"pid,omitempty" yaml:"pid,omitempty"` + // config + OutputInterval int `json:"output_interval,omitempty" yaml:"output_interval,omitempty"` // ms + SystemAttributes []string `json:"system_attributes,omitempty" yaml:"system_attributes,omitempty"` + ProcessAttributes []string `json:"process_attributes,omitempty" yaml:"process_attributes,omitempty"` +} + +func defaulPerfOption() *PerfOptions { + return &PerfOptions{ + SysCPU: false, + SysMem: false, + SysDisk: false, + SysNetwork: false, + gpu: false, + FPS: false, + Network: false, + OutputInterval: 1000, // default 1000ms + // SystemAttributes: []string{"vmExtPageCount", "vmFreeCount", "vmPurgeableCount", "vmSpeculativeCount", "physMemSize"}, + // ProcessAttributes: []string{"memVirtualSize", "cpuUsage", "ctxSwitch", "intWakeups", "physFootprint", "memResidentSize", "memAnon", "pid"}, + SystemAttributes: []string{ + // disk + "diskBytesRead", + "diskBytesWritten", + "diskReadOps", + "diskWriteOps", + // memory + "vmCompressorPageCount", + "vmExtPageCount", + "vmFreeCount", + "vmIntPageCount", + "vmPurgeableCount", + "vmWireCount", + "vmUsedCount", + "__vmSwapUsage", + // network + "netBytesIn", + "netBytesOut", + "netPacketsIn", + "netPacketsOut", + }, + ProcessAttributes: []string{ + "pid", + "cpuUsage", + }, + } +} + +func ListenSysmontap(device ios.DeviceEntry) (func() (map[string]interface{}, error), func() error, error) { + conn, err := connectInstruments(device) + if err != nil { + return nil, nil, err + } + + channel := conn.RequestChannelIdentifier(SysmontapChannel, channelDispatcher{}) + options := defaulPerfOption() + // interval := time.Millisecond * time.Duration(options.OutputInterval) + // config := map[string]interface{}{ + // "bm": 0, + // "cpuUsage": true, + // "procAttrs": []string{"memVirtualSize", "cpuUsage", "ctxSwitch", "intWakeups", "physFootprint", "memResidentSize", "memAnon", "pid"}, + // "sampleInterval": interval, + // "sysAttrs": []string{"vmExtPageCount", "vmFreeCount", "vmPurgeableCount", "vmSpeculativeCount", "physMemSize"}, + // "ur": 1000, + // } + config := map[string]interface{}{ + "bm": 0, + "cpuUsage": true, + "sampleInterval": options.OutputInterval, // time.Duration + "ur": options.OutputInterval, // 输出频率 + "procAttrs": options.ProcessAttributes, // process performance + "sysAttrs": options.SystemAttributes, // system performance + } + channel.MethodCall("setConfig:", config) + channel.MethodCall("start") + + time.Sleep(time.Duration(3) * time.Second) + channel.MethodCall("stop") + conn.Close() + fmt.Println("conn.Close") + return nil, nil, nil + +} diff --git a/ios/plistcodec.go b/ios/plistcodec.go index d3fd10d7..eb50b7f7 100755 --- a/ios/plistcodec.go +++ b/ios/plistcodec.go @@ -58,6 +58,20 @@ func (plistCodec PlistCodec) Decode(r io.Reader) ([]byte, error) { return payloadBytes, nil } +func (p *PlistCodec) RecvBytes(r io.Reader) ([]byte, error) { + size := uint32(0) + if err := binary.Read(r, binary.BigEndian, &size); err != nil { + return nil, err + } + + data := make([]byte, size) + if _, err := io.ReadFull(r, data); err != nil { + return nil, err + } + + return data, nil +} + // PlistCodecReadWriter handles length encoded plist messages // Each message starts with an uint32 value representing the length of the encoded payload // followed by the binary encoded plist data diff --git a/main.go b/main.go index 02588e48..f1a558d7 100644 --- a/main.go +++ b/main.go @@ -78,6 +78,8 @@ Usage: ios syslog [options] ios screenshot [options] [--output=] [--stream] [--port=] ios instruments notifications [options] + ios network [options] + ios sysmontap [options] ios crash ls [] [options] ios crash cp [options] ios crash rm [options] @@ -127,6 +129,8 @@ Usage: ios zoomtouch (enable | disable | toggle | get) [--force] [options] ios diskspace [options] ios batterycheck [options] + ios battery [options] + ios fps [options] ios tunnel start [options] [--pair-record-path=] ios tunnel ls [options] ios devmode (enable | get) [--enable-post-restart] [options] @@ -237,6 +241,8 @@ The commands work as following: ios timeformat (24h | 12h | toggle | get) [--force] [options] Sets, or returns the state of the "time format". iOS 11+ only (Use --force to try on older versions). ios diskspace [options] Prints disk space info. ios batterycheck [options] Prints battery info. + ios battery [options] Prints battery useage info. + ios fps [options] Prints fps info. ios tunnel start [options] [--pair-record-path=] Creates a tunnel connection to the device. If the device was not paired with the host yet, device pairing will also be executed. > On systems with System Integrity Protection enabled the argument '--pair-record-path' is required as we can not access the default path for the pair record > This command needs to be executed with admin privileges. @@ -437,6 +443,18 @@ The commands work as following: return } + if networkCommand(device, arguments) { + return + } + + if sysmontapCommand(device, arguments) { + return + } + + if fpsCommand(device, arguments) { + return + } + b, _ = arguments.Bool("pcap") if b { p, _ := arguments.String("--process") @@ -994,6 +1012,11 @@ The commands work as following: printBatteryDiagnostics(device) return } + b, _ = arguments.Bool("battery") + if b { + printBattery(device) + return + } if tunnelCommand { startCommand, _ := arguments.Bool("start") @@ -1169,6 +1192,32 @@ func instrumentsCommand(device ios.DeviceEntry, arguments docopt.Opts) bool { return b } +func networkCommand(device ios.DeviceEntry, arguments docopt.Opts) bool { + b, _ := arguments.Bool("network") + if b { + instruments.ListenNetwork(device) + fmt.Println("over") + } + return b +} +func sysmontapCommand(device ios.DeviceEntry, arguments docopt.Opts) bool { + b, _ := arguments.Bool("sysmontap") + if b { + instruments.ListenSysmontap(device) + fmt.Println("over") + } + return b +} + +func fpsCommand(device ios.DeviceEntry, arguments docopt.Opts) bool { + b, _ := arguments.Bool("fps") + if b { + instruments.IterOpenglData(device) + fmt.Println("over") + } + return b +} + func crashCommand(device ios.DeviceEntry, arguments docopt.Opts) bool { b, _ := arguments.Bool("crash") if b { @@ -1624,6 +1673,12 @@ func printDiagnostics(device ios.DeviceEntry) { fmt.Println(convertToJSONString(values)) } +func printBattery(device ios.DeviceEntry) { + resp, _ := diagnostics.Battery(device) + jb, _ := marshalJSON(resp) + fmt.Printf("battery:%s\n", jb) +} + func printBatteryDiagnostics(device ios.DeviceEntry) { battery, err := ios.GetBatteryDiagnostics(device) exitIfError("failed getting battery diagnostics", err)