forked from danielpaulus/go-ios
-
Notifications
You must be signed in to change notification settings - Fork 0
/
appservice.go
256 lines (230 loc) · 7.9 KB
/
appservice.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
// Package appservice provides functions to Launch and Kill apps on an iOS devices for iOS17+.
package appservice
import (
"bytes"
"errors"
"fmt"
"io"
"net"
"path"
"syscall"
"github.com/google/uuid"
"github.com/greficsmurf/go-ios/ios"
"github.com/greficsmurf/go-ios/ios/coredevice"
"github.com/greficsmurf/go-ios/ios/xpc"
"howett.net/plist"
)
// Connection represents a connection to the appservice on an iOS device for iOS17+.
// It is used to launch and kill apps and to list processes.
type Connection struct {
conn *xpc.Connection
deviceId string
}
const (
// RebootFull is the style for a full reboot of the device.
RebootFull = "full"
// RebootUserspace is the style for a reboot of the userspace of the device.
RebootUserspace = "userspace"
)
// New creates a new connection to the appservice on the device for iOS17+.
// It returns an error if the connection could not be established.
func New(deviceEntry ios.DeviceEntry) (*Connection, error) {
xpcConn, err := ios.ConnectToXpcServiceTunnelIface(deviceEntry, "com.apple.coredevice.appservice")
if err != nil {
return nil, fmt.Errorf("new: %w", err)
}
return &Connection{conn: xpcConn, deviceId: uuid.New().String()}, nil
}
// AppLaunch represents the result of launching an app on the device for iOS17+.
// It contains the PID of the launched app.
type AppLaunch struct {
Pid int
}
// Process represents a process running on the device for iOS17+.
// It contains the PID and the path of the process.
type Process struct {
Pid int
Path string
}
// LaunchApp launches an app on the device with the given bundleId and arguments for iOS17+.
func (c *Connection) LaunchApp(bundleId string, args []interface{}, env map[string]interface{}, options map[string]interface{}, terminateExisting bool) (AppLaunch, error) {
msg := buildAppLaunchPayload(c.deviceId, bundleId, args, env, options, terminateExisting)
err := c.conn.Send(msg, xpc.HeartbeatRequestFlag)
if err != nil {
return AppLaunch{}, fmt.Errorf("LaunchApp: failed to send launch-app request: %w", err)
}
m, err := c.conn.ReceiveOnServerClientStream()
if err != nil {
return AppLaunch{}, fmt.Errorf("launchApp2: %w", err)
}
pid, err := pidFromResponse(m)
if err != nil {
return AppLaunch{}, fmt.Errorf("launchApp3: %w", err)
}
return AppLaunch{Pid: int(pid)}, nil
}
// Close closes the connection to the appservice
func (c *Connection) Close() error {
return c.conn.Close()
}
// ListProcesses returns a list of processes with their PID and executable path running on the device for iOS17+.
func (c *Connection) ListProcesses() ([]Process, error) {
req := buildListProcessesPayload(c.deviceId)
err := c.conn.Send(req, xpc.HeartbeatRequestFlag)
if err != nil {
return nil, fmt.Errorf("listProcesses send: %w", err)
}
res, err := c.conn.ReceiveOnServerClientStream()
if err != nil {
return nil, fmt.Errorf("listProcesses receive: %w", err)
}
output, ok := res["CoreDevice.output"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("listProcesses output")
}
tokens, ok := output["processTokens"].([]interface{})
if !ok {
return nil, fmt.Errorf("listProcesses processTokens")
}
processes := make([]Process, len(tokens))
tokensTyped, err := ios.GenericSliceToType[map[string]interface{}](tokens)
if err != nil {
return nil, fmt.Errorf("listProcesses: %w", err)
}
for i, processMap := range tokensTyped {
var p Process
pid, ok := processMap["processIdentifier"].(int64)
if !ok {
return nil, fmt.Errorf("listProcesses processIdentifier")
}
processPathMap, ok := processMap["executableURL"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("listProcesses executableURL")
}
processPath, ok := processPathMap["relative"].(string)
if !ok {
return nil, fmt.Errorf("listProcesses relative")
}
p.Pid = int(pid)
p.Path = processPath
processes[i] = p
}
return processes, nil
}
// KillProcess kills the process with the given PID for iOS17+.
func (c *Connection) KillProcess(pid int) error {
req := buildSendSignalPayload(c.deviceId, pid, syscall.SIGKILL)
err := c.conn.Send(req, xpc.HeartbeatRequestFlag)
if err != nil {
return fmt.Errorf("killProcess send: %w", err)
}
m, err := c.conn.ReceiveOnServerClientStream()
if err != nil {
return fmt.Errorf("killProcess receive: %w", err)
}
err = getError(m)
if err != nil {
return fmt.Errorf("killProcess: %w", err)
}
return nil
}
// Reboot performs a full reboot of the device for iOS17+.
// Just calls RebootWithStyle with RebootFull.
func (c *Connection) Reboot() error {
return c.RebootWithStyle(RebootFull)
}
// RebootWithStyle performs a reboot of the device with the given style for iOS17+. For style use RebootFull or RebootUserSpace.
func (c *Connection) RebootWithStyle(style string) error {
err := c.conn.Send(buildRebootPayload(c.deviceId, style))
if err != nil {
return fmt.Errorf("reboot send: %w", err)
}
m, err := c.conn.ReceiveOnServerClientStream()
if err != nil {
if errors.Is(err, io.EOF) {
return nil
}
var opErr *net.OpError
if errors.As(err, &opErr) && opErr.Timeout() {
return nil
}
return fmt.Errorf("reboot receive: %w", err)
}
err = getError(m)
if err != nil {
return fmt.Errorf("reboot: %w", err)
}
return nil
}
// ExecutableName returns the executable name for a process by removing the path.
func (p Process) ExecutableName() string {
_, file := path.Split(p.Path)
return file
}
func buildAppLaunchPayload(deviceId string, bundleId string, args []interface{}, env map[string]interface{}, options map[string]interface{}, terminateExisting bool) map[string]interface{} {
platformSpecificOptions := bytes.NewBuffer(nil)
plistEncoder := plist.NewBinaryEncoder(platformSpecificOptions)
err := plistEncoder.Encode(options)
if err != nil {
panic(err)
}
return coredevice.BuildRequest(deviceId, "com.apple.coredevice.feature.launchapplication", map[string]interface{}{
"applicationSpecifier": map[string]interface{}{
"bundleIdentifier": map[string]interface{}{
"_0": bundleId,
},
},
"options": map[string]interface{}{
"arguments": args,
"environmentVariables": env,
"platformSpecificOptions": platformSpecificOptions.Bytes(),
"standardIOUsesPseudoterminals": true,
"startStopped": false,
"terminateExisting": terminateExisting,
"user": map[string]interface{}{
"active": true,
},
"workingDirectory": nil,
},
"standardIOIdentifiers": map[string]interface{}{},
})
}
func buildListProcessesPayload(deviceId string) map[string]interface{} {
return coredevice.BuildRequest(deviceId, "com.apple.coredevice.feature.listprocesses", nil)
}
func buildRebootPayload(deviceId string, style string) map[string]interface{} {
return coredevice.BuildRequest(deviceId, "com.apple.coredevice.feature.rebootdevice", map[string]interface{}{
"rebootStyle": map[string]interface{}{
style: map[string]interface{}{},
},
})
}
func buildSendSignalPayload(deviceId string, pid int, signal syscall.Signal) map[string]interface{} {
return coredevice.BuildRequest(deviceId, "com.apple.coredevice.feature.sendsignaltoprocess", map[string]interface{}{
"process": map[string]interface{}{
"processIdentifier": int64(pid),
},
"signal": int64(signal),
})
}
func pidFromResponse(response map[string]interface{}) (int64, error) {
output, ok := response["CoreDevice.output"].(map[string]interface{})
if !ok {
return 0, fmt.Errorf("pidFromResponse: could not get pid from response")
}
processToken, ok := output["processToken"].(map[string]interface{})
if !ok {
return 0, fmt.Errorf("pidFromResponse: could not get processToken from response")
}
pid, ok := processToken["processIdentifier"].(int64)
if !ok {
return 0, fmt.Errorf("pidFromResponse: could not get pid from processToken")
}
return pid, nil
}
func getError(response map[string]interface{}) error {
if e, ok := response["CoreDevice.error"].(map[string]interface{}); ok {
return fmt.Errorf("device returned error: %+v", e)
}
return nil
}