Skip to content

Commit

Permalink
add some errors, add generic methods to extract stuff from maps and c…
Browse files Browse the repository at this point in the history
…onvert arrays
  • Loading branch information
danielpaulus committed Feb 2, 2024
1 parent 86566f3 commit 2cafa66
Show file tree
Hide file tree
Showing 4 changed files with 124 additions and 57 deletions.
131 changes: 79 additions & 52 deletions ios/appservice/appservice.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package appservice provides functions to Launch and Kill apps on an iOS devices for iOS17+.
package appservice

import (
Expand Down Expand Up @@ -29,7 +30,7 @@ const (
func New(deviceEntry ios.DeviceEntry) (*Connection, error) {
xpcConn, err := ios.ConnectToXpcServiceTunnelIface(deviceEntry, "com.apple.coredevice.appservice")
if err != nil {
return nil, err
return nil, fmt.Errorf("new: %w", err)
}

return &Connection{conn: xpcConn, deviceId: uuid.New().String()}, nil
Expand All @@ -40,95 +41,111 @@ type AppLaunch struct {
}

type Process struct {
Pid int
Pid uint64
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: %w", err)
}
m, err := c.conn.ReceiveOnServerClientStream()
if err != nil {
return AppLaunch{}, err
return AppLaunch{}, fmt.Errorf("launchApp2: %w", err)
}
pid, err := pidFromResponse(m)
if err != nil {
return AppLaunch{}, err
return AppLaunch{}, fmt.Errorf("launchApp3: %w", err)
}
return AppLaunch{Pid: 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 PID and 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, err
return nil, fmt.Errorf("listProcesses send: %w", err)
}
res, err := c.conn.ReceiveOnServerClientStream()
if err != nil {
return nil, err
}

if output, ok := res["CoreDevice.output"].(map[string]interface{}); ok {
if tokens, ok := output["processTokens"].([]interface{}); ok {
processes := make([]Process, len(tokens), len(tokens))
for i, t := range tokens {
var p Process

if processMap, ok := t.(map[string]interface{}); ok {
if pid, ok := processMap["processIdentifier"].(int64); ok {
p.Pid = int(pid)
} else {
return nil, fmt.Errorf("could not parse pid (type: %T)", processMap["processIdentifier"])
}
if processPath, ok := processMap["executableURL"].(map[string]interface{})["relative"].(string); ok {
p.Path = processPath
} else {
return nil, fmt.Errorf("could not parse process path (type: %T)", processMap["executableURL"])
}
} else {
return nil, errors.New("could not get process info")
}

processes[i] = p
}
return processes, nil
return nil, fmt.Errorf("listProcesses receive: %w", err)
}

output, err := ios.GetFromMap[map[string]interface{}]("CoreDevice.output", res)
if err != nil {
return nil, fmt.Errorf("listProcesses: %w", err)
}
tokens, err := ios.GetFromMap[[]interface{}]("processTokens", output)
if err != nil {
return nil, fmt.Errorf("listProcesses: %w", err)
}
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, err := ios.GetFromMap[int64]("processIdentifier", processMap)
if err != nil {
return nil, fmt.Errorf("listProcesses: %w", err)
}
processPathMap, err := ios.GetFromMap[map[string]interface{}]("executableURL", processMap)
if err != nil {
return nil, fmt.Errorf("listProcesses: %w", err)
}
processPath, err := ios.GetFromMap[string]("relative", processPathMap)
if err != nil {
return nil, fmt.Errorf("listProcesses: %w", err)
}

p.Pid = uint64(pid)
p.Path = processPath

processes[i] = p
}

return nil, fmt.Errorf("could not parse response")
return processes, nil
}

// KillProcess kills the process with the given PID for iOS17+.
func (c *Connection) KillProcess(pid uint64) error {
req := buildSendSignalPayload(c.deviceId, pid, syscall.SIGKILL)
err := c.conn.Send(req, xpc.HeartbeatRequestFlag)
if err != nil {
return err
return fmt.Errorf("killProcess send: %w", err)
}
m, err := c.conn.ReceiveOnServerClientStream()
if err != nil {
return err
return fmt.Errorf("killProcess receive: %w", err)
}
err = getError(m)
if err != nil {
return err
return fmt.Errorf("killProcess: %w", err)
}
return nil
}

// Reboot performs a full reboot of the device
// 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 err
return fmt.Errorf("reboot send: %w", err)
}
m, err := c.conn.ReceiveOnServerClientStream()
if err != nil {
Expand All @@ -139,9 +156,19 @@ func (c *Connection) RebootWithStyle(style string) error {
if errors.As(err, &opErr) && opErr.Timeout() {
return nil
}
return err
return fmt.Errorf("reboot receive: %w", err)
}
err = getError(m)
if err != nil {
return fmt.Errorf("reboot: %w", err)
}
return getError(m)
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{} {
Expand Down Expand Up @@ -196,14 +223,19 @@ func buildSendSignalPayload(deviceId string, pid uint64, signal syscall.Signal)
}

func pidFromResponse(response map[string]interface{}) (int64, error) {
if output, ok := response["CoreDevice.output"].(map[string]interface{}); ok {
if processToken, ok := output["processToken"].(map[string]interface{}); ok {
if pid, ok := processToken["processIdentifier"].(int64); ok {
return pid, nil
}
}
output, err := ios.GetFromMap[map[string]interface{}]("CoreDevice.output", response)
if err != nil {
return 0, fmt.Errorf("pidFromResponse: could not get pid from response")
}
return 0, fmt.Errorf("could not get pid from response")
processToken, err := ios.GetFromMap[map[string]interface{}]("processToken", output)
if err != nil {
return 0, fmt.Errorf("pidFromResponse: could not get processToken from response")
}
pid, err := ios.GetFromMap[int64]("processIdentifier", processToken)
if err != nil {
return 0, fmt.Errorf("pidFromResponse: could not get pid from processToken")
}
return pid, nil
}

func getError(response map[string]interface{}) error {
Expand All @@ -212,8 +244,3 @@ func getError(response map[string]interface{}) error {
}
return nil
}

func (p Process) ExecutableName() string {
_, file := path.Split(p.Path)
return file
}
9 changes: 4 additions & 5 deletions ios/appservice/appservice_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package appservice_test
import (
"github.com/danielpaulus/go-ios/ios"
"github.com/danielpaulus/go-ios/ios/appservice"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"os"
Expand Down Expand Up @@ -50,7 +49,7 @@ func testLaunchAndKillApp(t *testing.T, device ios.DeviceEntry) {
require.NoError(t, err)
defer as.Close()

_, err = as.LaunchApp(uuid.New().String(), "com.apple.mobilesafari", nil, nil, nil)
_, err = as.LaunchApp("com.apple.mobilesafari", nil, nil, nil, true)
require.NoError(t, err)

processes, err := as.ListProcesses()
Expand All @@ -72,12 +71,12 @@ func testKillInvalidPidReturnsError(t *testing.T, device ios.DeviceEntry) {
require.NoError(t, err)
defer as.Close()

launched, err := as.LaunchApp(uuid.New().String(), "com.apple.mobilesafari", nil, nil, nil)
launched, err := as.LaunchApp("com.apple.mobilesafari", nil, nil, nil, true)
require.NoError(t, err)

err = as.KillProcess(int(launched.Pid))
err = as.KillProcess(uint64(launched.Pid))
require.NoError(t, err)

err = as.KillProcess(int(launched.Pid))
err = as.KillProcess(uint64(launched.Pid))
assert.Error(t, err)
}
31 changes: 31 additions & 0 deletions ios/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,34 @@ func InterfaceToStringSlice(intfSlice interface{}) []string {
}
return result
}

// GetFromMap tries to get a value from a map and cast it to the given type.
// It returns an error if the conversion fails but will not panic.
// Example: b, err := GetFromMap[bool]("key", map[string]interface{}{"key": true})
func GetFromMap[T any](key string, m map[string]interface{}) (T, error) {
zero := *new(T)
var resultIntf interface{}
var ok bool
if resultIntf, ok = m[key]; !ok {
return zero, fmt.Errorf("GetFromMap: key %s not found in map", key)
}
if result, ok := resultIntf.(T); ok {
return result, nil
}
return zero, fmt.Errorf("GetFromMap: could not convert %v to %T", resultIntf, zero)
}

// GenericSliceToType tries to convert a slice of interfaces to a slice of the given type.
// It returns an error if the conversion fails but will not panic.
// Example: var b []bool; b, err = GenericSliceToType[bool]([]interface{}{true, false})
func GenericSliceToType[T any](input []interface{}) ([]T, error) {
result := make([]T, len(input))
for i, intf := range input {
if t, ok := intf.(T); ok {
result[i] = t
} else {
return []T{}, fmt.Errorf("GenericSliceToType: could not convert %v to %T", intf, result[i])
}
}
return result, nil
}
10 changes: 10 additions & 0 deletions ios/utils_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,16 @@ type SampleData struct {
FloatValue float64
}

func TestGetFromMap(t *testing.T) {
m := map[string]interface{}{"s": 3}

v, err := ios.GetFromMap[int]("s", m)
assert.Equal(t, 3, v)
assert.Nil(t, err)
v, err = ios.GetFromMap[int]("sd", m)
assert.NotNil(t, err)
}

func TestNtohs(t *testing.T) {
assert.Equal(t, uint16(62078), ios.Ntohs(ios.Lockdownport))
}
Expand Down

0 comments on commit 2cafa66

Please sign in to comment.