From 94f24f8dd01f9dfbb973900175477a3d5683a635 Mon Sep 17 00:00:00 2001 From: wangchao <31813982+wangchaoHZ@users.noreply.github.com> Date: Thu, 14 Jul 2022 02:30:51 +0800 Subject: [PATCH] fix some bug when install and run xcuitest & supoort context in forward and xcuitestrunner & add fsync (#144) * fix: install ipa failed below iOS 10 * fix: run xcuitest failed bug * support context and export StartNewProxyConnection in forward * support fsync * fix: house_arrest connection close failed when it is nil * support context in xcuitestrunner Co-authored-by: colerwang --- ios/afc/afc.go | 438 ++++++------------ ios/afc/fsync.go | 408 ++++++++++++++++ ios/afc/fsync_test.go | 125 +++++ ios/afc/house_arrest.go | 88 ---- ios/crashreport/crashreport.go | 16 +- ios/dtx_codec/channel.go | 2 + ios/dtx_codec/connection.go | 17 +- ios/forward/forward.go | 48 +- ios/house_arrest/house_arrest.go | 180 +++++++ .../house_arrest_test.go | 4 +- ios/instruments/processcontrol.go | 5 +- ios/testmanagerd/xcuitestrunner.go | 66 ++- ios/testmanagerd/xcuitestrunner_11.go | 16 +- ios/testmanagerd/xcuitestrunner_test.go | 3 +- ios/zipconduit/plists.go | 12 +- main.go | 59 ++- 16 files changed, 1044 insertions(+), 443 deletions(-) create mode 100644 ios/afc/fsync.go create mode 100644 ios/afc/fsync_test.go delete mode 100644 ios/afc/house_arrest.go create mode 100644 ios/house_arrest/house_arrest.go rename ios/{afc => house_arrest}/house_arrest_test.go (76%) diff --git a/ios/afc/afc.go b/ios/afc/afc.go index 0f39b566..4622e39d 100644 --- a/ios/afc/afc.go +++ b/ios/afc/afc.go @@ -1,334 +1,178 @@ package afc import ( - "bytes" "encoding/binary" + "errors" "fmt" - log "github.com/sirupsen/logrus" "io" - "path/filepath" - "strconv" - "strings" ) -/* -Contains a basic AFC Client. I did not implement support for everything libimobiledevice has. -It only supports files and folders, no symlinks network sockets etc. I think that is usually -not really needed anyway? Let me know if you miss anything or send a PR. -*/ const ( - afcMagic uint64 = 0x4141504c36414643 - afcHeaderSize uint64 = 40 - afcFopenWronly uint64 = 0x3 - afcFopenReadonly uint64 = 0x1 - afcOperationStatus uint64 = 0x1 - afcOpData uint64 = 0x2 - afcOperationReadDir uint64 = 0x3 - afcOperationFileOpen uint64 = 0x0000000D - afcOperationFileClose uint64 = 0x00000014 - afcOperationFileRead uint64 = 0x0000000F - afcOperationFileWrite uint64 = 0x00000010 - afcOperationFileOpenResult uint64 = 0x0000000E - afcOpRemovePathAndContents uint64 = 0x00000022 - afcOpGetFileInfo uint64 = 0x0000000A + Afc_magic uint64 = 0x4141504c36414643 + Afc_header_size uint64 = 40 + Afc_operation_status uint64 = 0x00000001 + Afc_operation_data uint64 = 0x00000002 + Afc_operation_read_dir uint64 = 0x00000003 + Afc_operation_remove_path uint64 = 0x00000008 + Afc_operation_make_dir uint64 = 0x00000009 + Afc_operation_file_info uint64 = 0x0000000A + Afc_operation_file_open uint64 = 0x0000000D + Afc_operation_file_close uint64 = 0x00000014 + Afc_operation_file_write uint64 = 0x00000010 + Afc_operation_file_open_result uint64 = 0x0000000E + Afc_operation_file_read uint64 = 0x0000000F ) -type afcPacketHeader struct { - Magic uint64 - EntireLength uint64 - ThisLength uint64 - PacketNum uint64 - Operation uint64 -} - -type afcPacket struct { - header afcPacketHeader - headerPayload []byte - payload []byte -} - -//FileInfo a struct containing file info -type FileInfo struct { - StSize int - StBlocks int - StNlink int - StIfmt string - StMtime uint64 - StBirthtime uint64 -} - -//IsDir checks if info.StIfmt == "S_IFDIR" which means the FileInfo is for a directory -func (info FileInfo) IsDir() bool { - return info.StIfmt == "S_IFDIR" -} - -//SendFile writes fileContents to the given filePath on the device -//FIXME: right now sends the entire byte array, will probably fail for larger files -func (conn *Connection) SendFile(fileContents []byte, filePath string) error { - handle, err := conn.openFileForWriting(filePath) - if err != nil { - return err - } - err = conn.sendFileContents(fileContents, handle) - if err != nil { - return err - } - return conn.closeHandle(handle) -} - -//ListFiles returns all files in the given directory, matching the pattern. -//Example: ListFiles(".", "*") returns all files and dirs in the current path the afc connection is in -func (conn *Connection) ListFiles(cwd string, matchPattern string) ([]string, error) { - headerPayload := []byte(cwd) - headerLength := uint64(len(headerPayload)) - - this_length := afcHeaderSize + headerLength - header := afcPacketHeader{Magic: afcMagic, PacketNum: conn.packageNumber, Operation: afcOperationReadDir, ThisLength: this_length, EntireLength: this_length} - conn.packageNumber++ - packet := afcPacket{header: header, headerPayload: headerPayload, payload: make([]byte, 0)} - - response, err := conn.sendAfcPacketAndAwaitResponse(packet) - if err != nil { - return []string{}, err - } - fileList := string(response.payload) - files := strings.Split(fileList, string([]byte{0})) - var filteredFiles []string - for _, f := range files { - if f == "" { - continue - } - matches, err := filepath.Match(matchPattern, f) - if err != nil { - log.Warn("error while matching pattern", err) - } - if matches { - filteredFiles = append(filteredFiles, f) - } - } - return filteredFiles, nil -} - -//DownloadFile streams filecontents of file from the device to the given writer. -func (conn *Connection) DownloadFile(file string, target io.Writer) error { - handle, err := conn.openFileForReading(file) - if err != nil { - return err - } - log.Debugf("remote file %s open with handle %d", file, handle) - - totalBytes := 0 - bytesRead := 1 - for bytesRead > 0 { - data, n, readErr := conn.readBytes(handle, 4096) - if readErr != nil { - return readErr - } - bytesRead = n - totalBytes += bytesRead - _, err = target.Write(data) - if err != nil { - return err - } - } - log.Debugf("finished reading %d kb %s", totalBytes/1024, file) - return conn.closeHandle(handle) -} - -//Delete removes a given file or directory -func (conn *Connection) Delete(path string) error { - log.Debugf("Delete:%s", path) - headerPayload := []byte(path) - this_length := afcHeaderSize + uint64(len(headerPayload)) - header := afcPacketHeader{Magic: afcMagic, PacketNum: conn.packageNumber, Operation: afcOpRemovePathAndContents, ThisLength: this_length, EntireLength: this_length} - conn.packageNumber++ - packet := afcPacket{header: header, headerPayload: headerPayload, payload: make([]byte, 0)} - response, err := conn.sendAfcPacketAndAwaitResponse(packet) - log.Debugf("%+v", response) - return err -} - -//GetFileInfo gets basic info about the path on the device -func (conn *Connection) GetFileInfo(path string) (FileInfo, error) { - log.Debugf("GetFileInfo:%s", path) - headerPayload := []byte(path) - this_length := afcHeaderSize + uint64(len(headerPayload)) - header := afcPacketHeader{Magic: afcMagic, PacketNum: conn.packageNumber, Operation: afcOpGetFileInfo, ThisLength: this_length, EntireLength: this_length} - conn.packageNumber++ - packet := afcPacket{header: header, headerPayload: headerPayload, payload: make([]byte, 0)} - response, err := conn.sendAfcPacketAndAwaitResponse(packet) - log.Debugf("%+v", response) - if err != nil { - return FileInfo{}, err - } - - if len(response.payload) != int(response.header.EntireLength-response.header.ThisLength) { - return FileInfo{}, fmt.Errorf("not all payload data was received") - } - if response.header.Operation != afcOpData { - return FileInfo{}, fmt.Errorf("expected afcOpData got: %d", response.header.Operation) - } - res := bytes.Split(response.payload, []byte{0}) - result := FileInfo{} - for i, b := range res { - switch string(b) { - case "st_size": - result.StSize, _ = strconv.Atoi(string(res[i+1])) - break - case "st_blocks": - result.StBlocks, _ = strconv.Atoi(string(res[i+1])) - break - case "st_nlink": - result.StNlink, _ = strconv.Atoi(string(res[i+1])) - break - case "st_ifmt": - result.StIfmt = string(res[i+1]) - break - case "st_mtime": - result.StMtime, _ = strconv.ParseUint(string(res[i+1]), 0, 64) - break - case "st_birthtime": - result.StBirthtime, _ = strconv.ParseUint(string(res[i+1]), 0, 64) - break - } - } - return result, nil -} - -func (conn *Connection) openFileForWriting(filePath string) (byte, error) { - pathBytes := []byte(filePath) - headerLength := 8 + uint64(len(pathBytes)) - headerPayload := make([]byte, headerLength) - binary.LittleEndian.PutUint64(headerPayload, afcFopenWronly) - copy(headerPayload[8:], pathBytes) - this_length := afcHeaderSize + headerLength - header := afcPacketHeader{Magic: afcMagic, PacketNum: conn.packageNumber, Operation: afcOperationFileOpen, ThisLength: this_length, EntireLength: this_length} - conn.packageNumber++ - packet := afcPacket{header: header, headerPayload: headerPayload, payload: make([]byte, 0)} - - response, err := conn.sendAfcPacketAndAwaitResponse(packet) - if err != nil { - return 0, err - } - if response.header.Operation != afcOperationFileOpenResult { - return 0, fmt.Errorf("Unexpected afc response, expected %x received %x", afcOperationStatus, response.header.Operation) - } - return response.headerPayload[0], nil -} - -func (conn *Connection) openFileForReading(filePath string) (byte, error) { - pathBytes := []byte(filePath) - headerLength := 8 + uint64(len(pathBytes)) - headerPayload := make([]byte, headerLength) - binary.LittleEndian.PutUint64(headerPayload, afcFopenReadonly) - copy(headerPayload[8:], pathBytes) - this_length := afcHeaderSize + headerLength - header := afcPacketHeader{Magic: afcMagic, PacketNum: conn.packageNumber, Operation: afcOperationFileOpen, ThisLength: this_length, EntireLength: this_length} - conn.packageNumber++ - packet := afcPacket{header: header, headerPayload: headerPayload, payload: make([]byte, 0)} - - response, err := conn.sendAfcPacketAndAwaitResponse(packet) - if err != nil { - return 0, err - } - if response.header.Operation != afcOperationFileOpenResult { - return 0, fmt.Errorf("Unexpected afc response, expected %x received %x", afcOperationStatus, response.header.Operation) - } - return response.headerPayload[0], nil -} - -func (conn *Connection) readBytes(handle byte, length int) ([]byte, int, error) { - headerPayload := make([]byte, 16) - binary.LittleEndian.PutUint64(headerPayload, uint64(handle)) - binary.LittleEndian.PutUint64(headerPayload[8:], uint64(length)) - payloadLength := uint64(16) - header := afcPacketHeader{Magic: afcMagic, PacketNum: conn.packageNumber, Operation: afcOperationFileRead, ThisLength: payloadLength + afcHeaderSize, EntireLength: payloadLength + afcHeaderSize} - conn.packageNumber++ - packet := afcPacket{header: header, headerPayload: headerPayload, payload: make([]byte, 0)} - response, err := conn.sendAfcPacketAndAwaitResponse(packet) - if err != nil { - return []byte{}, 0, err - } - return response.payload, len(response.payload), nil -} - -func (conn *Connection) sendAfcPacketAndAwaitResponse(packet afcPacket) (afcPacket, error) { - err := encode(packet, conn.deviceConn.Writer()) - if err != nil { - return afcPacket{}, err - } - return decode(conn.deviceConn.Reader()) -} - -func (conn *Connection) sendFileContents(fileContents []byte, handle byte) error { - headerPayload := make([]byte, 8) - headerPayload[0] = handle - header := afcPacketHeader{Magic: afcMagic, PacketNum: conn.packageNumber, Operation: afcOperationFileWrite, ThisLength: 8 + afcHeaderSize, EntireLength: 8 + afcHeaderSize + uint64(len(fileContents))} - conn.packageNumber++ - packet := afcPacket{header: header, headerPayload: headerPayload, payload: fileContents} - response, err := conn.sendAfcPacketAndAwaitResponse(packet) - if err != nil { - return err - } - if response.header.Operation != afcOperationStatus { - return fmt.Errorf("Unexpected afc response, expected %x received %x", afcOperationStatus, response.header.Operation) - } - return nil -} +const ( + Afc_Mode_RDONLY uint64 = 0x00000001 + Afc_Mode_RW uint64 = 0x00000002 + Afc_Mode_WRONLY uint64 = 0x00000003 + Afc_Mode_WR uint64 = 0x00000004 + Afc_Mode_APPEND uint64 = 0x00000005 + Afc_Mode_RDAPPEND uint64 = 0x00000006 +) -func (conn *Connection) closeHandle(handle byte) error { - headerPayload := make([]byte, 8) - headerPayload[0] = handle - this_length := 8 + afcHeaderSize - header := afcPacketHeader{Magic: afcMagic, PacketNum: conn.packageNumber, Operation: afcOperationFileClose, ThisLength: this_length, EntireLength: this_length} - conn.packageNumber++ - packet := afcPacket{header: header, headerPayload: headerPayload, payload: make([]byte, 0)} - response, err := conn.sendAfcPacketAndAwaitResponse(packet) - if err != nil { - return err - } - if response.header.Operation != afcOperationStatus { - return fmt.Errorf("Unexpected afc response, expected %x received %x", afcOperationStatus, response.header.Operation) - } - return nil -} +const ( + Afc_Err_Success = 0 + Afc_Err_UnknownError = 1 + Afc_Err_OperationHeaderInvalid = 2 + Afc_Err_NoResources = 3 + Afc_Err_ReadError = 4 + Afc_Err_WriteError = 5 + Afc_Err_UnknownPacketType = 6 + Afc_Err_InvalidArgument = 7 + Afc_Err_ObjectNotFound = 8 + Afc_Err_ObjectIsDir = 9 + Afc_Err_PermDenied = 10 + Afc_Err_ServiceNotConnected = 11 + Afc_Err_OperationTimeout = 12 + Afc_Err_TooMuchData = 13 + Afc_Err_EndOfData = 14 + Afc_Err_OperationNotSupported = 15 + Afc_Err_ObjectExists = 16 + Afc_Err_ObjectBusy = 17 + Afc_Err_NoSpaceLeft = 18 + Afc_Err_OperationWouldBlock = 19 + Afc_Err_IoError = 20 + Afc_Err_OperationInterrupted = 21 + Afc_Err_OperationInProgress = 22 + Afc_Err_InternalError = 23 + Afc_Err_MuxError = 30 + Afc_Err_NoMemory = 31 + Afc_Err_NotEnoughData = 32 + Afc_Err_DirNotEmpty = 33 +) -func decode(reader io.Reader) (afcPacket, error) { - var header afcPacketHeader +func getError(errorCode uint64) error { + switch errorCode { + case Afc_Err_UnknownError: + return errors.New("UnknownError") + case Afc_Err_OperationHeaderInvalid: + return errors.New("OperationHeaderInvalid") + case Afc_Err_NoResources: + return errors.New("NoResources") + case Afc_Err_ReadError: + return errors.New("ReadError") + case Afc_Err_WriteError: + return errors.New("WriteError") + case Afc_Err_UnknownPacketType: + return errors.New("UnknownPacketType") + case Afc_Err_InvalidArgument: + return errors.New("InvalidArgument") + case Afc_Err_ObjectNotFound: + return errors.New("ObjectNotFound") + case Afc_Err_ObjectIsDir: + return errors.New("ObjectIsDir") + case Afc_Err_PermDenied: + return errors.New("PermDenied") + case Afc_Err_ServiceNotConnected: + return errors.New("ServiceNotConnected") + case Afc_Err_OperationTimeout: + return errors.New("OperationTimeout") + case Afc_Err_TooMuchData: + return errors.New("TooMuchData") + case Afc_Err_EndOfData: + return errors.New("EndOfData") + case Afc_Err_OperationNotSupported: + return errors.New("OperationNotSupported") + case Afc_Err_ObjectExists: + return errors.New("ObjectExists") + case Afc_Err_ObjectBusy: + return errors.New("ObjectBusy") + case Afc_Err_NoSpaceLeft: + return errors.New("NoSpaceLeft") + case Afc_Err_OperationWouldBlock: + return errors.New("OperationWouldBlock") + case Afc_Err_IoError: + return errors.New("IoError") + case Afc_Err_OperationInterrupted: + return errors.New("OperationInterrupted") + case Afc_Err_OperationInProgress: + return errors.New("OperationInProgress") + case Afc_Err_InternalError: + return errors.New("InternalError") + case Afc_Err_MuxError: + return errors.New("MuxError") + case Afc_Err_NoMemory: + return errors.New("NoMemory") + case Afc_Err_NotEnoughData: + return errors.New("NotEnoughData") + case Afc_Err_DirNotEmpty: + return errors.New("DirNotEmpty") + default: + return nil + } +} + +type AfcPacketHeader struct { + Magic uint64 + Entire_length uint64 + This_length uint64 + Packet_num uint64 + Operation uint64 +} + +type AfcPacket struct { + Header AfcPacketHeader + HeaderPayload []byte + Payload []byte +} + +func Decode(reader io.Reader) (AfcPacket, error) { + var header AfcPacketHeader err := binary.Read(reader, binary.LittleEndian, &header) if err != nil { - return afcPacket{}, err + return AfcPacket{}, err } - if header.Magic != afcMagic { - return afcPacket{}, fmt.Errorf("Wrong magic:%x expected: %x", header.Magic, afcMagic) + if header.Magic != Afc_magic { + return AfcPacket{}, fmt.Errorf("Wrong magic:%x expected: %x", header.Magic, Afc_magic) } - headerPayloadLength := header.ThisLength - afcHeaderSize + headerPayloadLength := header.This_length - Afc_header_size headerPayload := make([]byte, headerPayloadLength) _, err = io.ReadFull(reader, headerPayload) if err != nil { - return afcPacket{}, err + return AfcPacket{}, err } - - contentPayloadLength := header.EntireLength - header.ThisLength + contentPayloadLength := header.Entire_length - header.This_length payload := make([]byte, contentPayloadLength) _, err = io.ReadFull(reader, payload) if err != nil { - return afcPacket{}, err + return AfcPacket{}, err } - return afcPacket{header, headerPayload, payload}, nil + return AfcPacket{header, headerPayload, payload}, nil } -func encode(packet afcPacket, writer io.Writer) error { - err := binary.Write(writer, binary.LittleEndian, packet.header) +func Encode(packet AfcPacket, writer io.Writer) error { + err := binary.Write(writer, binary.LittleEndian, packet.Header) if err != nil { return err } - _, err = writer.Write(packet.headerPayload) + _, err = writer.Write(packet.HeaderPayload) if err != nil { return err } - _, err = writer.Write(packet.payload) + _, err = writer.Write(packet.Payload) if err != nil { return err } diff --git a/ios/afc/fsync.go b/ios/afc/fsync.go new file mode 100644 index 00000000..7ac82141 --- /dev/null +++ b/ios/afc/fsync.go @@ -0,0 +1,408 @@ +package afc + +import ( + "bytes" + "encoding/binary" + "fmt" + "github.com/danielpaulus/go-ios/ios" + log "github.com/sirupsen/logrus" + "io" + "os" + "path" + "path/filepath" + "strconv" + "strings" +) + +const serviceName = "com.apple.afc" + +type Connection struct { + deviceConn ios.DeviceConnectionInterface + packageNumber uint64 +} + +type statInfo struct { + stSize int64 + stBlocks int64 + stCtime int64 + stMtime int64 + stNlink string + stIfmt string + stLinktarget string +} + +func (s *statInfo) IsDir() bool { + return s.stIfmt == "S_IFDIR" +} + +func (s *statInfo) IsLink() bool { + return s.stIfmt == "S_IFLNK" +} + +func New(device ios.DeviceEntry) (*Connection, error) { + deviceConn, err := ios.ConnectToService(device, serviceName) + if err != nil { + return nil, err + } + return &Connection{deviceConn: deviceConn}, nil +} + +//NewFromConn allows to use AFC on a DeviceConnectionInterface, see crashreport for an example +func NewFromConn(deviceConn ios.DeviceConnectionInterface) *Connection { + return &Connection{deviceConn: deviceConn} +} + +func (conn *Connection) sendAfcPacketAndAwaitResponse(packet AfcPacket) (AfcPacket, error) { + err := Encode(packet, conn.deviceConn.Writer()) + if err != nil { + return AfcPacket{}, err + } + return Decode(conn.deviceConn.Reader()) +} + +func (conn *Connection) checkOperationStatus(packet AfcPacket) error { + if packet.Header.Operation == Afc_operation_status { + errorCode := binary.LittleEndian.Uint64(packet.HeaderPayload) + if errorCode != Afc_Err_Success { + return getError(errorCode) + } + } + return nil +} + +func (conn *Connection) Remove(path string) error { + headerPayload := []byte(path) + headerLength := uint64(len(headerPayload)) + thisLength := Afc_header_size + headerLength + + header := AfcPacketHeader{Magic: Afc_magic, Packet_num: conn.packageNumber, Operation: Afc_operation_remove_path, This_length: thisLength, Entire_length: thisLength} + conn.packageNumber++ + packet := AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: make([]byte, 0)} + response, err := conn.sendAfcPacketAndAwaitResponse(packet) + if err != nil { + return err + } + if err = conn.checkOperationStatus(response); err != nil { + return fmt.Errorf("remove: unexpected afc status: %v", err) + } + return nil +} + +func (conn *Connection) MkDir(path string) error { + headerPayload := []byte(path) + headerLength := uint64(len(headerPayload)) + thisLength := Afc_header_size + headerLength + + header := AfcPacketHeader{Magic: Afc_magic, Packet_num: conn.packageNumber, Operation: Afc_operation_make_dir, This_length: thisLength, Entire_length: thisLength} + conn.packageNumber++ + packet := AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: make([]byte, 0)} + response, err := conn.sendAfcPacketAndAwaitResponse(packet) + if err != nil { + return err + } + if err = conn.checkOperationStatus(response); err != nil { + return fmt.Errorf("mkdir: unexpected afc status: %v", err) + } + return nil +} + +func (conn *Connection) Stat(path string) (*statInfo, error) { + headerPayload := []byte(path) + headerLength := uint64(len(headerPayload)) + thisLength := Afc_header_size + headerLength + + header := AfcPacketHeader{Magic: Afc_magic, Packet_num: conn.packageNumber, Operation: Afc_operation_file_info, This_length: thisLength, Entire_length: thisLength} + conn.packageNumber++ + packet := AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: make([]byte, 0)} + response, err := conn.sendAfcPacketAndAwaitResponse(packet) + if err != nil { + return nil, err + } + if err = conn.checkOperationStatus(response); err != nil { + return nil, fmt.Errorf("stat: unexpected afc status: %v", err) + } + ret := bytes.Split(response.Payload, []byte{0}) + retLen := len(ret) + if retLen%2 != 0 { + retLen = retLen - 1 + } + statInfoMap := make(map[string]string) + for i := 0; i <= retLen-2; i = i + 2 { + k := string(ret[i]) + v := string(ret[i+1]) + statInfoMap[k] = v + } + + var si statInfo + si.stSize, _ = strconv.ParseInt(statInfoMap["st_size"], 10, 64) + si.stBlocks, _ = strconv.ParseInt(statInfoMap["st_blocks"], 10, 64) + si.stCtime, _ = strconv.ParseInt(statInfoMap["st_birthtime"], 10, 64) + si.stMtime, _ = strconv.ParseInt(statInfoMap["st_mtime"], 10, 64) + si.stNlink = statInfoMap["st_nlink"] + si.stIfmt = statInfoMap["st_ifmt"] + si.stLinktarget = statInfoMap["st_linktarget"] + return &si, nil +} + +func (conn *Connection) listDir(path string) ([]string, error) { + headerPayload := []byte(path) + headerLength := uint64(len(headerPayload)) + thisLength := Afc_header_size + headerLength + + header := AfcPacketHeader{Magic: Afc_magic, Packet_num: conn.packageNumber, Operation: Afc_operation_read_dir, This_length: thisLength, Entire_length: thisLength} + conn.packageNumber++ + packet := AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: make([]byte, 0)} + response, err := conn.sendAfcPacketAndAwaitResponse(packet) + if err != nil { + return nil, err + } + if err = conn.checkOperationStatus(response); err != nil { + return nil, fmt.Errorf("list dir: unexpected afc status: %v", err) + } + ret := bytes.Split(response.Payload, []byte{0}) + var fileList []string + for _, v := range ret { + if string(v) != "." && string(v) != ".." && string(v) != "" { + fileList = append(fileList, string(v)) + } + } + return fileList, nil +} + +//ListFiles returns all files in the given directory, matching the pattern. +//Example: ListFiles(".", "*") returns all files and dirs in the current path the afc connection is in +func (conn *Connection) ListFiles(cwd string, matchPattern string) ([]string, error) { + headerPayload := []byte(cwd) + headerLength := uint64(len(headerPayload)) + + thisLength := Afc_header_size + headerLength + header := AfcPacketHeader{Magic: Afc_magic, Packet_num: conn.packageNumber, Operation: Afc_operation_read_dir, This_length: thisLength, Entire_length: thisLength} + conn.packageNumber++ + packet := AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: make([]byte, 0)} + + response, err := conn.sendAfcPacketAndAwaitResponse(packet) + if err != nil { + return nil, err + } + fileList := string(response.Payload) + files := strings.Split(fileList, string([]byte{0})) + var filteredFiles []string + for _, f := range files { + if f == "" { + continue + } + matches, err := filepath.Match(matchPattern, f) + if err != nil { + log.Warn("error while matching pattern", err) + } + if matches { + filteredFiles = append(filteredFiles, f) + } + } + return filteredFiles, nil +} + +func (conn *Connection) TreeView(dpath string, prefix string, treePoint bool) error { + fileInfo, err := conn.Stat(dpath) + if err != nil { + return err + } + namePrefix := "`--" + if !treePoint { + namePrefix = "|--" + } + tPrefix := prefix + namePrefix + if fileInfo.IsDir() { + fmt.Printf("%s %s/\n", tPrefix, filepath.Base(dpath)) + fileList, err := conn.listDir(dpath) + if err != nil { + return err + } + for i, v := range fileList { + tp := false + if i == len(fileList)-1 { + tp = true + } + rp := prefix + " " + if !treePoint { + rp = prefix + "| " + } + nPath := path.Join(dpath, v) + err = conn.TreeView(nPath, rp, tp) + if err != nil { + return err + } + } + } else { + fmt.Printf("%s %s\n", tPrefix, filepath.Base(dpath)) + } + return nil +} + +func (conn *Connection) openFile(path string, mode uint64) (byte, error) { + pathBytes := []byte(path) + headerLength := 8 + uint64(len(pathBytes)) + headerPayload := make([]byte, headerLength) + binary.LittleEndian.PutUint64(headerPayload, mode) + copy(headerPayload[8:], pathBytes) + thisLength := Afc_header_size + headerLength + header := AfcPacketHeader{Magic: Afc_magic, Packet_num: conn.packageNumber, Operation: Afc_operation_file_open, This_length: thisLength, Entire_length: thisLength} + conn.packageNumber++ + packet := AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: make([]byte, 0)} + + response, err := conn.sendAfcPacketAndAwaitResponse(packet) + if err != nil { + return 0, err + } + if err = conn.checkOperationStatus(response); err != nil { + return 0, fmt.Errorf("open file: unexpected afc status: %v", err) + } + return response.HeaderPayload[0], nil +} + +func (conn *Connection) closeFile(handle byte) error { + headerPayload := make([]byte, 8) + headerPayload[0] = handle + thisLength := 8 + Afc_header_size + header := AfcPacketHeader{Magic: Afc_magic, Packet_num: conn.packageNumber, Operation: Afc_operation_file_close, This_length: thisLength, Entire_length: thisLength} + conn.packageNumber++ + packet := AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: make([]byte, 0)} + response, err := conn.sendAfcPacketAndAwaitResponse(packet) + if err != nil { + return err + } + if err = conn.checkOperationStatus(response); err != nil { + return fmt.Errorf("close file: unexpected afc status: %v", err) + } + return nil +} + +func (conn *Connection) PullSingleFile(srcPath, dstPath string) error { + fileInfo, err := conn.Stat(srcPath) + if err != nil { + return err + } + if fileInfo.IsLink() { + srcPath = fileInfo.stLinktarget + } + fd, err := conn.openFile(srcPath, Afc_Mode_RDONLY) + if err != nil { + return err + } + defer conn.closeFile(fd) + + f, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm) + if err != nil { + return err + } + defer f.Close() + + leftSize := fileInfo.stSize + maxReadSize := 64 * 1024 + for leftSize > 0 { + headerPayload := make([]byte, 16) + headerPayload[0] = fd + thisLength := Afc_header_size + 16 + binary.LittleEndian.PutUint64(headerPayload[8:], uint64(maxReadSize)) + header := AfcPacketHeader{Magic: Afc_magic, Packet_num: conn.packageNumber, Operation: Afc_operation_file_read, This_length: thisLength, Entire_length: thisLength} + conn.packageNumber++ + packet := AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: make([]byte, 0)} + response, err := conn.sendAfcPacketAndAwaitResponse(packet) + if err != nil { + return err + } + if err = conn.checkOperationStatus(response); err != nil { + return fmt.Errorf("read file: unexpected afc status: %v", err) + } + leftSize = leftSize - int64(len(response.Payload)) + f.Write(response.Payload) + } + return nil +} + +func (conn *Connection) Pull(srcPath, dstPath string) error { + fileInfo, err := conn.Stat(srcPath) + if err != nil { + return err + } + if fileInfo.IsDir() { + ret, _ := ios.PathExists(dstPath) + if !ret { + err = os.MkdirAll(dstPath, os.ModePerm) + if err != nil { + return err + } + } + fileList, err := conn.listDir(srcPath) + if err != nil { + return err + } + for _, v := range fileList { + sp := path.Join(srcPath, v) + dp := path.Join(dstPath, v) + err = conn.Pull(sp, dp) + if err != nil { + return err + } + } + } else { + return conn.PullSingleFile(srcPath, dstPath) + } + return nil +} + +func (conn *Connection) Push(srcPath, dstPath string) error { + ret, _ := ios.PathExists(srcPath) + if !ret { + return fmt.Errorf("%s: no such file.", srcPath) + } + + f, err := os.Open(srcPath) + if err != nil { + return err + } + defer f.Close() + + if fileInfo, _ := conn.Stat(dstPath); fileInfo != nil { + if fileInfo.IsDir() { + dstPath = path.Join(dstPath, filepath.Base(srcPath)) + } + } + + fd, err := conn.openFile(dstPath, Afc_Mode_WR) + if err != nil { + return err + } + defer conn.closeFile(fd) + + maxWriteSize := 64 * 1024 + chunk := make([]byte, maxWriteSize) + for { + n, err := f.Read(chunk) + if err != nil && err != io.EOF { + return err + } + if n == 0 { + break + } + + headerPayload := make([]byte, 8) + headerPayload[0] = fd + thisLength := Afc_header_size + 8 + header := AfcPacketHeader{Magic: Afc_magic, Packet_num: conn.packageNumber, Operation: Afc_operation_file_write, This_length: thisLength, Entire_length: thisLength + uint64(n)} + conn.packageNumber++ + packet := AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: chunk} + response, err := conn.sendAfcPacketAndAwaitResponse(packet) + if err != nil { + return err + } + if err = conn.checkOperationStatus(response); err != nil { + return fmt.Errorf("write file: unexpected afc status: %v", err) + } + } + return nil +} + +func (conn *Connection) Close() { + conn.deviceConn.Close() +} diff --git a/ios/afc/fsync_test.go b/ios/afc/fsync_test.go new file mode 100644 index 00000000..9769949a --- /dev/null +++ b/ios/afc/fsync_test.go @@ -0,0 +1,125 @@ +package afc + +/* +const test_device_udid = "your device udid" + +func TestConnection_Remove(t *testing.T) { + deviceEnrty, _ := ios.GetDevice(test_device_udid) + + conn, err := New(deviceEnrty) + if err != nil { + log.Fatalf("connect service failed: %v", err) + } + + err = conn.Remove("/DCIM/goios") + if err != nil { + log.Fatalf("remove failed:%v", err) + } +} + +func TestConnection_Mkdir(t *testing.T) { + deviceEnrty, _ := ios.GetDevice(test_device_udid) + + conn, err := New(deviceEnrty) + if err != nil { + log.Fatalf("connect service failed: %v", err) + } + + err = conn.MkDir("/DCIM/TestDir") + if err != nil { + log.Fatalf("mkdir failed:%v", err) + } +} + +func TestConnection_stat(t *testing.T) { + deviceEnrty, _ := ios.GetDevice(test_device_udid) + + conn, err := New(deviceEnrty) + if err != nil { + log.Fatalf("connect service failed: %v", err) + } + + si, err := conn.Stat("/DCIM/architecture_diagram.png") + if err != nil { + log.Fatalf("get Stat failed:%v", err) + } + log.Printf("Stat :%+v", si) +} + +func TestConnection_listDir(t *testing.T) { + deviceEnrty, _ := ios.GetDevice(test_device_udid) + + conn, err := New(deviceEnrty) + if err != nil { + log.Fatalf("connect service failed: %v", err) + } + + flist, err := conn.listDir("/DCIM/") + if err != nil { + log.Fatalf("tree view failed:%v", err) + } + for _, v := range flist { + fmt.Printf("path: %+v\n", v) + } +} + +func TestConnection_TreeView(t *testing.T) { + deviceEnrty, _ := ios.GetDevice(test_device_udid) + + conn, err := New(deviceEnrty) + if err != nil { + log.Fatalf("connect service failed: %v", err) + } + + err = conn.TreeView("/DCIM/", "", true) + if err != nil { + log.Fatalf("tree view failed:%v", err) + } +} + +func TestConnection_pullSingleFile(t *testing.T) { + deviceEnrty, _ := ios.GetDevice(test_device_udid) + + conn, err := New(deviceEnrty) + if err != nil { + log.Fatalf("connect service failed: %v", err) + } + + err = conn.PullSingleFile("/DCIM/architecture_diagram.png", "architecture_diagram.png") + if err != nil { + log.Fatalf("pull single file failed:%v", err) + } +} + +func TestConnection_Pull(t *testing.T) { + deviceEnrty, _ := ios.GetDevice(test_device_udid) + + conn, err := New(deviceEnrty) + if err != nil { + log.Fatalf("connect service failed: %v", err) + } + srcPath := "/DCIM/" + dstpath := "TempRecv" + dstpath = path.Join(dstpath, srcPath) + err = conn.Pull(srcPath, dstpath) + if err != nil { + log.Fatalf("pull failed:%v", err) + } +} + +func TestConnection_Push(t *testing.T) { + deviceEnrty, _ := ios.GetDevice(test_device_udid) + conn, err := New(deviceEnrty) + if err != nil { + log.Fatalf("connect service failed: %v", err) + } + + srcPath := "your src path" + dstpath := "your dst path" + + err = conn.Push(srcPath, dstpath) + if err != nil { + log.Fatalf("push failed:%v", err) + } +} +*/ \ No newline at end of file diff --git a/ios/afc/house_arrest.go b/ios/afc/house_arrest.go deleted file mode 100644 index fb2b9998..00000000 --- a/ios/afc/house_arrest.go +++ /dev/null @@ -1,88 +0,0 @@ -package afc - -import ( - "bytes" - "errors" - "fmt" - "github.com/danielpaulus/go-ios/ios" - - "howett.net/plist" -) - -const serviceName = "com.apple.mobile.house_arrest" - -type Connection struct { - deviceConn ios.DeviceConnectionInterface - packageNumber uint64 -} - -//NewFromConn allows to use AFC on a DeviceConnectionInterface, see crashreport for an example -func NewFromConn(deviceConn ios.DeviceConnectionInterface) *Connection { - return &Connection{deviceConn: deviceConn} -} - -//NewWithHouseArrest gives an AFC connection to an app container using the house arrest service -func NewWithHouseArrest(device ios.DeviceEntry, bundleID string) (*Connection, error) { - deviceConn, err := ios.ConnectToService(device, serviceName) - if err != nil { - return &Connection{}, err - } - err = vendContainer(deviceConn, bundleID) - if err != nil { - return &Connection{}, err - } - return &Connection{deviceConn: deviceConn}, nil -} - -func vendContainer(deviceConn ios.DeviceConnectionInterface, bundleID string) error { - plistCodec := ios.NewPlistCodec() - vendContainer := map[string]interface{}{"Command": "VendContainer", "Identifier": bundleID} - msg, err := plistCodec.Encode(vendContainer) - if err != nil { - return fmt.Errorf("VendContainer Encoding cannot fail unless the encoder is broken: %v", err) - } - err = deviceConn.Send(msg) - if err != nil { - return err - } - reader := deviceConn.Reader() - response, err := plistCodec.Decode(reader) - if err != nil { - return err - } - return checkResponse(response) -} - -func checkResponse(vendContainerResponseBytes []byte) error { - response, err := plistFromBytes(vendContainerResponseBytes) - if err != nil { - return err - } - if "Complete" == response.Status { - return nil - } - if response.Error != "" { - return errors.New(response.Error) - } - return errors.New("unknown error during vendcontainer") -} - -func plistFromBytes(plistBytes []byte) (vendContainerResponse, error) { - var vendResponse vendContainerResponse - decoder := plist.NewDecoder(bytes.NewReader(plistBytes)) - - err := decoder.Decode(&vendResponse) - if err != nil { - return vendResponse, err - } - return vendResponse, nil -} - -type vendContainerResponse struct { - Status string - Error string -} - -func (conn Connection) Close() error { - return conn.deviceConn.Close() -} diff --git a/ios/crashreport/crashreport.go b/ios/crashreport/crashreport.go index 72c78133..fa427ac5 100644 --- a/ios/crashreport/crashreport.go +++ b/ios/crashreport/crashreport.go @@ -50,14 +50,14 @@ func copyReports(afc *afc.Connection, cwd string, pattern string, targetDir stri devicePath := path.Join(cwd, f) targetFilePath := path.Join(targetDir, f) log.WithFields(log.Fields{"from": devicePath, "to": targetFilePath}).Info("downloading") - info, err := afc.GetFileInfo(devicePath) + info, err := afc.Stat(devicePath) if err != nil { log.Warnf("failed getting info for file: %s, skipping", f) continue } log.Debugf("%+v", info) - if info.IsDir() { + if info.IsDir(){ err := os.Mkdir(targetFilePath, targetDirInfo.Mode().Perm()) if err != nil { return err @@ -69,15 +69,7 @@ func copyReports(afc *afc.Connection, cwd string, pattern string, targetDir stri continue } - targetFileHandle, err := os.Create(targetFilePath) - if err != nil { - return err - } - err = afc.DownloadFile(devicePath, targetFileHandle) - if err != nil { - return err - } - err = targetFileHandle.Close() + err = afc.PullSingleFile(devicePath, targetFilePath) if err != nil { return err } @@ -109,7 +101,7 @@ func RemoveReports(device ios.DeviceEntry, cwd string, pattern string) error { continue } log.WithFields(log.Fields{"path": path.Join(cwd, f)}).Info("delete") - err := afc.Delete(path.Join(cwd, f)) + err := afc.Remove(path.Join(cwd, f)) if err != nil { return err } diff --git a/ios/dtx_codec/channel.go b/ios/dtx_codec/channel.go index c4da2f59..51750682 100644 --- a/ios/dtx_codec/channel.go +++ b/ios/dtx_codec/channel.go @@ -57,6 +57,7 @@ func (d *Channel) MethodCall(selector string, args ...interface{}) (Message, err msg, err := d.SendAndAwaitReply(true, Methodinvocation, payload, auxiliary) if err != nil { log.WithFields(log.Fields{"channel_id": d.channelName, "error": err, "methodselector": selector}).Info("failed starting invoking method") + return msg, err } if msg.HasError() { return msg, fmt.Errorf("Failed invoking method '%s' with error: %s", selector, msg.Payload[0]) @@ -73,6 +74,7 @@ func (d *Channel) MethodCallAsync(selector string, args ...interface{}) error { err := d.Send(false, Methodinvocation, payload, auxiliary) if err != nil { log.WithFields(log.Fields{"channel_id": d.channelName, "error": err, "methodselector": selector}).Info("failed starting invoking method") + return err } return nil } diff --git a/ios/dtx_codec/connection.go b/ios/dtx_codec/connection.go index dec0fd63..d7ba9d18 100644 --- a/ios/dtx_codec/connection.go +++ b/ios/dtx_codec/connection.go @@ -18,7 +18,7 @@ type MethodWithResponse func(msg Message) (interface{}, error) type Connection struct { deviceConnection ios.DeviceConnectionInterface channelCodeCounter int - activeChannels map[int]*Channel + activeChannels sync.Map globalChannel *Channel capabilities map[string]interface{} mutex sync.Mutex @@ -72,8 +72,10 @@ func (g GlobalDispatcher) Dispatch(msg Message) { } //TODO: use the dispatchFunctions map if "outputReceived:fromProcess:atTime:" == msg.Payload[0] { - msg, _ := nskeyedarchiver.Unarchive(msg.Auxiliary.GetArguments()[0].([]byte)) - log.Info(msg[0]) + msg, err := nskeyedarchiver.Unarchive(msg.Auxiliary.GetArguments()[0].([]byte)) + if err == nil { + log.Info(msg[0]) + } return } } @@ -96,7 +98,7 @@ func NewConnection(device ios.DeviceEntry, serviceName string) (*Connection, err requestChannelMessages := make(chan Message, 5) //The global channel has channelCode 0, so we need to start with channelCodeCounter==1 - dtxConnection := &Connection{deviceConnection: conn, channelCodeCounter: 1, activeChannels: map[int]*Channel{}, requestChannelMessages: requestChannelMessages} + dtxConnection := &Connection{deviceConnection: conn, channelCodeCounter: 1, requestChannelMessages: requestChannelMessages} //The global channel is automatically present and used for requesting other channels and some other methods like notifyPublishedCapabilities globalChannel := Channel{channelCode: 0, @@ -132,7 +134,8 @@ func reader(dtxConn *Connection) { return } - if channel, ok := dtxConn.activeChannels[msg.ChannelCode]; ok { + if _channel, ok := dtxConn.activeChannels.Load(msg.ChannelCode); ok { + channel := _channel.(*Channel) channel.Dispatch(msg) } else { dtxConn.globalChannel.Dispatch(msg) @@ -158,7 +161,7 @@ func (dtxConn *Connection) ForChannelRequest(messageDispatcher Dispatcher) *Chan identifier, _ := nskeyedarchiver.Unarchive(msg.Auxiliary.GetArguments()[1].([]byte)) //TODO: Setting the channel code here manually to -1 for making testmanagerd work. For some reason it requests the TestDriver proxy channel with code 1 but sends messages on -1. Should probably be fixed somehow channel := &Channel{channelCode: -1, channelName: identifier[0].(string), messageIdentifier: 1, connection: dtxConn, messageDispatcher: messageDispatcher, responseWaiters: map[int]chan Message{}, defragmenters: map[int]*FragmentDecoder{}, timeout: 5 * time.Second} - dtxConn.activeChannels[-1] = channel + dtxConn.activeChannels.Store(-1, channel) return channel } @@ -184,7 +187,7 @@ func (dtxConn *Connection) RequestChannelIdentifier(identifier string, messageDi } log.WithFields(log.Fields{"channel_id": identifier}).Debug("Channel open") channel := &Channel{channelCode: code, channelName: identifier, messageIdentifier: 1, connection: dtxConn, messageDispatcher: messageDispatcher, responseWaiters: map[int]chan Message{}, defragmenters: map[int]*FragmentDecoder{}, timeout: 5 * time.Second} - dtxConn.activeChannels[code] = channel + dtxConn.activeChannels.Store(code, channel) for _, opt := range opts { opt(channel) } diff --git a/ios/forward/forward.go b/ios/forward/forward.go index f5c6b8fc..a630af31 100644 --- a/ios/forward/forward.go +++ b/ios/forward/forward.go @@ -1,9 +1,11 @@ package forward import ( + "context" "fmt" "io" "net" + "sync" "github.com/danielpaulus/go-ios/ios" log "github.com/sirupsen/logrus" @@ -37,37 +39,67 @@ func connectionAccept(l net.Listener, deviceID int, phonePort uint16) { continue } log.WithFields(log.Fields{"conn": fmt.Sprintf("%#v", clientConn)}).Info("new client connected") - go startNewProxyConnection(clientConn, deviceID, phonePort) + go StartNewProxyConnection(context.TODO(), clientConn, deviceID, phonePort) } } -func startNewProxyConnection(clientConn net.Conn, deviceID int, phonePort uint16) { +func StartNewProxyConnection(ctx context.Context, clientConn net.Conn, deviceID int, phonePort uint16) error { usbmuxConn, err := ios.NewUsbMuxConnectionSimple() if err != nil { log.Errorf("could not connect to usbmuxd: %+v", err) clientConn.Close() - return + return fmt.Errorf("could not connect to usbmuxd: %v", err) } muxError := usbmuxConn.Connect(deviceID, phonePort) if muxError != nil { log.WithFields(log.Fields{"conn": fmt.Sprintf("%#v", clientConn), "err": muxError, "phonePort": phonePort}).Infof("could not connect to phone") clientConn.Close() - return + return fmt.Errorf("could not connect to port:%d on iOS: %v", phonePort, err) } log.WithFields(log.Fields{"conn": fmt.Sprintf("%#v", clientConn), "phonePort": phonePort}).Infof("Connected to port") deviceConn := usbmuxConn.ReleaseDeviceConnection() //proxyConn := iosproxy{clientConn, deviceConn} + ctx2, cancel := context.WithCancel(ctx) + var wg sync.WaitGroup + wg.Add(1) + + closed := false go func() { io.Copy(clientConn, deviceConn.Reader()) - clientConn.Close() - deviceConn.Close() + if ctx2.Err() == nil { + cancel() + clientConn.Close() + deviceConn.Close() + closed = true + } + + log.Errorf("forward: close clientConn <-- deviceConn") + wg.Done() }() + + wg.Add(1) go func() { io.Copy(deviceConn.Writer(), clientConn) - clientConn.Close() - deviceConn.Close() + if ctx2.Err() == nil { + cancel() + clientConn.Close() + deviceConn.Close() + closed = true + } + + log.Errorf("forward: close clientConn --> deviceConn") + wg.Done() }() + + <-ctx2.Done() + if !closed { + clientConn.Close() + deviceConn.Close() + } + + wg.Wait() + return nil } func (proxyConn *iosproxy) Close() { diff --git a/ios/house_arrest/house_arrest.go b/ios/house_arrest/house_arrest.go new file mode 100644 index 00000000..6ea340da --- /dev/null +++ b/ios/house_arrest/house_arrest.go @@ -0,0 +1,180 @@ +package house_arrest + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "strings" + + "github.com/danielpaulus/go-ios/ios/afc" + + "github.com/danielpaulus/go-ios/ios" + + "howett.net/plist" +) + +const serviceName = "com.apple.mobile.house_arrest" + +type Connection struct { + deviceConn ios.DeviceConnectionInterface + packageNumber uint64 +} + +func New(device ios.DeviceEntry, bundleID string) (*Connection, error) { + deviceConn, err := ios.ConnectToService(device, serviceName) + if err != nil { + return &Connection{}, err + } + err = vendContainer(deviceConn, bundleID) + if err != nil { + return &Connection{}, err + } + return &Connection{deviceConn: deviceConn}, nil +} + +func vendContainer(deviceConn ios.DeviceConnectionInterface, bundleID string) error { + plistCodec := ios.NewPlistCodec() + vendContainer := map[string]interface{}{"Command": "VendContainer", "Identifier": bundleID} + msg, err := plistCodec.Encode(vendContainer) + if err != nil { + return fmt.Errorf("VendContainer Encoding cannot fail unless the encoder is broken: %v", err) + } + err = deviceConn.Send(msg) + if err != nil { + return err + } + reader := deviceConn.Reader() + response, err := plistCodec.Decode(reader) + if err != nil { + return err + } + return checkResponse(response) +} + +func checkResponse(vendContainerResponseBytes []byte) error { + response, err := plistFromBytes(vendContainerResponseBytes) + if err != nil { + return err + } + if "Complete" == response.Status { + return nil + } + if response.Error != "" { + return errors.New(response.Error) + } + return errors.New("unknown error during vendcontainer") +} + +func plistFromBytes(plistBytes []byte) (vendContainerResponse, error) { + var vendResponse vendContainerResponse + decoder := plist.NewDecoder(bytes.NewReader(plistBytes)) + + err := decoder.Decode(&vendResponse) + if err != nil { + return vendResponse, err + } + return vendResponse, nil +} + +type vendContainerResponse struct { + Status string + Error string +} + +func (c Connection) Close() { + if c.deviceConn != nil { + c.deviceConn.Close() + } +} + +func (conn *Connection) SendFile(fileContents []byte, filePath string) error { + handle, err := conn.openFileForWriting(filePath) + if err != nil { + return err + } + err = conn.sendFileContents(fileContents, handle) + if err != nil { + return err + } + return conn.closeHandle(handle) +} + +func (conn *Connection) ListFiles(filePath string) ([]string, error) { + headerPayload := []byte(filePath) + headerLength := uint64(len(headerPayload)) + + this_length := afc.Afc_header_size + headerLength + header := afc.AfcPacketHeader{Magic: afc.Afc_magic, Packet_num: conn.packageNumber, Operation: afc.Afc_operation_read_dir, This_length: this_length, Entire_length: this_length} + conn.packageNumber++ + packet := afc.AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: make([]byte, 0)} + + response, err := conn.sendAfcPacketAndAwaitResponse(packet) + if err != nil { + return []string{}, err + } + fileList := string(response.Payload) + return strings.Split(fileList, string([]byte{0})), nil +} + +func (conn *Connection) openFileForWriting(filePath string) (byte, error) { + pathBytes := []byte(filePath) + headerLength := 8 + uint64(len(pathBytes)) + headerPayload := make([]byte, headerLength) + binary.LittleEndian.PutUint64(headerPayload, afc.Afc_Mode_WRONLY) + copy(headerPayload[8:], pathBytes) + this_length := afc.Afc_header_size + headerLength + header := afc.AfcPacketHeader{Magic: afc.Afc_magic, Packet_num: conn.packageNumber, Operation: afc.Afc_operation_file_open, This_length: this_length, Entire_length: this_length} + conn.packageNumber++ + packet := afc.AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: make([]byte, 0)} + + response, err := conn.sendAfcPacketAndAwaitResponse(packet) + if err != nil { + return 0, err + } + if response.Header.Operation != afc.Afc_operation_file_open_result { + return 0, fmt.Errorf("Unexpected afc response, expected %x received %x", afc.Afc_operation_status, response.Header.Operation) + } + return response.HeaderPayload[0], nil +} + +func (conn *Connection) sendAfcPacketAndAwaitResponse(packet afc.AfcPacket) (afc.AfcPacket, error) { + err := afc.Encode(packet, conn.deviceConn.Writer()) + if err != nil { + return afc.AfcPacket{}, err + } + return afc.Decode(conn.deviceConn.Reader()) +} + +func (conn *Connection) sendFileContents(fileContents []byte, handle byte) error { + headerPayload := make([]byte, 8) + headerPayload[0] = handle + header := afc.AfcPacketHeader{Magic: afc.Afc_magic, Packet_num: conn.packageNumber, Operation: afc.Afc_operation_file_write, This_length: 8 + afc.Afc_header_size, Entire_length: 8 + afc.Afc_header_size + uint64(len(fileContents))} + conn.packageNumber++ + packet := afc.AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: fileContents} + response, err := conn.sendAfcPacketAndAwaitResponse(packet) + if err != nil { + return err + } + if response.Header.Operation != afc.Afc_operation_status { + return fmt.Errorf("Unexpected afc response, expected %x received %x", afc.Afc_operation_status, response.Header.Operation) + } + return nil +} + +func (conn *Connection) closeHandle(handle byte) error { + headerPayload := make([]byte, 8) + headerPayload[0] = handle + this_length := 8 + afc.Afc_header_size + header := afc.AfcPacketHeader{Magic: afc.Afc_magic, Packet_num: conn.packageNumber, Operation: afc.Afc_operation_file_close, This_length: this_length, Entire_length: this_length} + conn.packageNumber++ + packet := afc.AfcPacket{Header: header, HeaderPayload: headerPayload, Payload: make([]byte, 0)} + response, err := conn.sendAfcPacketAndAwaitResponse(packet) + if err != nil { + return err + } + if response.Header.Operation != afc.Afc_operation_status { + return fmt.Errorf("Unexpected afc response, expected %x received %x", afc.Afc_operation_status, response.Header.Operation) + } + return nil +} diff --git a/ios/afc/house_arrest_test.go b/ios/house_arrest/house_arrest_test.go similarity index 76% rename from ios/afc/house_arrest_test.go rename to ios/house_arrest/house_arrest_test.go index 8b0b4c6b..4b1381f6 100644 --- a/ios/afc/house_arrest_test.go +++ b/ios/house_arrest/house_arrest_test.go @@ -1,4 +1,4 @@ -package afc_test +package house_arrest_test /* import ( @@ -13,7 +13,7 @@ import ( func TestIT(t *testing.T) { device := ios.ListDevices().DeviceList[0] - conn, err := house_arrest.NewWithHouseArrest(device.DeviceID, device.Properties.SerialNumber, "d.blaUITests.xctrunner") + conn, err := house_arrest.New(device.DeviceID, device.Properties.SerialNumber, "d.blaUITests.xctrunner") list, err := conn.ListFiles("tmp") log.Info(list) filePath := "tmp/test1003.txt" diff --git a/ios/instruments/processcontrol.go b/ios/instruments/processcontrol.go index 81f9859f..3b6d6abc 100644 --- a/ios/instruments/processcontrol.go +++ b/ios/instruments/processcontrol.go @@ -57,10 +57,11 @@ func (p ProcessControl) StartProcess(bundleID string, envVars map[string]interfa arguments, options) if err != nil { - log.WithFields(log.Fields{"channel_id": processControlChannelName, "error": err}).Info("failed starting process") + log.WithFields(log.Fields{"channel_id": processControlChannelName, "error": err}).Errorln("failed starting process: ", bundleID) + return 0, err } if msg.HasError() { - return 0, fmt.Errorf("Failed starting process: %s", msg.Payload[0]) + return 0, fmt.Errorf("Failed starting process: %s, msg:%v", bundleID, msg.Payload[0]) } if pid, ok := msg.Payload[0].(uint64); ok { log.WithFields(log.Fields{"channel_id": processControlChannelName, "pid": pid}).Info("Process started successfully") diff --git a/ios/testmanagerd/xcuitestrunner.go b/ios/testmanagerd/xcuitestrunner.go index 5d495780..a6039899 100644 --- a/ios/testmanagerd/xcuitestrunner.go +++ b/ios/testmanagerd/xcuitestrunner.go @@ -1,13 +1,14 @@ package testmanagerd import ( + "context" "fmt" + "github.com/danielpaulus/go-ios/ios/house_arrest" "path" "strings" "time" "github.com/danielpaulus/go-ios/ios" - "github.com/danielpaulus/go-ios/ios/afc" dtx "github.com/danielpaulus/go-ios/ios/dtx_codec" "github.com/danielpaulus/go-ios/ios/installationproxy" "github.com/danielpaulus/go-ios/ios/instruments" @@ -50,7 +51,10 @@ func (xdc XCTestManager_DaemonConnectionInterface) startExecutingTestPlanWithPro func (xdc XCTestManager_DaemonConnectionInterface) authorizeTestSessionWithProcessID(pid uint64) (bool, error) { rply, err := xdc.IDEDaemonProxy.MethodCall("_IDE_authorizeTestSessionWithProcessID:", pid) - + if err != nil { + log.Errorf("authorizeTestSessionWithProcessID failed: %v, err:%v", pid, err) + return false, err + } returnValue := rply.Payload[0] var val bool var ok bool @@ -63,11 +67,14 @@ func (xdc XCTestManager_DaemonConnectionInterface) authorizeTestSessionWithProce } func (xdc XCTestManager_DaemonConnectionInterface) initiateSessionWithIdentifierAndCaps(uuid uuid.UUID, caps nskeyedarchiver.XCTCapabilities) (nskeyedarchiver.XCTCapabilities, error) { - rply, err := xdc.IDEDaemonProxy.MethodCall("_IDE_initiateSessionWithIdentifier:capabilities:", nskeyedarchiver.NewNSUUID(uuid), caps) - - returnValue := rply.Payload[0] var val nskeyedarchiver.XCTCapabilities var ok bool + rply, err := xdc.IDEDaemonProxy.MethodCall("_IDE_initiateSessionWithIdentifier:capabilities:", nskeyedarchiver.NewNSUUID(uuid), caps) + if err != nil { + log.Errorf("initiateSessionWithIdentifierAndCaps failed: %v", err) + return val, err + } + returnValue := rply.Payload[0] if val, ok = returnValue.(nskeyedarchiver.XCTCapabilities); !ok { return val, fmt.Errorf("_IDE_initiateSessionWithIdentifier:capabilities: got wrong returnvalue: %s", rply.Payload) } @@ -76,11 +83,15 @@ func (xdc XCTestManager_DaemonConnectionInterface) initiateSessionWithIdentifier return val, err } func (xdc XCTestManager_DaemonConnectionInterface) initiateControlSessionWithCapabilities(caps nskeyedarchiver.XCTCapabilities) (nskeyedarchiver.XCTCapabilities, error) { - rply, err := xdc.IDEDaemonProxy.MethodCall("_IDE_initiateControlSessionWithCapabilities:", caps) - - returnValue := rply.Payload[0] var val nskeyedarchiver.XCTCapabilities var ok bool + rply, err := xdc.IDEDaemonProxy.MethodCall("_IDE_initiateControlSessionWithCapabilities:", caps) + if err != nil { + log.Errorf("initiateControlSessionWithCapabilities failed: %v", err) + return val, err + } + returnValue := rply.Payload[0] + if val, ok = returnValue.(nskeyedarchiver.XCTCapabilities); !ok { return val, fmt.Errorf("_IDE_initiateControlSessionWithCapabilities got wrong returnvalue: %s", rply.Payload) } @@ -91,16 +102,19 @@ func (xdc XCTestManager_DaemonConnectionInterface) initiateControlSessionWithCap func (xdc XCTestManager_DaemonConnectionInterface) initiateSessionWithIdentifier(sessionIdentifier uuid.UUID, protocolVersion uint64) (uint64, error) { log.WithFields(log.Fields{"channel_id": ideToDaemonProxyChannelName}).Debug("Launching init test Session") + var val uint64 + var ok bool rply, err := xdc.IDEDaemonProxy.MethodCall( "_IDE_initiateSessionWithIdentifier:forClient:atPath:protocolVersion:", nskeyedarchiver.NewNSUUID(sessionIdentifier), "thephonedoesntcarewhatisendhereitseems", "/Applications/Xcode.app", protocolVersion) - + if err != nil { + log.Errorf("initiateSessionWithIdentifier failed: %v", err) + return val, err + } returnValue := rply.Payload[0] - var val uint64 - var ok bool if val, ok = returnValue.(uint64); !ok { return 0, fmt.Errorf("initiateSessionWithIdentifier got wrong returnvalue: %s", rply.Payload) } @@ -252,13 +266,13 @@ func RunXCUITest(bundleID string, device ios.DeviceEntry) error { return err } xctestConfigFileName := info.targetAppBundleName + "UITests.xctest" - return RunXCUIWithBundleIds(bundleID, testRunnerBundleID, xctestConfigFileName, device, nil, nil) + return RunXCUIWithBundleIdsCtx(nil, bundleID, testRunnerBundleID, xctestConfigFileName, device, nil, nil) } var closeChan = make(chan interface{}) var closedChan = make(chan interface{}) -func runXUITestWithBundleIdsXcode12(bundleID string, testRunnerBundleID string, xctestConfigFileName string, +func runXUITestWithBundleIdsXcode12Ctx(ctx context.Context, bundleID string, testRunnerBundleID string, xctestConfigFileName string, device ios.DeviceEntry, conn *dtx.Connection, args []string, env []string) error { testSessionId, xctestConfigPath, testConfig, testInfo, err := setupXcuiTest(device, bundleID, testRunnerBundleID, xctestConfigFileName) if err != nil { @@ -312,7 +326,22 @@ func runXUITestWithBundleIdsXcode12(bundleID string, testRunnerBundleID string, err = ideDaemonProxy2.daemonConnection.startExecutingTestPlanWithProtocolVersion(ideInterfaceChannel, 36) if err != nil { log.Error(err) + return err } + + if ctx != nil { + select { + case <-ctx.Done(): + log.Infof("Killing WebDriverAgent with pid %d ...", pid) + err = pControl.KillProcess(pid) + if err != nil { + return err + } + log.Info("WDA killed with success") + } + return nil + } + <-closeChan log.Infof("Killing UITest with pid %d ...", pid) err = pControl.KillProcess(pid) @@ -326,7 +355,8 @@ func runXUITestWithBundleIdsXcode12(bundleID string, testRunnerBundleID string, } -func RunXCUIWithBundleIds( +func RunXCUIWithBundleIdsCtx( + ctx context.Context, bundleID string, testRunnerBundleID string, xctestConfigFileName string, @@ -341,14 +371,14 @@ func RunXCUIWithBundleIds( log.Debugf("%v", version) if version.LessThan(ios.IOS14()) { log.Infof("iOS version: %s detected, running with ios11 support", version) - return RunXCUIWithBundleIds11(bundleID, testRunnerBundleID, xctestConfigFileName, device, wdaargs, wdaenv) + return RunXCUIWithBundleIds11Ctx(ctx, bundleID, testRunnerBundleID, xctestConfigFileName, device, wdaargs, wdaenv) } conn, err := dtx.NewConnection(device, testmanagerdiOS14) if err != nil { return err } - return runXUITestWithBundleIdsXcode12(bundleID, testRunnerBundleID, xctestConfigFileName, device, conn, wdaargs, wdaenv) + return runXUITestWithBundleIdsXcode12Ctx(ctx, bundleID, testRunnerBundleID, xctestConfigFileName, device, conn, wdaargs, wdaenv) } @@ -436,7 +466,7 @@ func setupXcuiTest(device ios.DeviceEntry, bundleID string, testRunnerBundleID s } log.Debugf("app info found: %+v", info) - houseArrestService, err := afc.NewWithHouseArrest(device, testRunnerBundleID) + houseArrestService, err := house_arrest.New(device, testRunnerBundleID) defer houseArrestService.Close() if err != nil { return uuid.UUID{}, "", nskeyedarchiver.XCTestConfiguration{}, testInfo{}, err @@ -450,7 +480,7 @@ func setupXcuiTest(device ios.DeviceEntry, bundleID string, testRunnerBundleID s return testSessionID, testConfigPath, testConfig, info, nil } -func createTestConfigOnDevice(testSessionID uuid.UUID, info testInfo, houseArrestService *afc.Connection, xctestConfigFileName string) (string, nskeyedarchiver.XCTestConfiguration, error) { +func createTestConfigOnDevice(testSessionID uuid.UUID, info testInfo, houseArrestService *house_arrest.Connection, xctestConfigFileName string) (string, nskeyedarchiver.XCTestConfiguration, error) { relativeXcTestConfigPath := path.Join("tmp", testSessionID.String()+".xctestconfiguration") xctestConfigPath := path.Join(info.testRunnerHomePath, relativeXcTestConfigPath) diff --git a/ios/testmanagerd/xcuitestrunner_11.go b/ios/testmanagerd/xcuitestrunner_11.go index 23a7ffa0..9ef062ba 100644 --- a/ios/testmanagerd/xcuitestrunner_11.go +++ b/ios/testmanagerd/xcuitestrunner_11.go @@ -1,6 +1,7 @@ package testmanagerd import ( + "context" "github.com/danielpaulus/go-ios/ios" dtx "github.com/danielpaulus/go-ios/ios/dtx_codec" "github.com/danielpaulus/go-ios/ios/instruments" @@ -8,7 +9,8 @@ import ( "strings" ) -func RunXCUIWithBundleIds11( +func RunXCUIWithBundleIds11Ctx( + ctx context.Context, bundleID string, testRunnerBundleID string, xctestConfigFileName string, @@ -67,6 +69,18 @@ func RunXCUIWithBundleIds11( if err != nil { log.Error(err) } + if ctx != nil { + select { + case <-ctx.Done(): + log.Infof("Killing WebDriverAgent with pid %d ...", pid) + err = pControl.KillProcess(pid) + if err != nil { + return err + } + log.Info("WDA killed with success") + } + return nil + } log.Debugf("done starting test") <-closeChan log.Infof("Killing WebDriverAgent with pid %d ...", pid) diff --git a/ios/testmanagerd/xcuitestrunner_test.go b/ios/testmanagerd/xcuitestrunner_test.go index c394f93a..5148db58 100644 --- a/ios/testmanagerd/xcuitestrunner_test.go +++ b/ios/testmanagerd/xcuitestrunner_test.go @@ -4,6 +4,7 @@ package testmanagerd_test import ( + "context" "fmt" "github.com/danielpaulus/go-ios/ios" "github.com/danielpaulus/go-ios/ios/imagemounter" @@ -52,7 +53,7 @@ func TestXcuiTest(t *testing.T) { var wdaargs []string var wdaenv []string go func() { - err := testmanagerd.RunXCUIWithBundleIds(bundleID, testbundleID, xctestconfig, device, wdaargs, wdaenv) + err := testmanagerd.RunXCUIWithBundleIdsCtx(context.Background(), bundleID, testbundleID, xctestconfig, device, wdaargs, wdaenv) if err != nil { log.WithFields(log.Fields{"error": err}).Fatal("Failed running WDA") diff --git a/ios/zipconduit/plists.go b/ios/zipconduit/plists.go index 6571ce25..c9e99a04 100644 --- a/ios/zipconduit/plists.go +++ b/ios/zipconduit/plists.go @@ -59,17 +59,17 @@ func evaluateProgress(progressUpdate map[string]interface{}) (bool, int, string, return false, 0, "", fmt.Errorf("failed installing: '%s' errorDescription:'%s'", errorMessage, description) } + var percent int percentIntf, ok := installProgressDict["PercentComplete"] - if !ok { - return false, 0, "", fmt.Errorf("invalid installProgressDict, missing PercentComplete field:+%+v", progressUpdate) + if ok { + percent = int(percentIntf.(uint64)) } - percent := int(percentIntf.(uint64)) + var status string statusIntf, ok = installProgressDict["Status"] - if !ok { - return false, 0, "", fmt.Errorf("invalid installProgressDict, missing Status field:+%+v", progressUpdate) + if ok { + status = statusIntf.(string) } - status := statusIntf.(string) return false, percent, status, nil } diff --git a/main.go b/main.go index 41df982d..faaeee12 100644 --- a/main.go +++ b/main.go @@ -2,9 +2,12 @@ package main import ( "bytes" + "context" "encoding/json" "fmt" + "github.com/danielpaulus/go-ios/ios/afc" "io/ioutil" + "path" "path/filepath" "runtime/debug" "strings" @@ -92,6 +95,8 @@ Usage: ios runwda [--bundleid=] [--testrunnerbundleid=] [--xctestconfig=] [--arg=]... [--env=]... [options] ios ax [options] ios debug [options] [--stop-at-entry] + ios fsync (rm | tree | mkdir) --path= + ios fsync (pull | push) --srcPath= --dstPath= ios reboot [options] ios -h | --help ios --version | version [options] @@ -168,6 +173,8 @@ The commands work as following: > specify runtime args and env vars like --env ENV_1=something --env ENV_2=else and --arg ARG1 --arg ARG2 ios ax [options] Access accessibility inspector features. ios debug [--stop-at-entry] Start debug with lldb + ios fsync (rm | tree | mkdir) --path= Remove | treeview | mkdir in target path. + ios fsync (pull | push) --srcPath= --dstPath= Pull or Push file from srcPath to dstPath. ios reboot [options] Reboot the given device ios -h | --help Prints this screen. ios --version | version [options] Prints the version @@ -554,6 +561,56 @@ The commands work as following: return } + b, _ = arguments.Bool("fsync") + if b { + afcService, err := afc.New(device) + exitIfError("fsync: connect afc service failed", err) + b, _ = arguments.Bool("rm") + if b { + path, _ := arguments.String("--path") + err = afcService.Remove(path) + exitIfError("fsync: remove failed", err) + } + + b, _ = arguments.Bool("tree") + if b { + path, _ := arguments.String("--path") + err = afcService.TreeView(path, "", true) + exitIfError("fsync: tree view failed", err) + } + + b, _ = arguments.Bool("mkdir") + if b { + path, _ := arguments.String("--path") + err = afcService.MkDir(path) + exitIfError("fsync: mkdir failed", err) + } + + b, _ = arguments.Bool("pull") + if b { + sp, _ := arguments.String("--srcPath") + dp, _ := arguments.String("--dstPath") + if dp != "" { + ret, _ := ios.PathExists(dp) + if !ret { + err = os.MkdirAll(dp, os.ModePerm) + exitIfError("mkdir failed", err) + } + } + dp = path.Join(dp, filepath.Base(sp)) + err = afcService.Pull(sp, dp) + exitIfError("fsync: pull failed", err) + } + b, _ = arguments.Bool("push") + if b { + sp, _ := arguments.String("--srcPath") + dp, _ := arguments.String("--dstPath") + err = afcService.Push(sp, dp) + exitIfError("fsync: push failed", err) + } + afcService.Close() + return + } } func mobileGestaltCommand(device ios.DeviceEntry, arguments docopt.Opts) bool { @@ -629,7 +686,7 @@ func runWdaCommand(device ios.DeviceEntry, arguments docopt.Opts) bool { } log.WithFields(log.Fields{"bundleid": bundleID, "testbundleid": testbundleID, "xctestconfig": xctestconfig}).Info("Running wda") go func() { - err := testmanagerd.RunXCUIWithBundleIds(bundleID, testbundleID, xctestconfig, device, wdaargs, wdaenv) + err := testmanagerd.RunXCUIWithBundleIdsCtx(context.Background(), bundleID, testbundleID, xctestconfig, device, wdaargs, wdaenv) if err != nil { log.WithFields(log.Fields{"error": err}).Fatal("Failed running WDA")