├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── bin ├── .gitkeep ├── cfa ├── go-ios ├── gojq ├── iosif ├── vidstream └── wda ├── bridge_goios.go ├── bridge_interface.go ├── bridge_iosif.go ├── cfa.go ├── config.go ├── config.json.example ├── controlfloor.go ├── default.json ├── dev_info.go ├── device.go ├── device_tracker.go ├── go.mod ├── http_server.go ├── main.go ├── proc_backoff.go ├── proc_generic.go ├── python ├── configure_cfa.py ├── configure_vidstream.py ├── configure_wda.py ├── pbxproj └── requires.txt ├── repos ├── .gitkeep └── versionMarkers │ ├── CFAgent │ ├── WebDriverAgent │ ├── calculated_json │ ├── go-ios │ ├── iosif │ ├── ujsonin │ └── vidapp ├── sanity.go ├── shutdown.go ├── util └── signers.pl ├── video_app_stream.go ├── video_img_consumer.go ├── video_img_handler.go └── wda.go /.gitignore: -------------------------------------------------------------------------------- 1 | **~ 2 | \#** 3 | **_old 4 | **_off 5 | .DS_Store 6 | main 7 | go.sum 8 | python/deps_installed 9 | vidtest.xcarchive 10 | vidtest.ipa 11 | vidtest_clean.ipa 12 | vidtest_clean.xcarchive 13 | tmp 14 | muxed.json 15 | config.mk 16 | repos/WebDriverAgent/ 17 | repos/iosif 18 | repos/mod-pbxproj/ 19 | repos/ujsonin 20 | repos/vidapp 21 | repos/go-ios 22 | calculated.json 23 | wda.log 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 David Helkowski 2 | Free Use Anti-Corruption License 3 | https://faircoding.com/license 4 | 5 | ======================================================================= 6 | 7 | The content of this project is licensed for free conditional use by LICENSEE 8 | as defined below. The conditional aspect is detailed in THE RESTRICTION below. 9 | 10 | A DIRECT RESTRICTED ENTITY is defined to be any of the following: 11 | - Any company present on the current online list: https://faircoding.com/license/list 12 | - Accenture https://en.wikipedia.org/wiki/Accenture 13 | - Amazon https://en.wikipedia.org/wiki/Amazon_(company) 14 | - Apple 15 | - AptEdge https://aptedge.io 16 | - Avalara https://www.avalara.com 17 | - Baltimore Sun 18 | - BCG https://en.wikipedia.org/wiki/Boston_Consulting_Group 19 | - BrowserStack https://www.browserstack.com 20 | - The Canton Group https://cantongroup.com 21 | - Comcast 22 | - Cruise LLC https://en.wikipedia.org/wiki/Cruise_(autonomous_vehicle) 23 | - Disney 24 | - Ebay 25 | - Epic Games 26 | - EQT Partners https://en.wikipedia.org/wiki/EQT_Partners 27 | - Equifax 28 | - Experian 29 | - Extrahop https://www.extrahop.com 30 | - Facebook 31 | - FBI https://www.fbi.gov 32 | - Fox Entertainment Group 33 | - Google 34 | - Headspin https://www.headspin.io 35 | - IBM 36 | - Insight Global https://www.insightglobal.com 37 | - Jamf https://www.jamf.com 38 | - Kobiton https://www.kobiton.com 39 | - Logic 20/20 https://www.logic2020.com 40 | - Micro Focus https://en.wikipedia.org/wiki/Micro_Focus 41 | - MIT https://www.mit.edu 42 | - NBC 43 | - Nintendo https://en.wikipedia.org/wiki/Nintendo 44 | - Oracle Corporation https://en.wikipedia.org/wiki/Oracle_Corporation 45 | - Palantir 46 | - Perfecto https://www.perfecto.io 47 | - ProKarma https://pkglobal.com 48 | - Reddit https://reddit.com 49 | - Sauce Labs https://saucelabs.com 50 | - Sinclair https://en.wikipedia.org/wiki/Sinclair_Broadcast_Group 51 | - Smartbear https://smartbear.com 52 | - Sony Corporation https://en.wikipedia.org/wiki/Sony 53 | - Steam https://steampowered.com 54 | - Systems Alliance https://www.systemsalliance.com 55 | - SUSE https://en.wikipedia.org/wiki/SUSE 56 | - TEKsystems https://www.teksystems.com 57 | - Tesla https://www.tesla.com 58 | - TransUnion 59 | - T. Rowe Price https://en.wikipedia.org/wiki/T._Rowe_Price 60 | - UMB https://www.umaryland.edu 61 | - UMBC https://umbc.edu 62 | - UMCP https://www.umd.edu 63 | - Verizon 64 | - Wells Fargo 65 | - Ycombinator https://www.ycombinator.com 66 | - Zulily 67 | 68 | The links given are to clarify which entities exactly are meant. Should 69 | those links become invalid see the online list to find the latest updated 70 | website for the company/group. 71 | 72 | The online list may be updated at any time. If a company is added, causing 73 | a party to become a RESTRICTED ENTITY, then that party remains free to use 74 | versions of THE CONTENT created prior to becoming a RESTRICTED ENTITY under 75 | the license terms previous to the change. If this is a concern, it is suggested 76 | that LICENSEE fork THE CONTENT and opt to make use of the option to remove 77 | the online list described below in condition point 6. 78 | 79 | Changes made to THE CONTENT of any sort after the date of addition will be 80 | forbidden from use in that case, and those new changes are only licensed under 81 | the new license terms with the updated DIRECT RESTRICTED ENTITY list. 82 | 83 | A RESTRICTED ENTITY is defined to be any of the following: 84 | 1. A DIRECT RESTRICTED ENTITY 85 | 2. An owner of a RESTRICTED ENTITY 86 | 3. A subsidiary or any functional business unit of a RESTRICTED ENTITY 87 | 4. Any entity of which 50% or more of its annual revenue comes from work done 88 | for a RESTRICTED ENTITY 89 | 5. An entity that broke off from a RESTRICTED ENTITY ( such as a company 90 | splitting up into different legal entities ) 91 | 6. An international counterpart of a RESTRICTED ENTITY. 92 | 7. An employee of a RESTRICTED ENTITY 93 | 8. A contractor delivering work to a RESTRICTED ENTITY at more than 20hrs per week. 94 | 95 | A PARTIALLY RESTRICTED ENTITY is defined to be any of the following: 96 | 1. A contractor delivering work to a RESTRICTED ENTITY at less than 20hrs per week. 97 | 2. A person or legal entity acting under command, request, or legal contract 98 | by/with a RESTRICTED ENTITY. 99 | 100 | Should a PARTIALLY RESTRICTED ENTITY fall under any of the points defining them 101 | as A RESTRICTED ENTITY, they are not a PARTIALLY RESTRICTED ENTITY. 102 | 103 | A PARTIALLY RESTRICTED ENTITY shall be considered a RESTRICTED ENTITY to the extent 104 | that their usage of THE CONTENT is related to commands, requests, or legal contract 105 | with a RESTRICTED ENTITY. That is, A PARTIALLY RESTRICTED ENTITY cannot utilize 106 | THE CONTENT in any way in relation to a RESTRICTED ENTITY. 107 | 108 | A PARTIALLY RESTRICTED ENTITY remains able to be a LICENSEE of the content, to the 109 | extent that all usage and access to THE CONTENT remains within the license terms. 110 | That is, a PARTIALLY RESTRICTED ENTITY may utilize THE CONTENT in work related to 111 | other LICENSEE. They remain bound by THE RESTRICTION, prevented from enabling use 112 | of THE CONTENT by any RESTRICTED ENTITY. 113 | 114 | LICENSEE refers to any party ( person or legal entity ) who/that is not a 115 | RESTRICTED ENTITY and accepts this license by making use of THE CONTENT or 116 | utilizing any of the permissions provided by the license. 117 | 118 | THE CONTENT refers to the content in full, any portion of it, and anything 119 | derived from it. 120 | 121 | LICENSEE agrees not to provide THE CONTENT to any RESTRICTED ENTITY, nor to 122 | allow THE CONTENT to be used in any way by any RESTRICTED ENTITY. 123 | 124 | THE RESTRICTION is defined to be this license in full, specifically to 125 | refer to the way the license restricts use of THE CONTENT in any way by 126 | any RESTRICTED ENTITY. 127 | 128 | THE RESTRICTION shall apply to all projects with Copyright 129 | ownership by any RESTRICTED ENTITY. This shall remain true even if the 130 | project contributions themselves are done by a LICENSEE. LICENSEE 131 | are forbidden from contributing THE CONTENT to a project Copyrighted by 132 | any RESTRICTED ENTITY. 133 | 134 | Should any portion of this license be found to be unenforceable, the remaining 135 | portion of the license shall continue to be in effect, preserving the intent. 136 | The intent is to prevent any DIRECT RESTRICTED ENTITY from benefiting from 137 | THE CONTENT in any way. 138 | 139 | Should it be found or enacted into law that using such a restriction list is 140 | illegal as a whole, then the entire license shall dissolve and zero permissions 141 | given to anyone by the broken license. 142 | 143 | ======================================================================= 144 | 145 | Permission is hereby granted, free of charge, to LICENSEE, to conditionally 146 | use, modify, publish, distribute, or sell copies of THE CONTENT. 147 | 148 | These permissions are granted with the following conditions: 149 | 150 | 1. THE CONTENT may not be distributed or sold to any RESTRICTED ENTITY. 151 | 152 | 2. Access to a service containing or utilizing THE CONTENT may not be 153 | given, sold, or licensed to any RESTRICTED ENTITY. 154 | 155 | 3. Any service containing or utilizing THE CONTENT shall be considered a 156 | derivative of this license and therefore by bound by THE RESTRICTION. 157 | This includes use of THE CONTENT by way of dynamic libraries, indirect calls, 158 | scripting, virtualization, containerization, or instantiation as a remote 159 | callable service. Such restricted software must be prevented from being 160 | used by any RESTRICTED ENTITY. 161 | 162 | 4. THE CONTENT cannot be sold to any RESTRICTED ENTITY. 163 | 164 | 5. THE CONTENT may not be published to or distributed through a system 165 | owned or controlled by any RESTRICTED ENTITY. 166 | 167 | 6. Any derivatives of THE CONTENT must be licensed under this same license. 168 | By same license this means the contents of this LICENSE file in its 169 | entirety. The license "duplicate" may have the online restricted list removed, 170 | effectively fixing the DIRECT RESTRICTED ENTITY from that point onward for 171 | that derivative. At the moment in time of that removal, the online list must 172 | be checked, and any new DIRECT RESTRICTED ENTITY present in the online list 173 | added to the new updated fixed list in the license file. Besides that single 174 | permitted modification, the LICENSE may not be modified in any other way. 175 | 176 | 7. Any derivatives of THE CONTENT must be publicly released within 2 months 177 | of the change or discarded. 178 | 179 | 8. The above copyright notice and this LICENSE file must be included in 180 | all copies and derivative forms of THE CONTENT. 181 | 182 | 9. LICENSEE understands and agrees that this content is provided "as is", 183 | without warranty of any kind, express or implied, including but not limited 184 | to the warranties of merchantability, fitness for a particular purpose and 185 | noninfringement. In no event shall the authors or copyright holders be liable 186 | for any claim, damages or other liability, whether in an action of contract, 187 | tort or otherwise, arising from, out of or in connection with THE CONTENT or 188 | the use or other dealings of it. 189 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | -include config.mk 2 | 3 | TARGET = main 4 | 5 | all: $(TARGET) repos/vidapp/versionMarker bin/go-ios 6 | 7 | bin/gojq: repos/ujsonin/versionMarker 8 | make -C repos/ujsonin gojq && touch bin/gojq 9 | 10 | bin/go-ios: repos/go-ios/versionMarker 11 | cd repos/go-ios && go build . 12 | touch bin/go-ios 13 | 14 | config.mk: config.json bin/gojq 15 | @rm -rf config.mk 16 | @./bin/gojq makevars -prefix config -file config.json -defaults default.json -outfile config.mk 17 | 18 | provider_sources := $(wildcard *.go) 19 | 20 | $(TARGET): config.mk $(provider_sources) go.mod 21 | @if [ "$(config_jsonfail)" == "1" ]; then\ 22 | echo $(config_jsonerr) ;\ 23 | exit 1;\ 24 | fi 25 | go build -o $(TARGET) -tags macos . 26 | 27 | go.sum: 28 | go get 29 | go get . 30 | 31 | clean: 32 | $(RM) $(TARGET) 33 | 34 | wdaclean: 35 | $(RM) -rf repos/WebDriverAgent/build 36 | 37 | cfaclean: 38 | $(RM) -rf repos/CFAgent/build 39 | 40 | repos/WebDriverAgent/versionMarker: repos/WebDriverAgent repos/versionMarkers/WebDriverAgent 41 | cd repos/WebDriverAgent && git pull 42 | touch repos/WebDriverAgent/versionMarker 43 | 44 | repos/CFAgent/versionMarker: repos/CFAgent repos/versionMarkers/CFAgent 45 | cd repos/CFAgent && git pull 46 | touch repos/CFAgent/versionMarker 47 | 48 | repos/WebDriverAgent: 49 | git clone $(config_repos_wda) repos/WebDriverAgent 50 | 51 | repos/CFAgent: 52 | git clone $(config_repos_cfa) repos/CFAgent 53 | 54 | repos/ujsonin/versionMarker: repos/ujsonin repos/versionMarkers/ujsonin 55 | cd repos/ujsonin && git pull 56 | touch repos/ujsonin/versionMarker 57 | 58 | repos/ujsonin: 59 | git clone https://github.com/nanoscopic/ujsonin.git repos/ujsonin 60 | touch repos/ujsonin/versionMarker 61 | 62 | bin/iosif: repos/iosif/versionMarker 63 | make -C repos/iosif 64 | touch bin/iosif 65 | 66 | repos/iosif/versionMarker: repos/iosif repos/versionMarkers/iosif 67 | cd repos/iosif && git pull 68 | touch repos/iosif/versionMarker 69 | 70 | repos/iosif: 71 | git clone $(config_repos_iosif) repos/iosif 72 | 73 | repos/vidapp/versionMarker: repos/vidapp repos/versionMarkers/vidapp 74 | cd repos/vidapp && git pull 75 | touch repos/vidapp/versionMarker 76 | 77 | repos/vidapp: 78 | git clone $(config_repos_vidapp) repos/vidapp 79 | 80 | repos/go-ios/versionMarker: repos/go-ios repos/versionMarkers/go-ios 81 | cd repos/go-ios && git pull 82 | touch repos/go-ios/versionMarker 83 | 84 | repos/go-ios: 85 | git clone $(config_repos_goios) repos/go-ios 86 | 87 | vidstream_unsigned.xcarchive: 88 | xcodebuild -project repos/vidapp/vidstream/vidstream.xcodeproj -scheme vidstream archive -archivePath ./vidstream.xcarchive CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO CODE_SIGNING_ALLOWED=NO 89 | 90 | vidstream.xcarchive: repos/vidapp 91 | @./bin/gojq overlay -file1 default.json -file2 config.json -json > muxed.json 92 | ./python/configure_vidstream.py muxed.json 93 | xcodebuild -project repos/vidapp/vidstream/vidstream.xcodeproj -scheme vidstream archive -archivePath ./vidstream.xcarchive 94 | 95 | vidstream.ipa: vidstream.xcarchive repos/vidapp 96 | plutil -replace teamID -string $(config_vidstream_devTeamOu) ./repos/vidapp/vidstream/ExportOptions.plist 97 | @if [ -e vidstream.ipa ]; then rm vidstream.ipa; fi 98 | xcodebuild -exportArchive -archivePath ./vidstream.xcarchive -exportOptionsPlist ./repos/vidapp/vidstream/ExportOptions.plist -exportPath vidstream.ipa 99 | 100 | vidstream.ipa/vidstream.ipa_x: vidstream.ipa 101 | mkdir vidstream.ipa/vidstream.ipa_x 102 | unzip vidstream.ipa/vidstream.ipa -d vidstream.ipa/vidstream.ipa_x 103 | find vidstream.ipa/vidstream.ipa_x | grep provision | xargs rm 104 | find vidstream.ipa/vidstream.ipa_x | grep _CodeSignature$$ | xargs rm -rf 105 | 106 | vidstream_clean.ipa: vidstream.ipa/vidstream.ipa_x 107 | cd vidstream.ipa/vidstream.ipa_x && zip -r ../../vidstream_clean.ipa Payload 108 | 109 | installvidapp: vidstream.xcarchive 110 | ios-deploy -b vidstream.xcarchive/Products/Applications/vidstream.app 111 | 112 | vidstream_unsigned.ipa: 113 | @if [ -e tmp ]; then rm -rf tmp; fi 114 | mkdir tmp 115 | mkdir tmp/Payload 116 | ln -s ../../vidstream.xcarchive/Products/Applications/vidstream.app tmp/Payload/vidstream.app 117 | cd tmp && zip -r ../vidstream.ipa Payload 118 | 119 | clonewda: repos/WebDriverAgent 120 | 121 | clonecfa: repos/CFAgent 122 | 123 | wda: repos/WebDriverAgent/build 124 | 125 | cfa: repos/CFAgent/build 126 | 127 | vidapp: repos/vidapp 128 | 129 | python/deps_installed: repos/mod-pbxproj 130 | pip3 install -r ./python/requires.txt 131 | touch python/deps_installed 132 | 133 | repos/WebDriverAgent/build: repos/WebDriverAgent/versionMarker repos/mod-pbxproj config.json python/deps_installed bin/gojq 134 | @if [ -e repos/WebDriverAgent/build ]; then rm -rf repos/WebDriverAgent/build; fi; 135 | mkdir repos/WebDriverAgent/build 136 | @./bin/gojq overlay -file1 default.json -file2 config.json -json > muxed.json 137 | ./python/configure_wda.py muxed.json 138 | cd repos/WebDriverAgent && xcodebuild -scheme WebDriverAgentRunner -allowProvisioningUpdates -destination generic/platform=iOS -derivedDataPath "./build" build-for-testing 139 | 140 | repos/CFAgent/build: repos/CFAgent/versionMarker repos/mod-pbxproj config.json python/deps_installed bin/gojq 141 | @if [ -e repos/CFAgent/build ]; then rm -rf repos/CFAgent/build; fi; 142 | mkdir repos/CFAgent/build 143 | @./bin/gojq overlay -file1 default.json -file2 config.json -json > muxed.json 144 | ./python/configure_cfa.py muxed.json 145 | cd repos/CFAgent && xcodebuild -scheme CFAgent -allowProvisioningUpdates -destination generic/platform=iOS -derivedDataPath "./build" build-for-testing 146 | 147 | repos/mod-pbxproj: 148 | git clone $(config_repos_pbxproj) repos/mod-pbxproj 149 | 150 | usetidevice: calculated.json 151 | 152 | calculated.json: bin/gojq repos/versionMarkers/calculated_json 153 | $(eval TIDEVICE_PKGS_PATH := $(shell pip3 show tidevice | grep Location | cut -c 11-)) 154 | $(eval TIDEVICE_BIN_PATH := $(shell pip3 show -f tidevice | grep bin/tidevice)) 155 | @./bin/gojq set -file calculated.json -path tidevice -val $(TIDEVICE_PKGS_PATH)/$(TIDEVICE_BIN_PATH) 156 | 157 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Provider 2 | Provider connects iOS devices to ControlFloor. This sets up video streaming from iOS devices to the browser, 3 | and also enables the devices to be controlled remotely. 4 | 5 | # Basic Install Instructions 6 | 7 | ## Clone repos 8 | 1. `git clone https://github.com/nanoscopic/ios_remote_provider.git` 9 | 1. `git clone https://github.com/nanoscopic/controlfloor.git` 10 | 11 | ## Build ControlFloor 12 | 1. `cd controlfloor` 13 | 1. Copy example config: `cp config.json.example config.json` 14 | 1. Edit `config.json` as desired 15 | 1. `make` 16 | 1. `./main run` 17 | 18 | Open `https://yourip:8080` to see if ControlFloor is running 19 | 20 | ## Build iOS Remote Provider and CFAgent 21 | 1. `cd ios_remote_provider` 22 | 1. Copy example config: `cp config.json.example config.json` 23 | 1. Edit `config.json` to add your Apple developer details 24 | 1. `make` 25 | 1. `security unlock-keychain login.keychain` # to make sure developer details are there for xcode build 26 | 1. `make cfa` 27 | 28 | ## Register Provider 29 | 1. `./main register` 30 | 1. Press [enter] to register using the default password 31 | 32 | ## Build and setup CF Vidstream App 33 | 1. `cd repos/vidapp` 34 | 1. Open the xcode project and install CF Vidstream on the device 35 | 36 | ## Start CF Vidstream App Manually 37 | 1. Open the app 38 | 1. Click "Broadcast Selector" 39 | 1. Click "Start Recording" 40 | 41 | ## Start Provider 42 | 1. `cd ios_remote_provider` 43 | 1. `./main run` 44 | 45 | ## Automatically starting CF Vidstream App 46 | 1. Figure out your device id 47 | A. `./bin/iosif list` 48 | 1. Figure out your device UI width/height 49 | A. `./main winsize` 50 | B. -or- `./main winsize -id [your device id]` 51 | C. Observe "Width" and "Height" displayed 52 | D. Ctrl-C to stop 53 | 1. Add device specific config block to `config.json`: 54 | ``` 55 | { 56 | ... 57 | devices:[ 58 | { 59 | udid:"[your device id]" 60 | uiWidth:[your device width] 61 | uiHeight:[your device height] 62 | } 63 | ] 64 | } 65 | ``` 66 | 1. That's it. The video app will be started automatically when the provider is started. 67 | 68 | ## Using tidevice instead of go-ios 69 | 70 | You may wish to use tidevice instead of go-ios to start CFA. Do the following to get it setup: 71 | 72 | 1. Reconsider using tidevice and don't follow these steps 73 | 74 | 1. Install tidevice. `pip3 install tidevice` 75 | 76 | 1. Add a WDA start method to your `config.json`: 77 | ``` 78 | { 79 | ... 80 | wda:{ 81 | ... 82 | startMethod: "tidevice" 83 | } 84 | } 85 | ``` 86 | 87 | 1. Run `make usetidevice` to auto-generate the `calculated.json` file containing the location of tidevice installed on your system. 88 | 89 | 1. Start provider normally; tidevice will be used. 90 | -------------------------------------------------------------------------------- /bin/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dryark/ios_remote_provider/8fe86caf15dcdc96cf7bf8f7446cd6344680fcba/bin/.gitkeep -------------------------------------------------------------------------------- /bin/cfa: -------------------------------------------------------------------------------- 1 | ../repos/CFAgent/build/Build/Products/ -------------------------------------------------------------------------------- /bin/go-ios: -------------------------------------------------------------------------------- 1 | ../repos/go-ios/go-ios -------------------------------------------------------------------------------- /bin/gojq: -------------------------------------------------------------------------------- 1 | ../repos/ujsonin/gojq -------------------------------------------------------------------------------- /bin/iosif: -------------------------------------------------------------------------------- 1 | ../repos/iosif/iosif -------------------------------------------------------------------------------- /bin/vidstream: -------------------------------------------------------------------------------- 1 | ../vidstream.xcarchive/Products/Applications -------------------------------------------------------------------------------- /bin/wda: -------------------------------------------------------------------------------- 1 | ../repos/WebDriverAgent/build/Build/Products/ -------------------------------------------------------------------------------- /bridge_interface.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | uj "github.com/nanoscopic/ujsonin/v2/mod" 6 | ) 7 | 8 | type TunPair struct { 9 | from int 10 | to int 11 | } 12 | 13 | func (self TunPair) String() string { 14 | return fmt.Sprintf("%d:%d",self.from,self.to) 15 | } 16 | 17 | type Screenshot struct { 18 | format string 19 | data []byte 20 | } 21 | 22 | type BridgeDevInfo struct { 23 | udid string 24 | } 25 | 26 | // detect( onDevConnect func( bridge CliBridge ) ) 27 | 28 | type BridgeRoot interface { 29 | //OnConnect( dev BridgeDev ) 30 | //OnDisconnect( dev BridgeDev ) 31 | list() []BridgeDevInfo 32 | GetDevs(*Config) []string 33 | } 34 | 35 | type iProc struct { 36 | pid int32 37 | name string 38 | } 39 | 40 | type BridgeDev interface { 41 | getUdid() string 42 | tunnel( pairs []TunPair, onready func() ) 43 | info( names []string ) map[string]string 44 | gestalt( names []string ) map[string]string 45 | gestaltnode( names []string ) map[string]uj.JNode 46 | ps() []iProc 47 | screenshot() Screenshot 48 | cfa( onStart func(), onStop func(interface{}) ) 49 | wda( onStart func(), onStop func(interface{}) ) 50 | destroy() 51 | setProcTracker( procTracker ProcTracker ) 52 | NewBackupVideo( port int, onStop func( interface{} ) ) BackupVideo 53 | GetPid( appname string ) uint64 54 | AppInfo( bundleId string ) uj.JNode 55 | InstallApp( appPath string ) bool 56 | LaunchApp( bundleId string ) bool 57 | NewSyslogMonitor( handleLogItem func( msg string, app string ) ) 58 | Kill( pid uint64 ) 59 | KillBid( bid string ) 60 | Launch( bid string ) 61 | SetConfig( devConfig *CDevice ) 62 | SetDevice( device *Device ) 63 | SetCustom( name string, val interface{} ) 64 | } 65 | 66 | type BackupVideo interface { 67 | GetFrame() []byte 68 | } -------------------------------------------------------------------------------- /bridge_iosif.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "bufio" 6 | "fmt" 7 | //"io/ioutil" 8 | "image/png" 9 | "image/jpeg" 10 | uj "github.com/nanoscopic/ujsonin/v2/mod" 11 | log "github.com/sirupsen/logrus" 12 | nr "github.com/nfnt/resize" 13 | "net/http" 14 | "os" 15 | "os/exec" 16 | "strings" 17 | "strconv" 18 | "time" 19 | //"go.nanomsg.org/mangos/v3" 20 | //nanoReq "go.nanomsg.org/mangos/v3/protocol/req" 21 | ) 22 | 23 | type IIFBridge struct { 24 | onConnect func( dev BridgeDev ) ProcTracker 25 | onDisconnect func( dev BridgeDev ) 26 | cli string 27 | devs map[string]*IIFDev 28 | procTracker ProcTracker 29 | config *Config 30 | } 31 | 32 | type IIFDev struct { 33 | bridge *IIFBridge 34 | udid string 35 | name string 36 | procTracker ProcTracker 37 | config *CDevice 38 | device *Device 39 | } 40 | 41 | // IosIF bridge 42 | func NewIIFBridge( config *Config, OnConnect func( dev BridgeDev ) (ProcTracker), OnDisconnect func( dev BridgeDev ), iosIfPath string, procTracker ProcTracker, detect bool ) ( BridgeRoot ) { 43 | self := &IIFBridge{ 44 | onConnect: OnConnect, 45 | onDisconnect: OnDisconnect, 46 | cli: iosIfPath, 47 | devs: make( map[string]*IIFDev ), 48 | procTracker: procTracker, 49 | config: config, 50 | } 51 | if detect { self.startDetect() } 52 | return self 53 | } 54 | 55 | func (self *IIFDev) getUdid() string { 56 | return self.udid 57 | } 58 | 59 | func (self *IIFBridge) startDetect() { 60 | o := ProcOptions{ 61 | procName: "device_trigger", 62 | binary: self.cli, 63 | args: []string{ "detectloop" }, 64 | stdoutHandler: func( line string, plog *log.Entry ) { 65 | }, 66 | stderrHandler: func( line string, plog *log.Entry ) { 67 | if strings.HasPrefix( line, "{" ) { 68 | root, _ := uj.Parse( []byte(line) ) 69 | evType := root.Get("type").String() 70 | udid := root.Get("udid").String() 71 | if evType == "connect" { 72 | name := root.Get("name").String() 73 | self.OnConnect( udid, name, plog ) 74 | } else if evType == "disconnect" { 75 | self.OnDisconnect( udid, plog ) 76 | } 77 | } 78 | }, 79 | onStop: func( interface{} ) { 80 | log.Println("device trigger stopped") 81 | }, 82 | } 83 | proc_generic( self.procTracker, nil, &o ) 84 | } 85 | 86 | func (self *IIFBridge) list() []BridgeDevInfo { 87 | infos := []BridgeDevInfo{} 88 | for _,dev := range self.devs { 89 | infos = append( infos, BridgeDevInfo{ udid: dev.udid } ) 90 | } 91 | return infos 92 | } 93 | 94 | func (self *IIFBridge) OnConnect( udid string, name string, plog *log.Entry ) { 95 | dev := NewIIFDev( self, udid, name, nil ) 96 | self.devs[ udid ] = dev 97 | 98 | devConfig, hasDevConfig := self.config.devs[ udid ] 99 | if hasDevConfig { 100 | dev.config = &devConfig 101 | } 102 | 103 | dev.procTracker = self.onConnect( dev ) 104 | } 105 | 106 | func (self *IIFBridge) OnDisconnect( udid string, plog *log.Entry ) { 107 | dev := self.devs[ udid ] 108 | dev.destroy() 109 | self.onDisconnect( dev ) 110 | delete( self.devs, udid ) 111 | } 112 | 113 | func (self *IIFBridge) destroy() { 114 | for _,dev := range self.devs { 115 | dev.destroy() 116 | } 117 | // close self processes 118 | } 119 | 120 | func NewIIFDev( bridge *IIFBridge, udid string, name string, device *Device ) (*IIFDev) { 121 | log.WithFields( log.Fields{ 122 | "type": "iifdev_create", 123 | "udid": censorUuid( udid ), 124 | } ).Debug( "Creating IIFDev" ) 125 | 126 | var procTracker ProcTracker = nil 127 | return &IIFDev{ 128 | bridge: bridge, 129 | name: name, 130 | udid: udid, 131 | procTracker: procTracker, 132 | device: device, 133 | } 134 | } 135 | 136 | func (self *IIFDev) setProcTracker( procTracker ProcTracker ) { 137 | self.procTracker = procTracker 138 | } 139 | 140 | //func (self *IIFDev) tunnel( pairs []TunPair, onready func() ) { 141 | // self.tunnelIosif( pairs, onready ) 142 | //} 143 | 144 | func (self *IIFDev) tunnelIproxy( pairs []TunPair, onready func() ) { 145 | tunName := "tunnel" 146 | specs := []string{} 147 | for _,pair := range pairs { 148 | tunName = fmt.Sprintf( "%s_%d->%d", tunName, pair.from, pair.to ) 149 | specs = append( specs, fmt.Sprintf("%d:%d",pair.from,pair.to) ) 150 | } 151 | 152 | args := []string { 153 | "-u", self.udid, 154 | } 155 | args = append( args, specs... ) 156 | fmt.Printf("Starting %s with %s\n", "/usr/local/bin/iproxy", args ) 157 | 158 | o := ProcOptions{ 159 | procName: tunName, 160 | binary: "/usr/local/bin/iproxy", 161 | args: args, 162 | stdoutHandler: func( line string, plog *log.Entry ) { 163 | fmt.Printf( "tunnel:%s\n", line ) 164 | if strings.Contains( line, "waiting" ) { 165 | if onready != nil { 166 | //onready() 167 | } 168 | } 169 | }, 170 | stderrHandler: func( line string, plog *log.Entry ) { 171 | fmt.Printf( "tunnel err:%s\n", line ) 172 | 173 | }, 174 | onStop: func( interface{} ) { 175 | log.Printf("%s stopped\n", tunName) 176 | }, 177 | } 178 | proc_generic( self.procTracker, nil, &o ) 179 | time.Sleep( time.Second * 2 ) 180 | onready() 181 | } 182 | 183 | func (self *IIFDev) tunnelGoIos( pairs []TunPair, onready func() ) { 184 | count := len( pairs ) 185 | sofar := 0 186 | done := make( chan bool ) 187 | for _,pair := range( pairs ) { 188 | self.tunnelOne( pair, func() { 189 | sofar++ 190 | if sofar == count { 191 | done <- true 192 | } 193 | } ) 194 | } 195 | <- done 196 | onready() 197 | } 198 | 199 | func (self *IIFDev) tunnelOne( pair TunPair, onready func() ) { 200 | tunName := "tunnel" 201 | specs := []string{} 202 | 203 | tunName = fmt.Sprintf( "%s_%d->%d", tunName, pair.from, pair.to ) 204 | specs = append( specs, fmt.Sprintf("%d",pair.from) ) 205 | specs = append( specs, fmt.Sprintf("%d",pair.to) ) 206 | 207 | args := []string { 208 | "forward", 209 | "--udid", self.udid, 210 | } 211 | args = append( args, specs... ) 212 | fmt.Printf("Starting %s with %s\n", self.bridge.cli, args ) 213 | 214 | o := ProcOptions{ 215 | procName: tunName, 216 | binary: "bin/go-ios", 217 | args: args, 218 | stdoutHandler: func( line string, plog *log.Entry ) { 219 | fmt.Println( "tunnel:%s", line ) 220 | }, 221 | stderrHandler: func( line string, plog *log.Entry ) { 222 | 223 | //fmt.Println( "tunnel:%s", line ) 224 | if strings.Contains( line, "Start" ) { 225 | if onready != nil { 226 | onready() 227 | } 228 | fmt.Printf( "tunnel start:%s\n", line ) 229 | } else { 230 | fmt.Printf( "tunnel err:%s\n", line ) 231 | } 232 | }, 233 | onStop: func( interface{} ) { 234 | log.Printf("%s stopped\n", tunName) 235 | }, 236 | } 237 | proc_generic( self.procTracker, nil, &o ) 238 | } 239 | 240 | func (self *IIFDev) tunnel( pairs []TunPair, onready func() ) { 241 | tunName := "tunnel" 242 | specs := []string{} 243 | for _,pair := range pairs { 244 | from := pair.from 245 | to := pair.to 246 | 247 | tunName = fmt.Sprintf( "%s_%d->%d", tunName, from, to ) 248 | //specs = append( specs, fmt.Sprintf("%d:%d",from,to) ) 249 | specs = append( specs, strconv.Itoa( from ) + ":" + strconv.Itoa( to ) ) 250 | } 251 | 252 | args := []string { 253 | "tunnel", 254 | "-id", self.udid, 255 | } 256 | args = append( args, specs... ) 257 | fmt.Printf("Starting %s with %s\n", self.bridge.cli, args ) 258 | 259 | o := ProcOptions{ 260 | procName: tunName, 261 | binary: self.bridge.cli, 262 | args: args, 263 | stdoutHandler: func( line string, plog *log.Entry ) { 264 | //fmt.Printf( "tunnel:%s\n", line ) 265 | if strings.Contains( line, "Ready" ) { 266 | if onready != nil { 267 | onready() 268 | } 269 | } 270 | }, 271 | stderrHandler: func( line string, plog *log.Entry ) { 272 | //fmt.Printf( "tunnel err:%s\n", line ) 273 | }, 274 | onStop: func( interface{} ) { 275 | log.Printf("%s stopped\n", tunName) 276 | }, 277 | } 278 | proc_generic( self.procTracker, nil, &o ) 279 | } 280 | 281 | func (self *IIFBridge) GetDevs( config *Config ) []string { 282 | json, _ := exec.Command( config.iosIfPath, 283 | []string{ "list", "-json" }... ).Output() 284 | root, _ := uj.Parse( []byte( "[" + string(json) + "]" ) ) 285 | res := []string{} 286 | root.ForEach( func( dev uj.JNode ) { 287 | res = append( res, dev.Get("udid").String() ) 288 | } ) 289 | return res 290 | } 291 | 292 | func (self *IIFDev) GetPid( appname string ) uint64 { 293 | json, err := exec.Command( self.bridge.cli, 294 | []string{ 295 | "ps", 296 | "-raw", 297 | "-appname", appname, 298 | }... ).Output() 299 | 300 | if err != nil { 301 | return 0 302 | } 303 | 304 | jsonS := string( json ) 305 | jsonS = strings.ReplaceAll( jsonS, "i16.", "" ) 306 | jsonS = strings.ReplaceAll( jsonS, "i32.", "" ) 307 | root, _ := uj.Parse( []byte( jsonS ) ) 308 | pidNode := root.Get("pid") 309 | if pidNode == nil { return 0 } 310 | return uint64( pidNode.Int() ) 311 | } 312 | 313 | func (self *IIFDev) Kill( pid uint64 ) { 314 | } 315 | 316 | func (self *IIFDev) KillBid( bid string ) { 317 | } 318 | 319 | func (self *IIFDev) Launch( bid string ) { 320 | } 321 | 322 | func (self *IIFDev) AppInfo( bundleId string ) uj.JNode { 323 | json, err := exec.Command( self.bridge.cli, 324 | []string{ 325 | "listapps", 326 | "-bi", bundleId, 327 | }... ).Output() 328 | 329 | if err != nil { return nil } 330 | 331 | root, _ := uj.Parse( json ) 332 | return root 333 | } 334 | 335 | func (self *IIFDev) InstallApp( appPath string ) bool { 336 | status, _ := exec.Command( self.bridge.cli, 337 | []string{ 338 | "install", 339 | "-path", appPath, 340 | }... ).Output() 341 | 342 | if strings.Contains( string(status), "Installing:100%" ) { 343 | return true 344 | } 345 | return false 346 | } 347 | 348 | func (self *IIFDev) LaunchApp( bundleId string ) bool { 349 | return false 350 | } 351 | 352 | func (self *IIFDev) info( names []string ) map[string]string { 353 | mapped := make( map[string]string ) 354 | //fmt.Printf("udid for info: %s\n", self.udid ) 355 | args := []string { 356 | "info", 357 | "-json", 358 | "-id", self.udid, 359 | } 360 | args = append( args, names... ) 361 | json, _ := exec.Command( self.bridge.cli, args... ).Output() 362 | //fmt.Printf("json:%s\n",json) 363 | root, _ := uj.Parse( json ) 364 | 365 | for _,name := range names { 366 | node := root.Get(name) 367 | if node != nil { 368 | mapped[name] = node.String() 369 | } 370 | } 371 | //fmt.Printf("mapped result:%s\n",mapped) 372 | 373 | return mapped 374 | } 375 | 376 | func (self *IIFDev) gestalt( names []string ) map[string]string { 377 | mapped := make( map[string]string ) 378 | args := []string{ 379 | "mg", 380 | "-json", 381 | "-id", self.udid, 382 | } 383 | args = append( args, names... ) 384 | fmt.Printf("Running %s %s\n", self.bridge.cli, args ); 385 | json, _ := exec.Command( self.bridge.cli, args... ).Output() 386 | fmt.Printf("json:%s\n",json) 387 | root, _ := uj.Parse( json ) 388 | for _,name := range names { 389 | node := root.Get(name) 390 | if node != nil { 391 | mapped[name] = node.String() 392 | } 393 | } 394 | 395 | return mapped 396 | } 397 | 398 | func (self *IIFDev) gestaltnode( names []string ) map[string]uj.JNode { 399 | mapped := make( map[string]uj.JNode ) 400 | args := []string{ 401 | "mg", 402 | "-json", 403 | "-id", self.udid, 404 | } 405 | args = append( args, names... ) 406 | fmt.Printf("Running %s %s\n", self.bridge.cli, args ); 407 | json, _ := exec.Command( self.bridge.cli, args... ).Output() 408 | fmt.Printf("json:%s\n",json) 409 | root, _ := uj.Parse( json ) 410 | for _,name := range names { 411 | node := root.Get(name) 412 | if node != nil { 413 | mapped[name] = node 414 | } 415 | } 416 | 417 | return mapped 418 | } 419 | 420 | func (self *IIFDev) ps() []iProc { 421 | return []iProc{} 422 | } 423 | 424 | func (self *IIFDev) screenshot() Screenshot { 425 | return Screenshot{} 426 | } 427 | 428 | type BackupVideoIIF struct { 429 | port int 430 | //sock mangos.Socket 431 | spec string 432 | imgId int 433 | } 434 | 435 | func (self *IIFDev) NewSyslogMonitor( handleLogItem func( msg string, app string ) ) { 436 | bufstr := "" 437 | toFetch := 0 438 | o := ProcOptions{ 439 | procName: "syslogMonitor", 440 | binary: self.bridge.cli, 441 | args: []string { 442 | "log", 443 | "-id", self.udid, 444 | "proc", "SpringBoard(SpringBoard)", 445 | "proc", "SpringBoard(FrontBoard)", 446 | "proc", "dasd", 447 | }, 448 | startFields: log.Fields{ 449 | "id": self.udid, 450 | }, 451 | stdoutHandler: func( line string, plog *log.Entry ) { 452 | if line[0] == '*' { 453 | i:=1 454 | for ;i<6;i++ { 455 | char := line[i] 456 | if char == '[' { 457 | break 458 | } 459 | } 460 | bytesStr := line[ 1: i ] 461 | toFetch, _ = strconv.Atoi( bytesStr ) 462 | toFetch-- 463 | 464 | rest := line[ i: ] 465 | //fmt.Printf("msg len: %d -- want: %d\n", len(rest), toFetch ) 466 | if len( rest ) == toFetch { 467 | json := line[ i: ] 468 | root, _, err := uj.ParseFull( []byte( json ) ) 469 | if err == nil { 470 | msg := root.GetAt( 3 ).String() 471 | app := root.GetAt( 1 ).String() 472 | handleLogItem( msg, app ) 473 | } else { 474 | fmt.Printf("Could not parse:[%s]\n", json ) 475 | } 476 | } else { 477 | bufstr = rest 478 | toFetch -= len( rest ) 479 | } 480 | } else if toFetch > 0 { 481 | if len( line ) < toFetch { 482 | toFetch -= len( line ) 483 | bufstr = bufstr + line 484 | } else if len( line ) >= toFetch { 485 | bufstr = bufstr + line 486 | 487 | root, _, err := uj.ParseFull( []byte( bufstr ) ) 488 | if err == nil { 489 | msg := root.GetAt( 3 ).String() 490 | app := root.GetAt( 1 ).String() 491 | handleLogItem( msg, app ) 492 | } else { 493 | fmt.Printf("Could not parse:[%s]\n", bufstr ) 494 | } 495 | } 496 | 497 | } 498 | }, 499 | } 500 | 501 | proc_generic( self.procTracker, nil, &o ) 502 | } 503 | 504 | func (self *IIFDev) NewBackupVideo( port int, onStop func( interface{} ) ) BackupVideo { 505 | vid := &BackupVideoIIF{ 506 | port: port, 507 | spec: fmt.Sprintf( "http://127.0.0.1:%d", port ), 508 | } 509 | 510 | o := ProcOptions{ 511 | procName: "backupVideo", 512 | binary: self.bridge.cli, 513 | args: []string { 514 | "iserver", 515 | "-port", strconv.Itoa( port ), 516 | "-id", self.udid, 517 | }, 518 | startFields: log.Fields{ 519 | "port": strconv.Itoa( port ), 520 | "id": self.udid, 521 | }, 522 | onStop: func( wrapper interface{} ) { 523 | onStop( wrapper ) 524 | }, 525 | stdoutHandler: func( line string, plog *log.Entry ) { 526 | if strings.Contains( line, "listening" ) { 527 | plog.Println( line ) 528 | //vid.openBackupStream() 529 | } 530 | 531 | //fmt.Printf( "backup video:%s\n", line ) 532 | }, 533 | stderrHandler: func( line string, plog *log.Entry ) { 534 | //fmt.Printf( "backup video err:%s\n", line ) 535 | }, 536 | } 537 | 538 | proc_generic( self.procTracker, nil, &o ) 539 | 540 | return vid 541 | } 542 | 543 | func (self *BackupVideoIIF) GetFrame() []byte { 544 | resp, err := http.Get( self.spec ) 545 | if err != nil { 546 | panic(err) 547 | } 548 | defer resp.Body.Close() 549 | //data, _ := ioutil.ReadAll( resp.Body ) 550 | 551 | data := resp.Body 552 | img, err := png.Decode( data ) 553 | if err != nil { 554 | fmt.Printf("Could not decode backup video frame: %s\n", err ) 555 | //if length( data ) < 300 { 556 | // fmt.Printf("Data: %s\n", data ) 557 | //} 558 | return []byte{} 559 | } 560 | img2 := nr.Resize( 0, 1000, img, nr.Lanczos3 ) 561 | 562 | jpegBytes := bytes.Buffer{} 563 | writer := bufio.NewWriter( &jpegBytes ) 564 | jpeg.Encode( writer, img2, nil ) 565 | 566 | return jpegBytes.Bytes() 567 | } 568 | 569 | func (self *IIFDev) cfa( onStart func(), onStop func(interface{}) ) { 570 | config := self.bridge.config 571 | method := config.cfaMethod 572 | 573 | if method == "go-ios" { 574 | self.cfaGoIos( onStart, onStop ) 575 | } else if method == "tidevice" { 576 | self.cfaTidevice( onStart, onStop ) 577 | } else if method == "manual" { 578 | //self.wdaTidevice( port, onStart, onStop, mjpegPort ) 579 | } else { 580 | fmt.Printf("Unknown cfa start method %s\n", method ) 581 | os.Exit(1) 582 | } 583 | } 584 | 585 | func (self *IIFDev) wda( onStart func(), onStop func(interface{}) ) { 586 | } 587 | 588 | func (self *IIFDev) cfaGoIos( onStart func(), onStop func(interface{}) ) { 589 | f, err := os.OpenFile("cfa.log", 590 | os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 591 | if err != nil { 592 | log.WithFields( log.Fields{ 593 | "type": "wda_log_fail", 594 | } ).Fatal("Could not open cfa.log for writing") 595 | } 596 | 597 | config := self.bridge.config 598 | biPrefix := config.wdaPrefix 599 | bi := fmt.Sprintf( "%s.CFAgentRunner.xctrunner", biPrefix ) 600 | 601 | args := []string{ 602 | "runwda", 603 | "--bundleid", bi, 604 | "--testrunnerbundleid", bi, 605 | "--xctestconfig", "CFAgentRunner.xctest", 606 | "--udid", self.udid, 607 | } 608 | 609 | fmt.Fprintf( f, "Starting CFA via %s with args %s\n", "bin/go-ios", strings.Join( args, " " ) ) 610 | fmt.Printf( "Starting CFA via %s with args %s\n", "bin/go-ios", strings.Join( args, " " ) ) 611 | 612 | o := ProcOptions { 613 | procName: "cfa", 614 | binary: "bin/go-ios", 615 | args: args, 616 | stdoutHandler: func( line string, plog *log.Entry ) { 617 | if strings.Contains( line, "configuration is unsupported" ) { 618 | plog.Println( line ) 619 | } 620 | fmt.Fprintf( f, "runcfa: %s\n", line ) 621 | }, 622 | stderrHandler: func( line string, plog *log.Entry ) { 623 | if strings.Contains(line, "NNG Ready") { 624 | plog.WithFields( log.Fields{ 625 | "type": "cfa_start", 626 | "uuid": censorUuid(self.udid), 627 | } ).Info("[CFA] successfully started") 628 | onStart() 629 | } 630 | if strings.Contains( line, "configuration is unsupported" ) { 631 | plog.Println( line ) 632 | } 633 | fmt.Fprintf( f, "runcfa: %s\n", line ) 634 | }, 635 | onStop: func( wrapper interface{} ) { 636 | onStop( wrapper ) 637 | }, 638 | } 639 | 640 | proc_generic( self.procTracker, nil, &o ) 641 | } 642 | 643 | func (self *IIFDev) cfaTidevice( onStart func(), onStop func(interface{}) ) { 644 | config := self.bridge.config 645 | tiPath := config.tidevicePath 646 | 647 | if tiPath == "" { 648 | log.WithFields( log.Fields{ 649 | "type": "tidevice_path_unset", 650 | } ).Fatal("tidevice path is unknown. Run `make usetidevice` to correct") 651 | } 652 | 653 | f, err := os.OpenFile("cfa.log", 654 | os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) 655 | if err != nil { 656 | log.WithFields( log.Fields{ 657 | "type": "cfa_log_fail", 658 | } ).Fatal("Could not open cfa.log for writing") 659 | } 660 | 661 | biPrefix := config.cfaPrefix 662 | bi := fmt.Sprintf( "%s.CFAgentRunner.xctrunner", biPrefix ) 663 | 664 | args := []string{ 665 | "-u", self.udid, 666 | "wdaproxy", 667 | "-B", bi, 668 | "-p", "0", 669 | } 670 | 671 | fmt.Fprintf( f, "Starting CFA via %s with args %s\n", tiPath, strings.Join( args, " " ) ) 672 | 673 | o := ProcOptions { 674 | procName: "wda", 675 | binary: tiPath, 676 | args: args, 677 | stderrHandler: func( line string, plog *log.Entry ) { 678 | if strings.Contains(line, "CFAgent start successfully") { 679 | plog.WithFields( log.Fields{ 680 | "type": "cfa_start", 681 | "uuid": censorUuid(self.udid), 682 | } ).Info("[CFA] successfully started") 683 | onStart() 684 | } 685 | if strings.Contains( line, "have to mount the Developer disk image" ) { 686 | plog.WithFields( log.Fields{ 687 | "type": "cfa_start_err", 688 | "uuid": censorUuid(self.udid), 689 | } ).Fatal("[CFA] Developer disk not mounted. Cannot start CFA") 690 | } 691 | if strings.Contains( line, "'No app matches'" ) { 692 | plog.WithFields( log.Fields{ 693 | "type": "cfa_start_err", 694 | "uuid": censorUuid(self.udid), 695 | "rawErr": line, 696 | } ).Fatal("[CFA] Incorrect CFA bundle id") 697 | } 698 | fmt.Fprintln( f, line ) 699 | }, 700 | onStop: func( wrapper interface{} ) { 701 | onStop( wrapper ) 702 | }, 703 | } 704 | 705 | proc_generic( self.procTracker, nil, &o ) 706 | } 707 | 708 | func (self *IIFDev) destroy() { 709 | // close running processes 710 | } 711 | 712 | func (self *IIFDev) SetConfig( config *CDevice ) { 713 | self.config = config 714 | } 715 | 716 | func (self *IIFDev) SetDevice( device *Device ) { 717 | self.device = device 718 | } 719 | 720 | func (self *IIFDev) SetCustom( name string, val interface{} ) { 721 | } -------------------------------------------------------------------------------- /cfa.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | //"io/ioutil" 6 | "net/http" 7 | "strings" 8 | "os" 9 | "time" 10 | log "github.com/sirupsen/logrus" 11 | uj "github.com/nanoscopic/ujsonin/v2/mod" 12 | "go.nanomsg.org/mangos/v3" 13 | nanoReq "go.nanomsg.org/mangos/v3/protocol/req" 14 | ) 15 | 16 | type CFA struct { 17 | udid string 18 | devTracker *DeviceTracker 19 | dev *Device 20 | cfaProc *GenericProc 21 | config *Config 22 | base string 23 | //sessionId string 24 | startChan chan int 25 | js2hid map[int]int 26 | transport *http.Transport 27 | client *http.Client 28 | nngPort int 29 | nngPort2 int 30 | nngSocket mangos.Socket 31 | nngSocket2 mangos.Socket 32 | disableUpdate bool 33 | sessionMade bool 34 | } 35 | 36 | func NewCFA( config *Config, devTracker *DeviceTracker, dev *Device ) (*CFA) { 37 | self := NewCFANoStart( config, devTracker, dev ) 38 | if config.cfaMethod != "manual" { 39 | self.start( nil ) 40 | } else { 41 | self.startCfaNng( func( err int, stopChan chan bool ) { 42 | if err != 0 { 43 | dev.EventCh <- DevEvent{ action: DEV_CFA_START_ERR } 44 | } else { 45 | dev.EventCh <- DevEvent{ action: DEV_CFA_START } 46 | } 47 | } ) 48 | } 49 | return self 50 | } 51 | 52 | func addrange( amap map[int]int, from1 int, to1 int, from2 int ) { 53 | for i:=from1; i<=to1; i++ { 54 | amap[ i ] = i - from1 + from2 55 | } 56 | } 57 | 58 | func NewCFANoStart( config *Config, devTracker *DeviceTracker, dev *Device ) (*CFA) { 59 | jh := make( map[int]int ) 60 | 61 | self := CFA{ 62 | udid: dev.udid, 63 | nngPort: dev.cfaNngPort, 64 | nngPort2: dev.cfaNngPort2, 65 | devTracker: devTracker, 66 | dev: dev, 67 | config: config, 68 | //base: fmt.Sprintf("http://127.0.0.1:%d",dev.wdaPort), 69 | js2hid: jh, 70 | transport: &http.Transport{}, 71 | } 72 | //self.client = &http.Client{ 73 | // Transport: self.transport, 74 | //} 75 | 76 | /* 77 | The following generates a map of "JS keycodes" to Apple IO Hid event numbers. 78 | At least, for everything accessible without using Shift... 79 | At least for US keyboards. 80 | 81 | TODO: Modify this to read in information from configuration and also pay attention 82 | to the region of the device. In other regions keyboards will have different keys. 83 | 84 | The positive numbers here are the normal character set codes; in this case they are 85 | ASCII. 86 | 87 | The negative numbers are JS keyCodes for non-printable characters. 88 | */ 89 | addrange( jh, 97, 122, 4 ) // a-z 90 | addrange( jh, 49, 57, 0x1e ) // 1-9 91 | jh[32] = 0x2c // space 92 | jh[39] = 0x34 // ' 93 | jh[44] = 0x36 // , 94 | jh[45] = 0x2d // - 95 | jh[46] = 0x37 // . 96 | jh[47] = 0x38 // / 97 | jh[48] = 0x27 // 0 98 | jh[59] = 0x33 // ; 99 | jh[61] = 0x2e // = 100 | jh[91] = 0x2f // [ 101 | jh[92] = 0x31 // \ 102 | jh[93] = 0x30 // ] 103 | //jh[96] = // ` 104 | 105 | jh[-8] = 0x2a // backspace 106 | jh[-9] = 0x2b // tab 107 | jh[-13] = 0x28 // enter 108 | jh[-27] = 0x29 // esc 109 | jh[-33] = 0x4b // pageup 110 | jh[-34] = 0x4e // pagedown 111 | jh[-35] = 0x4d // end 112 | jh[-36] = 0x4a // home 113 | 114 | jh[-37] = 0x50 // left 115 | jh[-38] = 0x52 // up 116 | jh[-39] = 0x4f // right 117 | jh[-40] = 0x51 // down 118 | jh[-46] = 0x4c // delete 119 | 120 | return &self 121 | } 122 | 123 | func (self *CFA) dialNng( port int ) ( mangos.Socket, int, chan bool ) { 124 | spec := fmt.Sprintf( "tcp://127.0.0.1:%d", port ) 125 | 126 | var err error 127 | var reqSock mangos.Socket 128 | 129 | if reqSock, err = nanoReq.NewSocket(); err != nil { 130 | log.WithFields( log.Fields{ 131 | "type": "err_socket_new", 132 | "zmq_spec": spec, 133 | "err": err, 134 | } ).Info("Socket new error") 135 | return nil, 1, nil 136 | } 137 | 138 | if err = reqSock.Dial( spec ); err != nil { 139 | log.WithFields( log.Fields{ 140 | "type": "err_socket_dial", 141 | "spec": spec, 142 | "err": err, 143 | } ).Info("Socket dial error") 144 | return nil, 2, nil 145 | } 146 | 147 | stopChan := make( chan bool ) 148 | 149 | reqSock.SetPipeEventHook( func( action mangos.PipeEvent, pipe mangos.Pipe ) { 150 | //fmt.Printf("Pipe action %d\n", action ) 151 | if action == 2 { 152 | stopChan <- true 153 | } 154 | } ) 155 | 156 | return reqSock, 0, stopChan 157 | } 158 | 159 | func (self *CFA) startCfaNng( onready func( int, chan bool ) ) { 160 | pairs := []TunPair{ 161 | TunPair{ from: self.nngPort, to: 8101 }, 162 | TunPair{ from: self.nngPort2, to: 8102 }, 163 | } 164 | 165 | self.dev.bridge.tunnel( pairs, func() { 166 | nngSocket, err, stopChan := self.dialNng( self.nngPort ) 167 | if err != 0 { 168 | onready( err, nil ) 169 | return 170 | } 171 | self.nngSocket = nngSocket 172 | 173 | nngSocket2, err, _ := self.dialNng( self.nngPort2 ) 174 | if err != 0 { 175 | onready( err, nil ) 176 | return 177 | } 178 | self.nngSocket2 = nngSocket2 179 | 180 | self.create_session("") 181 | if onready != nil { 182 | onready( 0, stopChan ) 183 | } 184 | } ) 185 | } 186 | 187 | func (self *CFA) start( started func( int, chan bool ) ) { 188 | pairs := []TunPair{ 189 | TunPair{ from: self.nngPort, to: 8101 }, 190 | TunPair{ from: self.nngPort2, to: 8102 }, 191 | } 192 | 193 | self.dev.bridge.tunnel( pairs, func() { 194 | self.dev.bridge.cfa( 195 | func() { // onStart 196 | log.WithFields( log.Fields{ 197 | "type": "cfa_start", 198 | "udid": censorUuid(self.udid), 199 | "nngPort": self.nngPort, 200 | } ).Info("[CFA] successfully started") 201 | 202 | log.WithFields( log.Fields{ 203 | "type": "cfa_nng_dialing", 204 | "port": self.nngPort, 205 | } ).Debug("CFA - Dialing NNG") 206 | 207 | nngSocket, err1, stopChan := self.dialNng(self.nngPort) 208 | if err1 == 0 { 209 | self.nngSocket = nngSocket 210 | log.WithFields( log.Fields{ 211 | "type": "cfa_nng_dialed", 212 | "port": self.nngPort, 213 | } ).Debug("WDA - NNG Dialed") 214 | } 215 | 216 | nngSocket, err2, stopChan := self.dialNng(self.nngPort2) 217 | if err2 == 0 { 218 | self.nngSocket2 = nngSocket 219 | log.WithFields( log.Fields{ 220 | "type": "cfa_nng2_dialed", 221 | "port": self.nngPort2, 222 | } ).Debug("WDA - NNG2 Dialed") 223 | } 224 | 225 | if err1 != 0 || err2 != 0 { 226 | fmt.Printf("Error starting/connecting to CFA.\n") 227 | self.dev.EventCh <- DevEvent{ action: DEV_CFA_START_ERR } 228 | return 229 | } 230 | 231 | if started != nil { 232 | started( 0, stopChan ) 233 | } 234 | 235 | if self.startChan != nil { 236 | self.startChan <- 0 237 | } 238 | 239 | self.dev.EventCh <- DevEvent{ action: DEV_CFA_START } 240 | }, 241 | func(interface{}) { // onStop 242 | self.dev.EventCh <- DevEvent{ action: DEV_CFA_STOP } 243 | }, 244 | ) 245 | } ) 246 | } 247 | 248 | func (self *CFA) stop() { 249 | if self.cfaProc != nil { 250 | self.cfaProc.Kill() 251 | self.cfaProc = nil 252 | } 253 | } 254 | 255 | func (self *CFA) ensureSession() { 256 | sid := self.get_session() 257 | if sid == "" { 258 | //fmt.Printf("No CFA session exists. Creating\n" ) 259 | sid = self.create_session( "" ) 260 | //fmt.Printf("Created cfa session id=%s\n", sid ) 261 | } else { 262 | //fmt.Printf("Session existing; id=%s\n", sid ) 263 | } 264 | } 265 | 266 | func ( self *CFA ) get_session() ( string ) { 267 | if self.sessionMade { 268 | return "1" 269 | } else { 270 | return "" 271 | } 272 | } 273 | 274 | func ( self *CFA ) create_session( bundle string ) ( string ) { 275 | if bundle == "" { 276 | //bundle = "com.apple.Preferences" 277 | log.WithFields( log.Fields{ 278 | "type": "cfa_session_creating", 279 | "bi": "NONE", 280 | } ).Debug("Creating CFA session") 281 | } else { 282 | log.WithFields( log.Fields{ 283 | "type": "cfa_session_creating", 284 | "bi": bundle, 285 | } ).Debug("Creating CFA session") 286 | } 287 | 288 | self.disableUpdate = true 289 | 290 | json := fmt.Sprintf( `{ 291 | action: "createSession" 292 | bundleId: "%s" 293 | }`, bundle ) 294 | 295 | err := self.nngSocket.Send([]byte(json)) 296 | if err != nil { 297 | fmt.Printf("Send error: %s\n", err ) 298 | } 299 | //fmt.Printf("Sent; receiving\n" ) 300 | 301 | _, err = self.nngSocket.Recv() 302 | sid := "" 303 | if err != nil { 304 | fmt.Printf( "sessionCreate err: %s\n", err ) 305 | } else { 306 | sid = "1" 307 | self.sessionMade = true 308 | } 309 | 310 | self.disableUpdate = false 311 | 312 | log.WithFields( log.Fields{ 313 | "type": "cfa_session_created", 314 | } ).Info("Created CFA session") 315 | 316 | return sid 317 | } 318 | 319 | func (self *CFA) clickAt( x int, y int ) { 320 | json := fmt.Sprintf( `{ 321 | action: "tap" 322 | x:%d 323 | y:%d 324 | }`, x, y ) 325 | 326 | self.nngSocket.Send([]byte(json)) 327 | self.nngSocket.Recv() 328 | } 329 | 330 | func (self *CFA) mouseDown( x int, y int ) { 331 | json := fmt.Sprintf( `{ 332 | action: "mouseDown" 333 | x:%d 334 | y:%d 335 | }`, x, y ) 336 | 337 | self.nngSocket.Send([]byte(json)) 338 | self.nngSocket.Recv() 339 | } 340 | 341 | func (self *CFA) mouseUp( x int, y int ) { 342 | json := fmt.Sprintf( `{ 343 | action: "mouseUp" 344 | x:%d 345 | y:%d 346 | }`, x, y ) 347 | 348 | self.nngSocket.Send([]byte(json)) 349 | self.nngSocket.Recv() 350 | } 351 | 352 | func (self *CFA) hardPress( x int, y int ) { 353 | log.Info( "Firm Press:", x, y ) 354 | json := fmt.Sprintf( `{ 355 | action: "tapFirm" 356 | x:%d 357 | y:%d 358 | pressure:1 359 | }`, x, y ) 360 | 361 | self.nngSocket.Send([]byte(json)) 362 | self.nngSocket.Recv() 363 | } 364 | 365 | func (self *CFA) longPress( x int, y int, time float64 ) { 366 | log.Info( "Press for time:", x, y, time ) 367 | json := fmt.Sprintf( `{ 368 | action: "tapTime" 369 | x:%d 370 | y:%d 371 | time:%f 372 | }`, x, y, time ) 373 | 374 | self.nngSocket.Send([]byte(json)) 375 | self.nngSocket.Recv() 376 | } 377 | 378 | func (self *CFA) home() (string) { 379 | json := `{ 380 | action: "button" 381 | name: "home" 382 | }` 383 | self.nngSocket.Send([]byte(json)) 384 | self.nngSocket.Recv() 385 | 386 | return "" 387 | } 388 | 389 | func (self *CFA) AT() (string) { 390 | json := `{ 391 | action: "homebtn" 392 | }` 393 | self.nngSocket.Send([]byte(json)) 394 | self.nngSocket.Recv() 395 | self.nngSocket.Send([]byte(json)) 396 | self.nngSocket.Recv() 397 | self.nngSocket.Send([]byte(json)) 398 | self.nngSocket.Recv() 399 | 400 | return "" 401 | } 402 | 403 | func (self *CFA) keys( codes []int ) { 404 | if len( codes ) > 1 { 405 | self.typeText( codes ) 406 | return 407 | } 408 | code := codes[0] 409 | 410 | /* 411 | Only some keys are able to be pressed via IoHid, because I cannot 412 | figure out how to use 'shift' accessed characters/keys through it. 413 | 414 | If someone is able to figure it out please let me know as the "ViaKeys" 415 | method uses the much slower [application typeType] method. 416 | */ 417 | if self.config.cfaKeyMethod == "iohid" { 418 | dest, ok := self.js2hid[ code ] 419 | if ok { 420 | self.keysViaIohid( []int{dest} ) 421 | } else { 422 | self.typeText( codes ) 423 | } 424 | } else { 425 | self.typeText( codes ) 426 | } 427 | } 428 | 429 | func (self *CFA) keysViaIohid( codes []int ) { 430 | /* 431 | This loop of making repeated calls is obviously quite garbage. 432 | A better solution would be to make a call in CFA itself able to handle 433 | multiple characters at once. 434 | 435 | Despite this the performIoHidEvent call is very fast so it can generally 436 | keep up with typing speed of a manual user of CF. 437 | */ 438 | for _, code := range codes { 439 | json := fmt.Sprintf(`{ 440 | action: "iohid" 441 | page: 7 442 | usage: %d 443 | duration: 0.05 444 | }`, code ) 445 | 446 | log.Info( "sending " + json ) 447 | 448 | self.nngSocket.Send([]byte(json)) 449 | self.nngSocket.Recv() 450 | } 451 | } 452 | 453 | func (self *CFA) ioHid( page int, code int ) { 454 | json := fmt.Sprintf(`{ 455 | action: "iohid" 456 | page: %d 457 | usage: %d 458 | duration: 0.05 459 | }`, page, code ) 460 | 461 | log.Info( "sending " + json ) 462 | 463 | self.nngSocket.Send([]byte(json)) 464 | self.nngSocket.Recv() 465 | } 466 | 467 | func (self *CFA) typeText( codes []int ) { 468 | strArr := []string{} 469 | 470 | for _, code := range codes { 471 | // GoLang encodes to utf8 by default. typeText call expects utf8 encoding 472 | strArr = append( strArr, fmt.Sprintf("%c", rune( code ) ) ) 473 | } 474 | 475 | json := fmt.Sprintf(`{ 476 | action: "typeText" 477 | text: "%s" 478 | }`, strings.Join( strArr, "" ) ) 479 | 480 | log.Info( "sending " + json ) 481 | 482 | self.nngSocket.Send([]byte(json)) 483 | self.nngSocket.Recv() 484 | } 485 | 486 | func ( self *CFA ) swipe( x1 int, y1 int, x2 int, y2 int, delay float64 ) { 487 | log.Info( "Swiping:", x1, y1, x2, y2, delay ) 488 | 489 | json := fmt.Sprintf( `{ 490 | action: "swipe" 491 | x1:%d 492 | y1:%d 493 | x2:%d 494 | y2:%d 495 | delay:%.2f 496 | }`, x1, y1, x2, y2, delay ) 497 | 498 | self.nngSocket.Send([]byte(json)) 499 | self.nngSocket.Recv() 500 | } 501 | 502 | func (self *CFA) ElClick( elId string ) { 503 | log.Info( "elClick:", elId ) 504 | json := fmt.Sprintf( `{ 505 | action: "elClick" 506 | id: "%s" 507 | }`, elId ) 508 | 509 | self.nngSocket.Send([]byte(json)) 510 | self.nngSocket.Recv() 511 | } 512 | 513 | func (self *CFA) ElForceTouch( elId string, pressure int ) { 514 | log.Info( "elForceTouch:", elId, pressure ) 515 | json := fmt.Sprintf( `{ 516 | action: "elForceTouch" 517 | id: "%s" 518 | time: 2 519 | pressure: %d 520 | }`, elId, pressure ) 521 | 522 | self.nngSocket.Send([]byte(json)) 523 | self.nngSocket.Recv() 524 | } 525 | 526 | func (self *CFA) ElLongTouch( elId string ) { 527 | log.Info( "elTouchAndHold", elId ) 528 | json := fmt.Sprintf( `{ 529 | action: "elTouchAndHold" 530 | id: "%s" 531 | time: 2.0 532 | }`, elId ) 533 | 534 | self.nngSocket.Send([]byte(json)) 535 | self.nngSocket.Recv() 536 | } 537 | 538 | func (self *CFA) GetEl( elType string, elName string, system bool, wait float32 ) string { 539 | log.Info( "getEl:", elName ) 540 | 541 | sysLine := "" 542 | if system { 543 | sysLine = "system:1"; 544 | } 545 | 546 | waitLine := "" 547 | if wait > 0 { 548 | waitLine = fmt.Sprintf("wait:%f",wait) 549 | } 550 | 551 | json := fmt.Sprintf( `{ 552 | action: "getEl" 553 | type: "%s" 554 | id: "%s" 555 | %s 556 | %s 557 | }`, elType, elName, sysLine, waitLine ) 558 | 559 | self.nngSocket.Send([]byte(json)) 560 | idBytes, _ := self.nngSocket.Recv() 561 | 562 | log.Info( "getEl-result:", string(idBytes) ) 563 | 564 | return string( idBytes ) 565 | } 566 | 567 | func (self *CFA) WindowSize() (int,int) { 568 | log.Info("windowSize") 569 | self.nngSocket.Send([]byte(`{ action: "windowSize" }`)) 570 | jsonBytes, _ := self.nngSocket.Recv() 571 | root, _, _ := uj.ParseFull( jsonBytes ) 572 | width := root.Get("width").Int() 573 | height := root.Get("height").Int() 574 | 575 | log.Info("windowSize-result:",width,height) 576 | return width,height 577 | } 578 | 579 | func (self *CFA) Source(bi string, pid int) string { 580 | biLine := "" 581 | if bi != "" { 582 | biLine = "bi: \"" + bi + "\"" 583 | } 584 | if pid != 0 { 585 | biLine = fmt.Sprintf( "pid: %d", pid ) 586 | } 587 | json := fmt.Sprintf( `{ 588 | action: "source" 589 | %s 590 | }`, biLine ) 591 | self.nngSocket.Send([]byte(json)) 592 | srcBytes, _ := self.nngSocket.Recv() 593 | 594 | return string(srcBytes) 595 | } 596 | 597 | func (self *CFA) ElPos(id string) (int,int,int,int) { 598 | json := fmt.Sprintf( `{ 599 | action: "elPos" 600 | id: "%s" 601 | }`, id ) 602 | self.nngSocket.Send([]byte(json)) 603 | posJson, _ := self.nngSocket.Recv() 604 | 605 | root, _, _ := uj.ParseFull( posJson ) 606 | w := root.Get("w").Int() 607 | h := root.Get("h").Int() 608 | x := root.Get("x").Int() 609 | y := root.Get("y").Int() 610 | 611 | return x,y,w,h 612 | } 613 | 614 | func (self *CFA) AlertInfo() ( uj.JNode, string ) { 615 | self.ensureSession() 616 | self.nngSocket.Send([]byte(`{ action: "alertInfo" }`)) 617 | jsonBytes, _ := self.nngSocket.Recv() 618 | fmt.Printf("alertInfo res: %s\n", string(jsonBytes) ) 619 | root, _, _ := uj.ParseFull( jsonBytes ) 620 | presentNode := root.Get("present") 621 | if presentNode == nil { 622 | fmt.Printf("Error reading alertInfo; got back %s\n", string(jsonBytes) ) 623 | return nil, string(jsonBytes) 624 | } else { 625 | if presentNode.Bool() == false { return nil, string(jsonBytes) } 626 | return root, string(jsonBytes) 627 | } 628 | } 629 | 630 | func (self *CFA) WifiIp() string { 631 | self.nngSocket.Send([]byte(`{ action: "wifiIp" }`)) 632 | srcBytes, _ := self.nngSocket.Recv() 633 | 634 | return string(srcBytes) 635 | } 636 | 637 | func (self *CFA) ActiveApps() string { 638 | self.nngSocket.Send([]byte(`{ action: "activeApps" }`)) 639 | srcBytes, _ := self.nngSocket.Recv() 640 | 641 | return string(srcBytes) 642 | } 643 | 644 | func (self *CFA) SourceJson() string { 645 | self.nngSocket.Send([]byte(`{ action: "sourcej" }`)) 646 | srcBytes, _ := self.nngSocket.Recv() 647 | 648 | return string(srcBytes) 649 | } 650 | 651 | func (self *CFA) ToLauncher() string { 652 | self.nngSocket.Send([]byte(`{ action: "toLauncher" }`)) 653 | resp, _ := self.nngSocket.Recv() 654 | 655 | return string(resp) 656 | } 657 | 658 | func (self *CFA) Screenshot() []byte { 659 | self.nngSocket2.Send([]byte(`{ action: "screenshot2" }`)) 660 | imgBytes, _ := self.nngSocket2.Recv() 661 | 662 | return imgBytes 663 | } 664 | 665 | func (self *CFA) Siri(text string) { 666 | self.nngSocket.Send([]byte(fmt.Sprintf(`{ action: "siri", text: "%s" }`, text))) 667 | self.nngSocket.Recv() 668 | } 669 | 670 | func (self *CFA) ElByPid(pid int,json bool) string { 671 | if json { 672 | self.nngSocket.Send([]byte(fmt.Sprintf(`{ action: "elByPid", pid: %d, json: 1 }`, pid))) 673 | } else { 674 | self.nngSocket.Send([]byte(fmt.Sprintf(`{ action: "elByPid", pid: %d }`, pid))) 675 | } 676 | srcBytes, _ := self.nngSocket.Recv() 677 | 678 | return string(srcBytes) 679 | } 680 | 681 | func (self *CFA) PidChildWithWidth(pid int,width int) string { 682 | self.nngSocket.Send([]byte(fmt.Sprintf(`{ action: "pidChildWithWidth", pid: %d, width: %d }`, pid, width))) 683 | srcBytes, _ := self.nngSocket.Recv() 684 | 685 | return string(srcBytes) 686 | } 687 | 688 | func (self *CFA) AppAtPoint( x int, y int, asjson bool, nopid bool, top bool ) string { 689 | jsonLine := "" 690 | if asjson { jsonLine = "json: 1" } 691 | pidLine := "" 692 | if nopid { pidLine = "nopid: 1" } 693 | topLine := "" 694 | if top { topLine = "top: 1" } 695 | json := fmt.Sprintf( `{ 696 | action: "elementAtPoint" 697 | x: %d 698 | y: %d 699 | %s 700 | %s 701 | %s 702 | }`, x, y, jsonLine, pidLine, topLine ) 703 | self.nngSocket.Send([]byte(json)) 704 | srcBytes, _ := self.nngSocket.Recv() 705 | 706 | return string(srcBytes) 707 | } 708 | 709 | func (self *CFA) IsLocked() bool { 710 | self.nngSocket.Send([]byte(`{ action: "isLocked" }`)) 711 | jsonBytes, _ := self.nngSocket.Recv() 712 | root, _, _ := uj.ParseFull( jsonBytes ) 713 | return root.Get("locked").Bool() 714 | } 715 | 716 | func (self *CFA) Unlock () { 717 | self.nngSocket.Send([]byte(`{ action: "unlock" }`)) 718 | res, _ := self.nngSocket.Recv() 719 | fmt.Printf("Result:%s\n", string( res ) ) 720 | } 721 | 722 | func (self *CFA) OpenControlCenter() { 723 | ccMethod := self.dev.devConfig.controlCenterMethod 724 | 725 | fmt.Printf("Opening control center\n") 726 | width, height := self.WindowSize() 727 | 728 | if ccMethod == "bottomUp" { 729 | midx := width / 2 730 | maxy := height - 1 731 | self.swipe( midx, maxy, midx, maxy - 200, 0.2 ) 732 | } else if ccMethod == "topDown" { 733 | maxx := width - 1 734 | self.swipe( maxx, 0, maxx, 200, 0.2 ) 735 | } 736 | } 737 | 738 | func (self *CFA) swipeBack() { 739 | width, height := self.WindowSize() 740 | midy := height / 2 741 | midx := width / 2 742 | self.swipe( 1, midy, midx, midy, 0.1 ) 743 | } 744 | 745 | func (self *CFA) AddRecordingToCC() { 746 | self.create_session("com.apple.Preferences") 747 | 748 | self.AppChanged("com.apple.Preferences") 749 | 750 | i := 0 751 | ccEl := "" 752 | for { 753 | ccEl = self.GetEl("staticText","Control Center", false, 1 ) 754 | if ccEl == "" { 755 | self.swipeBack() 756 | i++ 757 | if i>3 { 758 | break 759 | } 760 | continue; 761 | } 762 | break 763 | } 764 | self.ElClick( ccEl ) 765 | 766 | customizeEl := self.GetEl("staticText","Customize Controls", false, 2 ) 767 | self.ElClick( customizeEl ) 768 | 769 | //x,y,w,h := self.ElPos( addRecEl ) 770 | //fmt.Printf("x:%d,y:%d,w:%d,h:%d\n",x,y,w,h) 771 | 772 | width, height := self.WindowSize() 773 | midy := height / 2 774 | midx := width / 2 775 | 776 | self.swipe( midx, midy, midx, midy-100, 0.1 ) 777 | 778 | addRecEl := self.GetEl("button","Insert Screen Recording", false, 2 ) 779 | 780 | self.ElClick( addRecEl ) 781 | } 782 | 783 | func (self *CFA) StartBroadcastStream( appName string, bid string, devConfig *CDevice ) { 784 | method := devConfig.vidStartMethod 785 | ccRecordingMethod := devConfig.ccRecordingMethod 786 | 787 | sid := self.create_session( bid ) 788 | if sid == "" { 789 | // TODO error creating session 790 | } 791 | 792 | fmt.Printf("Checking for alerts\n") 793 | alerts := self.config.vidAlerts 794 | for { 795 | alert, _ := self.AlertInfo() 796 | if alert == nil { break } 797 | text := alert.Get("alert").String() 798 | 799 | dismissed := false 800 | // dismiss the alert 801 | for _, alert := range alerts { 802 | if strings.Contains( text, alert.match ) { 803 | fmt.Printf("Alert matching \"%s\" appeared. Autoresponding with \"%s\"\n", 804 | alert.match, alert.response ) 805 | btn := self.GetEl( "button", alert.response, true, 0 ) 806 | if btn == "" { 807 | fmt.Printf("Alert does not contain button \"%s\"\n", alert.response ) 808 | } else { 809 | self.ElClick( btn ) 810 | dismissed = true 811 | break 812 | } 813 | } 814 | } 815 | if !dismissed { 816 | // TODO; get rid of the alert some other way 817 | break 818 | } 819 | 820 | // Give time for another alert to appear 821 | time.Sleep( time.Second * 1 ) 822 | } 823 | 824 | fmt.Printf("vidApp start method: %s\n", method ) 825 | if method == "app" { 826 | fmt.Printf("Starting vidApp through the app\n") 827 | 828 | toSelector := self.GetEl( "button", "Broadcast Selector", false, 5 ) 829 | self.ElClick( toSelector ) 830 | 831 | startBtn := self.GetEl( "button", "Start Broadcast", true, 5 ) 832 | if startBtn == "" { 833 | startBtn := self.GetEl( "staticText", "Start Broadcast", false, 2 ) 834 | if startBtn == "" { 835 | startBtn := self.GetEl( "button", "Start Broadcast", false, 2 ) 836 | if startBtn == "" { 837 | fmt.Printf("Error! Could not fetch Start Broadcast button\n") 838 | } 839 | } 840 | } 841 | self.ElClick( startBtn ) 842 | } else if method == "controlCenter" { 843 | fmt.Printf("Starting vidApp through control center\n") 844 | time.Sleep( time.Second * 2 ) 845 | self.OpenControlCenter() 846 | //self.Source() 847 | 848 | devEl := self.GetEl( "button", "Screen Recording", true, 5 ) 849 | fmt.Printf("Selecting Screen Recording; el=%s\n", devEl ) 850 | if ccRecordingMethod == "longTouch" { 851 | self.ElLongTouch( devEl ) 852 | } else if ccRecordingMethod == "forceTouch" { 853 | self.ElForceTouch( devEl, 1 ) 854 | } else { 855 | fmt.Printf("ccRecordingMethod for a device must be either longTouch or forceTouch\n") 856 | os.Exit(0) 857 | } 858 | 859 | appEl := self.GetEl( "staticText", appName, true, 5 ) 860 | self.ElClick( appEl ) 861 | 862 | startBtn := self.GetEl( "button", "Start Broadcast", true, 5 ) 863 | self.ElClick( startBtn ) 864 | 865 | time.Sleep( time.Second * 3 ) 866 | } else if method == "manual" { 867 | } 868 | 869 | self.ToLauncher() 870 | 871 | time.Sleep( time.Second * 5 ) 872 | } 873 | 874 | func (self *CFA) AppChanged( bundleId string ) { 875 | if self.disableUpdate { return } 876 | 877 | json := fmt.Sprintf( `{ 878 | action: "updateApplication" 879 | bundleId: "%s" 880 | }`, bundleId ) 881 | 882 | if self.nngSocket != nil { 883 | self.nngSocket.Send([]byte(json)) 884 | self.nngSocket.Recv() 885 | } 886 | } -------------------------------------------------------------------------------- /config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "fmt" 6 | "io/ioutil" 7 | "net/http" 8 | "os" 9 | uj "github.com/nanoscopic/ujsonin/v2/mod" 10 | log "github.com/sirupsen/logrus" 11 | ) 12 | 13 | type CDevice struct { 14 | udid string 15 | uiWidth int 16 | uiHeight int 17 | cfaMethod string 18 | wdaMethod string 19 | tunnelMethod string 20 | wdaPort int 21 | vidStartMethod string 22 | controlCenterMethod string 23 | ccRecordingMethod string 24 | videoMode string 25 | } 26 | 27 | type AlertConfig struct { 28 | match string 29 | response string 30 | } 31 | 32 | type Config struct { 33 | iosIfPath string 34 | goIosPath string 35 | httpPort int 36 | cfHost string 37 | cfUsername string 38 | devs map [string] CDevice 39 | //cfaXcPath string 40 | https bool 41 | selfSigned bool 42 | wdaPath string 43 | cfaPath string 44 | tidevicePath string 45 | cfaMethod string 46 | wdaMethod string 47 | cfaKeyMethod string 48 | wdaPrefix string 49 | cfaPrefix string 50 | cfaSanityCheck bool 51 | //wdaSanityCheck bool 52 | vidAppName string 53 | vidAppBid string 54 | vidAppBidPrefix string 55 | vidAppExtBid string 56 | portRange string 57 | bridge string 58 | alerts []AlertConfig 59 | vidAlerts []AlertConfig 60 | idList []string 61 | cpuProfile bool 62 | } 63 | 64 | func GetStr( root uj.JNode, path string ) string { 65 | node := root.Get( path ) 66 | if node == nil { 67 | fmt.Fprintf( os.Stderr, "%s is not set in either config.json or default.json", path ) 68 | os.Exit(1) 69 | } 70 | return node.String() 71 | } 72 | func GetBool( root uj.JNode, path string ) bool { 73 | node := root.Get( path ) 74 | if node == nil { 75 | fmt.Fprintf( os.Stderr, "%s is not set in either config.json or default.json", path ) 76 | os.Exit(1) 77 | } 78 | return node.Bool() 79 | } 80 | func GetInt( root uj.JNode, path string ) int { 81 | node := root.Get( path ) 82 | if node == nil { 83 | fmt.Fprintf( os.Stderr, "%s is not set in either config.json or default.json", path ) 84 | os.Exit(1) 85 | } 86 | return node.Int() 87 | } 88 | 89 | func NewConfig( configPath string, defaultsPath string, calculatedPath string ) (*Config) { 90 | config := Config{} 91 | 92 | root := loadConfig( configPath, defaultsPath, calculatedPath ) 93 | 94 | config.iosIfPath = GetStr( root, "bin_paths.iosif" ) 95 | config.goIosPath = GetStr( root, "bin_paths.goios" ) 96 | config.httpPort = GetInt( root, "port" ) 97 | config.cfHost = GetStr( root, "controlfloor.host" ) 98 | config.cfUsername = GetStr( root, "controlfloor.username" ) 99 | //config.xcPath = GetStr( root, "wdaXctestRunFolder" ) 100 | config.https = GetBool( root, "controlfloor.https" ) 101 | config.selfSigned = GetBool( root, "controlfloor.selfSigned" ) 102 | config.wdaPath = GetStr( root, "bin_paths.wda" ) 103 | config.cfaPath = GetStr( root, "bin_paths.cfa" ) 104 | config.cfaMethod = GetStr( root, "cfa.startMethod" ) 105 | config.wdaMethod = GetStr( root, "wda.startMethod" ) 106 | config.cfaKeyMethod = GetStr( root, "cfa.keyMethod" ) 107 | config.cfaPrefix = GetStr( root, "cfa.bundleIdPrefix" ) 108 | config.wdaPrefix = GetStr( root, "wda.bundleIdPrefix" ) 109 | config.cfaSanityCheck = GetBool( root, "cfa.sanityCheck" ) 110 | config.vidAppName = GetStr( root, "vidapp.name" ) 111 | config.vidAppBid = GetStr( root, "vidapp.bundleId" ) 112 | config.vidAppExtBid = GetStr( root, "vidapp.extBundleId" ) 113 | config.vidAppBidPrefix = GetStr( root, "vidapp.bundleIdPrefix" ) 114 | config.portRange = GetStr( root, "portRange" ) 115 | config.bridge = GetStr( root, "bridge" ) 116 | config.idList = []string{} 117 | 118 | tideviceNode := root.Get( "tidevice" ) 119 | if tideviceNode != nil { 120 | config.tidevicePath = tideviceNode.String() 121 | } else { 122 | config.tidevicePath = "" 123 | } 124 | 125 | if config.https { 126 | if config.selfSigned { 127 | http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{ 128 | InsecureSkipVerify: true, 129 | } 130 | //http.DefaultTransport.(*http.Transport).ForceAttemptHTTP2 = false 131 | } 132 | } 133 | 134 | config.devs = readDevs( root ) 135 | 136 | config.alerts = readAlerts( root, "alerts" ) 137 | config.vidAlerts = readAlerts( root, "vidStartAlerts" ) 138 | 139 | return &config 140 | } 141 | 142 | func readAlerts( root uj.JNode, nodeName string ) []AlertConfig { 143 | res := []AlertConfig{} 144 | 145 | alertNodes := root.Get(nodeName) 146 | if alertNodes == nil { return res } 147 | 148 | alertNodes.ForEach( func( alertNode uj.JNode ) { 149 | match := alertNode.Get("match").String() 150 | response := alertNode.Get("response").String() 151 | res = append( res, AlertConfig{ match, response } ) 152 | } ) 153 | 154 | return res 155 | } 156 | 157 | func readDevs( root uj.JNode ) ( map[string]CDevice ) { 158 | devs := make( map[string]CDevice ) 159 | 160 | devsNode := root.Get("devices") 161 | if devsNode != nil { 162 | devsNode.ForEach( func( devNode uj.JNode ) { 163 | udid := devNode.Get("udid").String() 164 | uiWidth := 0 165 | uiHeight := 0 166 | wdaPort := 0 167 | controlCenterMethod := "bottomUp" 168 | vidStartMethod := "app" 169 | widthNode := devNode.Get("uiWidth") 170 | cfaMethod := "" 171 | wdaMethod := "" 172 | ccRecordingMethod := "longTouch" 173 | tunnelMethod := "go-ios" 174 | videoMode := "cfagent" 175 | if widthNode != nil { 176 | uiWidth = widthNode.Int() 177 | } 178 | heightNode := devNode.Get("uiHeight") 179 | if heightNode != nil { 180 | uiHeight = heightNode.Int() 181 | } 182 | wdaPortNode := devNode.Get("wdaPort") 183 | if wdaPortNode != nil { 184 | wdaPort = wdaPortNode.Int() 185 | } 186 | cfaMethodNode := devNode.Get("cfaMethod") 187 | if cfaMethodNode != nil { 188 | cfaMethod = cfaMethodNode.String() 189 | } 190 | wdaMethodNode := devNode.Get("wdaMethod") 191 | if wdaMethodNode != nil { 192 | wdaMethod = wdaMethodNode.String() 193 | } 194 | methodNode := devNode.Get("controlCenterMethod") 195 | if methodNode != nil { 196 | controlCenterMethod = methodNode.String() 197 | } 198 | vidStartMethodNode := devNode.Get("vidStartMethod") 199 | if vidStartMethodNode != nil { 200 | vidStartMethod = vidStartMethodNode.String() 201 | } 202 | ccRecordingMethodNode := devNode.Get("ccRecordingMethod") 203 | if ccRecordingMethodNode != nil { 204 | ccRecordingMethod = ccRecordingMethodNode.String() 205 | } 206 | tunnelMethodNode := devNode.Get("tunnelMethod") 207 | if tunnelMethodNode != nil { 208 | tunnelMethod = tunnelMethodNode.String() 209 | } 210 | videoModeNode := devNode.Get("videoMode") 211 | if videoModeNode != nil { 212 | videoMode = videoModeNode.String() 213 | } 214 | 215 | dev := CDevice{ 216 | udid: udid, 217 | uiWidth: uiWidth, 218 | uiHeight: uiHeight, 219 | cfaMethod: cfaMethod, 220 | wdaMethod: wdaMethod, 221 | wdaPort: wdaPort, 222 | vidStartMethod: vidStartMethod, 223 | controlCenterMethod: controlCenterMethod, 224 | ccRecordingMethod: ccRecordingMethod, 225 | tunnelMethod: tunnelMethod, 226 | videoMode: videoMode, 227 | } 228 | devs[ udid ] = dev 229 | } ) 230 | } 231 | return devs 232 | } 233 | 234 | func loadConfig( configPath string, defaultsPath string, calculatedPath string ) (uj.JNode) { 235 | // read in defaults 236 | fh1, serr1 := os.Stat( defaultsPath ) 237 | if serr1 != nil { 238 | log.WithFields( log.Fields{ 239 | "type": "err_read_defaults", 240 | "error": serr1, 241 | "defaults_path": defaultsPath, 242 | } ).Fatal("Could not read specified defaults path") 243 | } 244 | defaultsFile := defaultsPath 245 | switch mode := fh1.Mode(); { 246 | case mode.IsDir(): defaultsFile = fmt.Sprintf("%s/default.json", defaultsPath) 247 | } 248 | content1, err1 := ioutil.ReadFile( defaultsFile ) 249 | if err1 != nil { log.Fatal( err1 ) } 250 | defaults, _ := uj.Parse( content1 ) 251 | 252 | // read in normal config 253 | fh, serr := os.Stat( configPath ) 254 | if serr != nil { 255 | log.WithFields( log.Fields{ 256 | "type": "err_read_config", 257 | "error": serr, 258 | "config_path": configPath, 259 | } ).Fatal("Could not read specified config path") 260 | } 261 | configFile := configPath 262 | switch mode := fh.Mode(); { 263 | case mode.IsDir(): configFile = fmt.Sprintf("%s/config.json", configPath) 264 | } 265 | content, err := ioutil.ReadFile( configFile ) 266 | if err != nil { log.Fatal( err ) } 267 | root, _ := uj.Parse( content ) 268 | 269 | defaults.Overlay( root ) 270 | 271 | if calculatedPath != "" { 272 | fh2, serr2 := os.Stat( calculatedPath ) 273 | if serr2 != nil { 274 | log.WithFields( log.Fields{ 275 | "type": "err_read_calculated", 276 | "error": serr2, 277 | "defaults_path": calculatedPath, 278 | } ).Warn("Could not read specified calculated path. Calculated options will not function.") 279 | } else { 280 | calculatedFile := calculatedPath 281 | switch mode := fh2.Mode(); { 282 | case mode.IsDir(): calculatedFile = fmt.Sprintf("%s/default.json", calculatedPath) 283 | } 284 | content2, err2 := ioutil.ReadFile( calculatedFile ) 285 | if err2 != nil { log.Fatal( err2 ) } 286 | calculated, _ := uj.Parse( content2 ) 287 | defaults.Overlay( calculated ) 288 | } 289 | } 290 | //defaults.Dump() 291 | 292 | return defaults 293 | } 294 | -------------------------------------------------------------------------------- /config.json.example: -------------------------------------------------------------------------------- 1 | { 2 | controlfloor: { 3 | host: "localhost:8080" 4 | username: "first" 5 | https: true 6 | selfSigned: true 7 | } 8 | cfa: { 9 | // Your Apple Developer Team OU 10 | // If you don't know this, you can find it by running ./util/signers.pl 11 | devTeamOu: "7628766FL2" 12 | 13 | // Some unique Bundle ID prefix to usefor the Bundle IDs for CFAgent 14 | // This default, "com.dryark", will likely work for paid developer accounts 15 | // Make sure the provisioning profile you setup has a wildcard identifier matching this 16 | // The two identifiers that will be made if "com.dryark" is used are 17 | // "com.dryark.CFAgentLib" 18 | // "com.dryark.CFAgent" 19 | // If you are using a free developer account, you will not have any provisioning profile, 20 | // so you will need to set this bundle prefix to something globally unique, such as 21 | // "com.[your name]" 22 | bundleIdPrefix: "com.dryark" 23 | 24 | runner: { 25 | buildStyle: "Automatic" // or "Manual" 26 | provisioningProfile: "" // specify when buildStyle is Manual 27 | } 28 | }, 29 | wda: { 30 | // Your Apple Developer Team OU 31 | // If you don't know this, you can find it by running ./util/signers.pl 32 | devTeamOu: "7628766FL2" 33 | 34 | // Some unique Bundle ID prefix to usefor the Bundle IDs for WebDriverAgent 35 | // This default, "com.appium", will likely work for paid developer accounts 36 | // Make sure the provisioning profile you setup has a wildcard identifier matching this 37 | // The two identifiers that will be made if "com.appium" is used are 38 | // "com.appium.WebDriverAgentLib" 39 | // "com.appium.WebDriverAgentRunner" 40 | // If you are using a free developer account, you will not have any provisioning profile, 41 | // so you will need to set this bundle prefix to something globally unique, such as 42 | // "com.[your name]" 43 | bundleIdPrefix: "com.appium" 44 | 45 | runner: { 46 | buildStyle: "Automatic" // or "Manual" 47 | provisioningProfile: "" // specify when buildStyle is Manual 48 | } 49 | }, 50 | vidapp: { 51 | devTeamOu: "7628766FL2" 52 | bundleIdPrefix: "com.dryark" 53 | }, 54 | devices: [ 55 | { 56 | udid: "00008020-001D0D661E040011" 57 | uiWidth: 414 58 | uiHeight: 896 59 | controlCenterMethod: "topDown" 60 | } 61 | ] 62 | } 63 | -------------------------------------------------------------------------------- /default.json: -------------------------------------------------------------------------------- 1 | { 2 | controlfloor: { 3 | https: false 4 | selfSigned: false 5 | } 6 | bin_paths: { 7 | iosif: "bin/iosif" 8 | wda: "bin/wda" 9 | cfa: "bin/cfa" 10 | goios: "bin/go-ios" 11 | } 12 | bridge: "go-ios" 13 | repos: { 14 | wda: "https://github.com/appium/WebDriverAgent.git" 15 | cfa: "https://github.com/nanoscopic/ControlFloorAgent.git" 16 | ujsonin: "https://github.com/nanoscopic/ujsonin.git" 17 | pbxproj: "https://github.com/kronenthaler/mod-pbxproj" 18 | iosif: "https://github.com/nanoscopic/iosif.git" 19 | vidapp: "https://github.com/nanoscopic/ios_video_app.git" 20 | goios: "https://github.com/nanoscopic/go-ios.git" 21 | } 22 | cfa: { 23 | lib: { 24 | buildStyle: "Automatic" // or "Manual" 25 | provisioningProfile: "" // always blank 26 | } 27 | runner: { 28 | buildStyle: "Automatic" // or "Manual" 29 | provisioningProfile: "" // specify when buildStyle is Manual 30 | } 31 | startMethod: "go-ios" 32 | keyMethod: "iohid" 33 | sanityCheck: true 34 | }, 35 | wda: { 36 | lib: { 37 | buildStyle: "Automatic" // or "Manual" 38 | provisioningProfile: "" // always blank 39 | } 40 | runner: { 41 | buildStyle: "Automatic" // or "Manual" 42 | provisioningProfile: "" // specify when buildStyle is Manual 43 | } 44 | startMethod: "go-ios" 45 | sanityCheck: true 46 | }, 47 | vidapp: { 48 | name: "CF Vidstream" 49 | bundleId: "vidstream" 50 | extBundleId: "vidstream_ext" 51 | main: { 52 | buildStyle: "Automatic" // or "Manual" 53 | provisioningProfile: "" 54 | }, 55 | extension: { 56 | buildStyle: "Automatic" // or "Manual" 57 | provisioningProfile: "" 58 | } 59 | }, 60 | wdaXctestRunFolder: "repos/WebDriverAgent/build/Build/Products" 61 | cfaXctestRunFolder: "repos/CFAgent/build/Build/Products" 62 | port: 8027 63 | portRange: "8101-8200" 64 | alerts: [ 65 | { 66 | match: "invalid broadcast session" 67 | response: "OK" 68 | } 69 | { 70 | match: "Vidstream has stopped" 71 | response: "OK" 72 | } 73 | ] 74 | vidStartAlerts: [ 75 | { 76 | match: "invalid broadcast session" 77 | response: "OK" 78 | } 79 | { 80 | match: "Vidstream has stopped" 81 | response: "OK" 82 | } 83 | ] 84 | } 85 | -------------------------------------------------------------------------------- /dev_info.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "strings" 5 | //log "github.com/sirupsen/logrus" 6 | //uj "github.com/nanoscopic/ujsonin/mod" 7 | ) 8 | 9 | func getDeviceName( bridge BridgeDev ) (string) { 10 | info := bridge.info( []string{ "DeviceName" } ) 11 | return info["DeviceName"] 12 | } 13 | 14 | func getAllDeviceInfo( bridge BridgeDev ) map[string] string { 15 | mainKeys := "DeviceName,EthernetAddress,ModelNumber,HardwareModel,PhoneNumber,ProductType,ProductVersion,UniqueDeviceID,InternationalCircuitCardIdentity,InternationalMobileEquipmentIdentity,InternationalMobileSubscriberIdentity" 16 | keyArr := strings.Split( mainKeys, "," ) 17 | return bridge.info( keyArr ) 18 | } 19 | 20 | func getDeviceInfo( bridge BridgeDev, keyName string ) map[string] string { 21 | if( keyName == "" ) { 22 | keyName = "DeviceName,EthernetAddress,ModelNumber,HardwareModel,PhoneNumber,ProductType,ProductVersion,UniqueDeviceID,InternationalCircuitCardIdentity,InternationalMobileEquipmentIdentity,InternationalMobileSubscriberIdentity" 23 | } 24 | keyArr := strings.Split( keyName, "," ) 25 | return bridge.info( keyArr ) 26 | } 27 | 28 | func getFirstDeviceId( root BridgeRoot ) ( string ) { 29 | return getDeviceIds( root )[0] 30 | } 31 | 32 | func getDeviceIds( root BridgeRoot ) ( []string ) { 33 | devs := root.list() 34 | ids := []string{} 35 | for _,dev := range devs { 36 | ids = append( ids, dev.udid ) 37 | } 38 | return ids 39 | } -------------------------------------------------------------------------------- /device.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "strconv" 7 | "sync" 8 | "time" 9 | log "github.com/sirupsen/logrus" 10 | ws "github.com/gorilla/websocket" 11 | uj "github.com/nanoscopic/ujsonin/v2/mod" 12 | ) 13 | 14 | const ( 15 | VID_NONE = iota 16 | VID_APP 17 | VID_BRIDGE 18 | VID_WDA 19 | VID_CFA 20 | VID_ENABLE 21 | VID_DISABLE 22 | VID_END 23 | ) 24 | 25 | const ( 26 | DEV_STOP = iota 27 | DEV_CFA_START 28 | DEV_CFA_START_ERR 29 | DEV_CFA_STOP 30 | DEV_WDA_START 31 | DEV_WDA_START_ERR 32 | DEV_WDA_STOP 33 | DEV_VIDEO_START 34 | DEV_VIDEO_STOP 35 | DEV_ALERT_APPEAR 36 | DEV_ALERT_GONE 37 | DEV_APP_CHANGED 38 | ) 39 | 40 | type Device struct { 41 | udid string 42 | name string 43 | lock *sync.Mutex 44 | wdaPort int 45 | wdaPortFixed bool 46 | cfaNngPort int 47 | cfaNngPort2 int 48 | vidPort int 49 | vidControlPort int 50 | vidLogPort int 51 | backupVideoPort int 52 | iosVersion string 53 | versionParts []int 54 | productType string 55 | productNum string 56 | vidWidth int 57 | vidHeight int 58 | vidMode int 59 | process map[string] *GenericProc 60 | owner string 61 | connected bool 62 | EventCh chan DevEvent 63 | BackupCh chan BackupEvent 64 | CFAFrameCh chan BackupEvent 65 | cfa *CFA 66 | wda *WDA 67 | cfaRunning bool 68 | wdaRunning bool 69 | devTracker *DeviceTracker 70 | config *Config 71 | devConfig *CDevice 72 | cf *ControlFloor 73 | info map[string] string 74 | vidStreamer VideoStreamer 75 | appStreamStopChan chan bool 76 | vidOut *ws.Conn 77 | bridge BridgeDev 78 | backupVideo BackupVideo 79 | backupActive bool 80 | shuttingDown bool 81 | alertMode bool 82 | vidUp bool 83 | } 84 | 85 | func NewDevice( config *Config, devTracker *DeviceTracker, udid string, bdev BridgeDev ) (*Device) { 86 | dev := Device{ 87 | devTracker: devTracker, 88 | wdaPortFixed: false, 89 | cfaNngPort: devTracker.getPort(), 90 | cfaNngPort2: devTracker.getPort(), 91 | vidPort: devTracker.getPort(), 92 | vidLogPort: devTracker.getPort(), 93 | vidMode: VID_NONE, 94 | vidControlPort: devTracker.getPort(), 95 | backupVideoPort: devTracker.getPort(), 96 | backupActive: false, 97 | config: config, 98 | udid: udid, 99 | lock: &sync.Mutex{}, 100 | process: make( map[string] *GenericProc ), 101 | cf: devTracker.cf, 102 | EventCh: make( chan DevEvent ), 103 | BackupCh: make( chan BackupEvent ), 104 | CFAFrameCh: make( chan BackupEvent ), 105 | bridge: bdev, 106 | cfaRunning: false, 107 | versionParts: []int{0,0,0}, 108 | } 109 | if devConfig, ok := config.devs[udid]; ok { 110 | dev.devConfig = &devConfig 111 | if devConfig.wdaPort != 0 { 112 | dev.wdaPort = devConfig.wdaPort 113 | dev.wdaPortFixed = true 114 | } else { 115 | dev.wdaPort = devTracker.getPort() 116 | } 117 | } else { 118 | dev.wdaPort = devTracker.getPort() 119 | } 120 | return &dev 121 | } 122 | 123 | func ( self *Device ) isShuttingDown() bool { 124 | return self.shuttingDown; 125 | } 126 | 127 | func ( self *Device ) releasePorts() { 128 | dt := self.devTracker 129 | if !self.wdaPortFixed { 130 | dt.freePort( self.wdaPort ) 131 | } 132 | dt.freePort( self.cfaNngPort ) 133 | dt.freePort( self.cfaNngPort2 ) 134 | dt.freePort( self.vidPort ) 135 | dt.freePort( self.vidLogPort ) 136 | dt.freePort( self.vidControlPort ) 137 | dt.freePort( self.backupVideoPort ) 138 | } 139 | 140 | func ( self *Device ) startProc( proc *GenericProc ) { 141 | self.lock.Lock() 142 | self.process[ proc.name ] = proc 143 | self.lock.Unlock() 144 | } 145 | 146 | func ( self *Device ) stopProc( procName string ) { 147 | self.lock.Lock() 148 | delete( self.process, procName ) 149 | self.lock.Unlock() 150 | } 151 | 152 | type BackupEvent struct { 153 | action int 154 | } 155 | 156 | type DevEvent struct { 157 | action int 158 | width int 159 | height int 160 | data string 161 | } 162 | 163 | func (self *Device) shutdown() { 164 | self.shutdownVidStream() 165 | 166 | go func() { self.endProcs() }() 167 | 168 | go func() { self.EventCh <- DevEvent{ action: DEV_STOP } }() 169 | go func() { self.BackupCh <- BackupEvent{ action: VID_END } }() 170 | 171 | for _,proc := range self.process { 172 | log.WithFields( log.Fields{ 173 | "type": "shutdown_dev_proc", 174 | "udid": censorUuid( self.udid ), 175 | "proc": proc.name, 176 | "pid": proc.pid, 177 | } ).Info("Shutting down " + proc.name + " process") 178 | go func() { proc.Kill() }() 179 | } 180 | } 181 | 182 | func (self *Device) onCfaReady() { 183 | self.cfaRunning = true 184 | self.cf.notifyCfaStarted( self.udid ) 185 | self.cfa.ensureSession() 186 | // start video streaming 187 | 188 | self.forwardVidPorts( self.udid, func() { 189 | videoMode := self.devConfig.videoMode 190 | if videoMode == "app" { 191 | self.enableAppVideo() 192 | } else if videoMode == "cfagent" { 193 | self.enableCFAVideo() 194 | } else { 195 | // TODO error 196 | } 197 | 198 | self.startProcs2() 199 | } ) 200 | } 201 | 202 | func (self *Device) onWdaReady() { 203 | self.wdaRunning = true 204 | self.cf.notifyWdaStarted( self.udid, self.wdaPort ) 205 | } 206 | 207 | func (self *Device) startEventLoop() { 208 | go func() { 209 | DEVEVENTLOOP: 210 | for { 211 | select { 212 | case event := <- self.EventCh: 213 | action := event.action 214 | if action == DEV_STOP { // stop event loop 215 | break DEVEVENTLOOP 216 | } else if action == DEV_CFA_START { // CFA started 217 | self.onCfaReady() 218 | } else if action == DEV_WDA_START { // CFA started 219 | self.onWdaReady() 220 | } else if action == DEV_CFA_START_ERR { 221 | fmt.Printf("Error starting/connecting to CFA.\n") 222 | self.shutdown() 223 | break DEVEVENTLOOP 224 | } else if action == DEV_CFA_STOP { // CFA stopped 225 | self.cfaRunning = false 226 | self.cf.notifyCfaStopped( self.udid ) 227 | } else if action == DEV_CFA_STOP { // CFA stopped 228 | self.wdaRunning = false 229 | self.cf.notifyWdaStopped( self.udid ) 230 | } else if action == DEV_VIDEO_START { // first video frame 231 | self.cf.notifyVideoStarted( self.udid ) 232 | self.onFirstFrame( &event ) 233 | } else if action == DEV_VIDEO_STOP { 234 | self.cf.notifyVideoStopped( self.udid ) 235 | } else if action == DEV_ALERT_APPEAR { 236 | self.enableBackupVideo() 237 | } else if action == DEV_ALERT_GONE { 238 | self.disableBackupVideo() 239 | } else if action == DEV_APP_CHANGED { 240 | self.devAppChanged( event.data ) 241 | } 242 | } 243 | } 244 | }() 245 | } 246 | 247 | func (self *Device) startBackupFrameProvider() { 248 | go func() { 249 | sending := false 250 | for { 251 | select { 252 | case ev := <- self.BackupCh: 253 | action := ev.action 254 | if action == VID_ENABLE { // begin sending backup frames 255 | sending = true 256 | fmt.Printf("backup video frame sender - enabling\n") 257 | } else if action == VID_DISABLE { 258 | sending = false 259 | fmt.Printf("backup video frame sender - disabling\n") 260 | } else if action == VID_END { 261 | break 262 | } 263 | default: 264 | } 265 | if sending { 266 | self.sendBackupFrame() 267 | } else { 268 | time.Sleep( time.Millisecond * 100 ) 269 | } 270 | } 271 | }() 272 | } 273 | 274 | func (self *Device) startCFAFrameProvider() { 275 | go func() { 276 | sending := false 277 | for { 278 | select { 279 | case ev := <- self.CFAFrameCh: 280 | action := ev.action 281 | if action == VID_ENABLE { 282 | sending = true 283 | fmt.Printf("cfa frame provider - enabling\n") 284 | } else if action == VID_DISABLE { 285 | sending = false 286 | fmt.Printf("cfa frame provider - disabled\n") 287 | } else if action == VID_END { 288 | break 289 | } 290 | default: 291 | } 292 | if sending { 293 | self.sendCFAFrame() 294 | } else { 295 | time.Sleep( time.Millisecond * 100 ) 296 | } 297 | } 298 | }() 299 | } 300 | 301 | func (self *Device) enableDefaultVideo() { 302 | videoMode := self.devConfig.videoMode 303 | if videoMode == "app" { 304 | self.vidMode = VID_APP 305 | self.vidStreamer.forceOneFrame() 306 | } else if videoMode == "cfagent" { 307 | self.vidMode = VID_CFA 308 | } else { 309 | // TODO error 310 | } 311 | } 312 | 313 | func (self *Device) disableBackupVideo() { 314 | fmt.Printf("Sending vid_disable\n") 315 | self.BackupCh <- BackupEvent{ action: VID_DISABLE } 316 | fmt.Printf("Sent vid_disable\n") 317 | self.backupActive = false 318 | self.enableDefaultVideo() 319 | } 320 | 321 | func (self *Device) enableBackupVideo() { 322 | fmt.Printf("Sending vid_enable\n") 323 | self.BackupCh <- BackupEvent{ action: VID_ENABLE } 324 | fmt.Printf("Sent vid_enable\n") 325 | self.vidMode = VID_BRIDGE 326 | self.backupActive = true 327 | } 328 | 329 | func (self *Device) disableCFAVideo() { 330 | fmt.Printf("Sending vid_disable\n") 331 | self.CFAFrameCh <- BackupEvent{ action: VID_DISABLE } 332 | fmt.Printf("Sent vid_disable\n") 333 | 334 | self.enableDefaultVideo() 335 | } 336 | 337 | func (self *Device) enableCFAVideo() { 338 | fmt.Printf("Sending vid_enable\n") 339 | self.CFAFrameCh <- BackupEvent{ action: VID_ENABLE } 340 | fmt.Printf("Sent vid_enable\n") 341 | self.vidMode = VID_CFA 342 | self.backupActive = true 343 | } 344 | 345 | func (self *Device) sendBackupFrame() { 346 | vidOut := self.vidOut 347 | if vidOut != nil { 348 | fmt.Printf("Fetching frame - ") 349 | pngData := self.backupVideo.GetFrame() 350 | fmt.Printf("%d bytes\n", len( pngData ) ) 351 | if( len( pngData ) > 0 ) { 352 | vidOut.WriteMessage( ws.BinaryMessage, pngData ) 353 | } 354 | } else { 355 | time.Sleep( time.Millisecond * 100 ) 356 | } 357 | } 358 | 359 | func (self *Device) sendCFAFrame() { 360 | vidOut := self.vidOut 361 | if vidOut != nil { 362 | pngData := self.cfa.Screenshot() 363 | //fmt.Printf("%d bytes\n", len( pngData ) ) 364 | if( len( pngData ) > 0 ) { 365 | vidOut.WriteMessage( ws.BinaryMessage, pngData ) 366 | } 367 | } else { 368 | time.Sleep( time.Millisecond * 100 ) 369 | } 370 | } 371 | 372 | func (self *Device) getBackupFrame() ( []byte, string) { 373 | if self == nil { 374 | return []byte{}, "wtf" 375 | } 376 | if self.backupVideo == nil { 377 | return []byte{}, "backup video not set on device object" 378 | } 379 | 380 | pngData := self.backupVideo.GetFrame() 381 | 382 | return pngData, "" 383 | } 384 | 385 | func (self *Device) stopEventLoop() { 386 | self.EventCh <- DevEvent{ action: DEV_STOP } 387 | } 388 | 389 | func (self *Device) startup() { 390 | self.startEventLoop() 391 | self.startProcs() 392 | } 393 | 394 | func (self *Device) startBackupVideo() { 395 | self.backupVideo = self.bridge.NewBackupVideo( 396 | self.backupVideoPort, 397 | func( interface{} ) {}, // onStop 398 | ) 399 | } 400 | 401 | func (self *Device) devAppChanged( bundleId string ) { 402 | if self.cfa == nil { 403 | return 404 | } 405 | 406 | self.cfa.AppChanged( bundleId ) 407 | } 408 | 409 | func (self *Device) startProcs() { 410 | // Start CFA 411 | self.cfa = NewCFA( self.config, self.devTracker, self ) 412 | 413 | if self.config.cfaMethod == "manual" { 414 | //self.cfa.startCfaNng() 415 | } 416 | 417 | self.startBackupFrameProvider() // just the timed loop 418 | self.startCFAFrameProvider() 419 | self.backupVideo = self.bridge.NewBackupVideo( 420 | self.backupVideoPort, 421 | func( interface{} ) {}, // onStop 422 | ) 423 | 424 | //self.enableBackupVideo() 425 | 426 | self.bridge.NewSyslogMonitor( func( msg string, app string ) { 427 | //msg := root.GetAt( 3 ).String() 428 | //app := root.GetAt( 1 ).String() 429 | 430 | //fmt.Printf("Msg:%s\n", msg ) 431 | 432 | if app == "SpringBoard(SpringBoard)" { 433 | if strings.Contains( msg, "Presenting 0 { 438 | for _, alert := range alerts { 439 | if strings.Contains( msg, alert.match ) { 440 | fmt.Printf("Alert matching \"%s\" appeared. Autoresponding with \"%s\"\n", 441 | alert.match, alert.response ) 442 | if self.cfaRunning { 443 | useAlertMode = false 444 | btn := self.cfa.GetEl( "button", alert.response, true, 0 ) 445 | if btn == "" { 446 | fmt.Printf("Alert does not contain button \"%s\"\n", alert.response ) 447 | } else { 448 | self.cfa.ElClick( btn ) 449 | } 450 | } 451 | 452 | } 453 | } 454 | } 455 | 456 | if useAlertMode && self.vidUp { 457 | fmt.Printf("Alert appeared\n") 458 | if len( alerts ) > 0 { 459 | fmt.Printf("Alert did not match any autoresponses; Msg content: %s\n", msg ) 460 | } 461 | self.EventCh <- DevEvent{ action: DEV_ALERT_APPEAR } 462 | self.alertMode = true 463 | } 464 | } else if strings.Contains( msg, "deactivate alertItem: " ) 480 | app := left[:endPos] 481 | fmt.Printf("app:%s\n", app ) 482 | self.EventCh <- DevEvent{ action: DEV_APP_CHANGED, data: app } 483 | } 484 | } 485 | } else if app == "dasd" { 486 | if strings.HasPrefix( msg, "Foreground apps changed" ) { 487 | //fmt.Printf("App changed\n") 488 | //self.EventCh <- DevEvent{ action: DEV_APP_CHANGED } 489 | } 490 | } 491 | } ) 492 | } 493 | 494 | func (self *Device) startProcs2() { 495 | self.appStreamStopChan = make( chan bool ) 496 | 497 | videoMode := self.devConfig.videoMode 498 | if videoMode == "app" { 499 | self.vidStreamer = NewAppStream( 500 | self.appStreamStopChan, 501 | self.vidControlPort, 502 | self.vidPort, 503 | self.vidLogPort, 504 | self.udid, 505 | self ) 506 | self.vidStreamer.mainLoop() 507 | } else if videoMode == "cfagent" { 508 | // Nothing todo 509 | } else { 510 | // TODO error 511 | } 512 | 513 | // Start WDA 514 | self.wda = NewWDA( self.config, self.devTracker, self ) 515 | } 516 | 517 | func (self *Device) vidAppIsAlive() bool { 518 | vidPid := self.bridge.GetPid( self.config.vidAppExtBid ) 519 | if vidPid != 0 { 520 | return true 521 | } 522 | return false 523 | } 524 | 525 | func (self *Device) enableAppVideo() { 526 | // check if video app is running 527 | vidPid := self.bridge.GetPid( self.config.vidAppExtBid ) 528 | 529 | // if it is running, go ahead and use it 530 | /*if vidPid != 0 { 531 | self.vidMode = VID_APP 532 | return 533 | }*/ 534 | 535 | // If it is running, kill it 536 | if vidPid != 0 { 537 | self.bridge.Kill( vidPid ) 538 | 539 | // Kill off replayd in case it is stuck 540 | rp_id := self.bridge.GetPid("replayd") 541 | if rp_id != 0 { 542 | self.bridge.Kill( rp_id ) 543 | } 544 | } 545 | 546 | // if video app is not running, check if it is installed 547 | 548 | bid := self.config.vidAppBidPrefix + "." + self.config.vidAppBid 549 | 550 | installInfo := self.bridge.AppInfo( bid ) 551 | // if installed, start it 552 | if installInfo != nil { 553 | fmt.Printf("Attempting to start video app stream\n") 554 | version := installInfo.Get("CFBundleShortVersionString").String() 555 | 556 | if version != "1.1" { 557 | fmt.Printf("Installed CF Vidstream app is version %s; must be version 1.1\n", version) 558 | panic("Wrong vidstream version") 559 | } 560 | 561 | self.cfa.StartBroadcastStream( self.config.vidAppName, bid, self.devConfig ) 562 | self.vidUp = true 563 | self.vidMode = VID_APP 564 | return 565 | } 566 | 567 | fmt.Printf("Vidstream not installed; attempting to install\n") 568 | 569 | // if video app is not installed 570 | // install it, then start it 571 | success := self.bridge.InstallApp( "vidstream.xcarchive/Products/Applications/vidstream.app" ) 572 | if success { 573 | self.cfa.StartBroadcastStream( self.config.vidAppName, bid, self.devConfig ) 574 | self.vidMode = VID_APP 575 | return 576 | } 577 | 578 | // if video app failed to start or install, just leave backup video running 579 | } 580 | 581 | func (self *Device) justStartBroadcast() { 582 | bid := self.config.vidAppBidPrefix + "." + self.config.vidAppBid 583 | self.cfa.StartBroadcastStream( self.config.vidAppName, bid, self.devConfig ) 584 | } 585 | 586 | func (self *Device) startVidStream() { 587 | conn := self.cf.connectVidChannel( self.udid ) 588 | 589 | imgData := self.cfa.Screenshot() 590 | conn.WriteMessage( ws.BinaryMessage, imgData ) 591 | 592 | var controlChan chan int 593 | if self.vidStreamer != nil { 594 | controlChan = self.vidStreamer.getControlChan() 595 | } 596 | 597 | // Necessary so that writes to the socket fail when the connection is lost 598 | go func() { 599 | for { 600 | if _, _, err := conn.NextReader(); err != nil { 601 | conn.Close() 602 | break 603 | } 604 | } 605 | }() 606 | 607 | self.vidOut = conn 608 | 609 | imgConsumer := NewImageConsumer( func( text string, data []byte ) (error) { 610 | if self.vidMode != VID_APP { return nil } 611 | //conn.WriteMessage( ws.TextMessage, []byte( fmt.Sprintf("{\"action\":\"normalFrame\"}") ) ) 612 | conn.WriteMessage( ws.TextMessage, []byte( text ) ) 613 | return conn.WriteMessage( ws.BinaryMessage, data ) 614 | }, func() { 615 | // there are no frames to send 616 | } ) 617 | 618 | if self.vidStreamer != nil { 619 | self.vidStreamer.setImageConsumer( imgConsumer ) 620 | fmt.Printf("Telling video stream to start\n") 621 | controlChan <- 1 // start 622 | } 623 | } 624 | 625 | func (self *Device) shutdownVidStream() { 626 | if self.vidOut != nil { 627 | self.stopVidStream() 628 | } 629 | ext_id := self.bridge.GetPid("vidstream_ext") 630 | if ext_id != 0 { 631 | self.bridge.Kill( ext_id ) 632 | } 633 | } 634 | 635 | func (self *Device) stopVidStream() { 636 | self.vidOut = nil 637 | self.cf.destroyVidChannel( self.udid ) 638 | } 639 | 640 | func (self *Device) forwardVidPorts( udid string, onready func() ) { 641 | self.bridge.tunnel( []TunPair{ 642 | TunPair{ from: self.vidPort, to: 8352 }, 643 | TunPair{ from: self.vidControlPort, to: 8351 }, 644 | TunPair{ from: self.vidLogPort, to: 8353 }, 645 | }, onready ) 646 | } 647 | 648 | func (self *Device) endProcs() { 649 | if self.appStreamStopChan != nil { 650 | self.appStreamStopChan <- true 651 | } 652 | } 653 | 654 | func (self *Device) onFirstFrame( event *DevEvent ) { 655 | self.vidWidth = event.width 656 | self.vidWidth = event.height 657 | log.WithFields( log.Fields{ 658 | "type": "first_frame", 659 | "proc": "ios_video_stream", 660 | "width": self.vidWidth, 661 | "height": self.vidWidth, 662 | "udid": censorUuid( self.udid ), 663 | } ).Info("Video - first frame") 664 | } 665 | 666 | func (self *Device) clickAt( x int, y int ) { 667 | self.cfa.clickAt( x, y ) 668 | } 669 | 670 | func (self *Device) mouseDown( x int, y int ) { 671 | self.cfa.mouseDown( x, y ) 672 | } 673 | 674 | func (self *Device) mouseUp( x int, y int ) { 675 | self.cfa.mouseUp( x, y ) 676 | } 677 | 678 | func (self *Device) hardPress( x int, y int ) { 679 | self.cfa.hardPress( x, y ) 680 | } 681 | 682 | func (self *Device) longPress( x int, y int, time float64 ) { 683 | self.cfa.longPress( x, y, time ) 684 | } 685 | 686 | func (self *Device) home() { 687 | self.cfa.home() 688 | } 689 | 690 | func findNodeWithAtt( cur uj.JNode, att string, label string ) uj.JNode { 691 | lNode := cur.Get(att) 692 | if lNode != nil { 693 | if lNode.String() == label { return cur } 694 | } 695 | 696 | cNode := cur.Get("c") 697 | if cNode == nil { return nil } 698 | 699 | var gotIt uj.JNode 700 | cNode.ForEach( func( child uj.JNode ) { 701 | res := findNodeWithAtt( child, att, label ) 702 | if res != nil { gotIt = res } 703 | } ) 704 | return gotIt 705 | } 706 | 707 | // Assumes AssistiveTouch is enabled already 708 | func (self *Device) openAssistiveTouch( pid int32 ) int { 709 | y := 0 710 | i := 0 711 | for { 712 | i++ 713 | if i>10 { 714 | fmt.Printf("AssistiveTouch icon did not appear\n") 715 | return 0 716 | } 717 | json := self.cfa.ElByPid( int(pid), true ) 718 | // Todo; element may not be there 719 | root, _ := uj.Parse( []byte(json) ) 720 | btnNode := findNodeWithAtt( root, "label", "AssistiveTouch menu" ) 721 | if btnNode == nil { 722 | time.Sleep( time.Millisecond * 100 ) 723 | continue 724 | } 725 | x := btnNode.Get("x").Int() 726 | y = btnNode.Get("y").Int() 727 | x += 20 728 | y += 20 729 | time.Sleep( time.Millisecond * 100 ) 730 | self.cfa.clickAt( x / 2, y / 2 ) 731 | break 732 | } 733 | 734 | return y 735 | } 736 | 737 | func (self *Device) taskSwitcher() { 738 | //self.cfa.Siri("activate assistivetouch") 739 | 740 | self.enableAssistiveTouch() 741 | 742 | _, pid := self.isAssistiveTouchEnabled() 743 | 744 | y := self.openAssistiveTouch( pid ) 745 | 746 | i := 0 747 | for { 748 | i++ 749 | if i>10 { 750 | fmt.Printf("Could not find multitasking button") 751 | return 752 | } 753 | 754 | // TODO don't use hardcoded screen center 755 | atJson := self.cfa.AppAtPoint( 187, y/2, true, true, false ) 756 | fmt.Println( atJson ) 757 | 758 | root2, _ := uj.Parse( []byte(atJson) ) 759 | taskNode := findNodeWithAtt( root2, "label", "Multitasking" ) 760 | if taskNode == nil { 761 | time.Sleep( time.Millisecond * 100 ) 762 | continue 763 | } 764 | x2 := taskNode.Get("x").Int() 765 | y2 := taskNode.Get("y").Int() 766 | x2 += 20 767 | y2 += 20 768 | time.Sleep( time.Millisecond * 200 ) 769 | self.cfa.clickAt( x2 / 2, y2 / 2 ) 770 | break 771 | } 772 | 773 | // Todo: Wait for task switcher to actually appear 774 | //time.Sleep( time.Millisecond * 600 ) 775 | //self.cfa.GetEl("other", "SBSwitcherWindow", false, 1 ) 776 | i = 0 777 | for { 778 | i++ 779 | if i>20 { 780 | fmt.Printf("Task Switcher did not appear\n") 781 | return 782 | } 783 | centerScreenJson := self.cfa.AppAtPoint( 187, 333, true, true, true ); 784 | root3, _ := uj.Parse( []byte(centerScreenJson) ) 785 | closeBox := findNodeWithAtt( root3, "id", "appCloseBox" ) 786 | if closeBox != nil { break } 787 | time.Sleep( time.Millisecond * 100 ) 788 | //fmt.Printf("Task switcher appeared\n") 789 | } 790 | 791 | self.disableAssistiveTouch() 792 | } 793 | 794 | func (self *Device) shake() { 795 | self.enableAssistiveTouch() 796 | 797 | self.disableAssistiveTouch() 798 | } 799 | 800 | func (self *Device) cc() { 801 | self.cfa.OpenControlCenter() 802 | } 803 | 804 | func (self *Device) isAssistiveTouchEnabled() (bool, int32) { 805 | var pid int32 806 | procs := self.bridge.ps() 807 | for _,proc := range procs { 808 | if proc.name == "assistivetouchd" { 809 | pid = proc.pid 810 | break 811 | } 812 | } 813 | if pid != 0 { return true, pid } 814 | return false, 0 815 | } 816 | 817 | func (self *Device) enableAssistiveTouch() { 818 | enabled, _ := self.isAssistiveTouchEnabled() 819 | if !enabled { self.toggleAssistiveTouch() } 820 | 821 | /*i := 0 822 | var pid int32 823 | for { 824 | i++ 825 | if i> 20 { // Wait up to 4 seconds for it to start 826 | fmt.Printf("AssistiveTouch process did not start") 827 | return 828 | } 829 | 830 | procs := self.bridge.ps() 831 | for _,proc := range procs { 832 | if proc.name == "assistivetouchd" { 833 | pid = proc.pid 834 | break 835 | } 836 | } 837 | if pid != 0 { break } 838 | time.Sleep( time.Millisecond * 200 ) 839 | }*/ 840 | } 841 | 842 | func (self *Device) disableAssistiveTouch() { 843 | enabled, _ := self.isAssistiveTouchEnabled() 844 | if enabled { self.toggleAssistiveTouch() } 845 | 846 | /*i = 0 847 | for { 848 | i++ 849 | if i > 20 { // Wait up to 4 seconds for it to stop 850 | fmt.Printf("AssistiveTouch process did not stop") 851 | return 852 | } 853 | 854 | procs := self.bridge.ps() 855 | pid = 0 856 | for _,proc := range procs { 857 | if proc.name == "assistivetouchd" { 858 | pid = proc.pid 859 | break 860 | } 861 | } 862 | if pid == 0 { break } 863 | time.Sleep( time.Millisecond * 200 ) 864 | }*/ 865 | } 866 | 867 | func (self *Device) toggleAssistiveTouch() { 868 | cfa := self.cfa 869 | self.cc() 870 | shortcutsBtn := cfa.GetEl( "button", "Accessibility Shortcuts", true, 2 ) 871 | cfa.ElClick( shortcutsBtn ) 872 | atBtn := cfa.GetEl( "button", "AssistiveTouch", true, 2 ) 873 | cfa.ElClick( atBtn ) 874 | time.Sleep( time.Millisecond * 100 ) 875 | cfa.home() 876 | time.Sleep( time.Millisecond * 300 ) 877 | cfa.home() 878 | } 879 | 880 | func (self *Device) iohid( page int, code int ) { 881 | self.cfa.ioHid( page, code ) 882 | } 883 | 884 | func (self *Device) swipe( x1 int, y1 int, x2 int, y2 int, delayBy100 int ) { 885 | delay := float64( delayBy100 ) / 100.0 886 | self.cfa.swipe( x1, y1, x2, y2, delay ) 887 | } 888 | 889 | func (self *Device) keys( keys string ) { 890 | parts := strings.Split( keys, "," ) 891 | codes := []int{} 892 | for _, key := range parts { 893 | code, _ := strconv.Atoi( key ) 894 | codes = append( codes, code ) 895 | } 896 | self.cfa.keys( codes ) 897 | } 898 | 899 | func (self *Device) source() string { 900 | return self.cfa.SourceJson() 901 | } 902 | 903 | func (self *Device) WifiIp() string { 904 | return self.cfa.WifiIp() 905 | } 906 | 907 | func (self *Device) AppAtPoint(x int, y int) string { 908 | return self.cfa.AppAtPoint(x,y,false,false,false) 909 | } 910 | 911 | func (self *Device) WifiMac() string { 912 | info := self.bridge.info( []string{"WiFiAddress"} ) 913 | val, ok := info["WiFiAddress"] 914 | if ok { return val } 915 | return "unknown" 916 | } 917 | 918 | func (self *Device) killBid( bid string ) { 919 | self.bridge.KillBid( bid ) 920 | } 921 | 922 | func (self *Device) launch( bid string ) { 923 | self.bridge.Launch( bid ) 924 | } -------------------------------------------------------------------------------- /device_tracker.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "sync" 8 | log "github.com/sirupsen/logrus" 9 | uj "github.com/nanoscopic/ujsonin/v2/mod" 10 | ) 11 | 12 | type Event struct { 13 | action int 14 | uuid string 15 | } 16 | 17 | type DeviceTracker struct { 18 | Config *Config 19 | DevMap map [string] *Device 20 | freePorts []int 21 | portMin int 22 | portMax int 23 | process map[string] *GenericProc 24 | lock *sync.Mutex 25 | cf *ControlFloor 26 | cfStop chan bool 27 | bridge BridgeRoot 28 | pendingDevs []BridgeDev 29 | shuttingDown bool 30 | // only activate the specific list of ids 31 | idList []string 32 | } 33 | 34 | func NewDeviceTracker( config *Config, detect bool, idList []string ) (*DeviceTracker) { 35 | var cf *ControlFloor 36 | var cfStop chan bool 37 | var cfReady chan bool 38 | if detect { 39 | cf, cfStop, cfReady = NewControlFloor( config ) 40 | <- cfReady 41 | } 42 | 43 | portRange := config.portRange 44 | parts := strings.Split(portRange,"-") 45 | portMin, _ := strconv.Atoi( parts[0] ) 46 | portMax, _ := strconv.Atoi( parts[1] ) 47 | 48 | self := &DeviceTracker{ 49 | process: make( map[string] *GenericProc ), 50 | lock: &sync.Mutex{}, 51 | DevMap: make( map [string] *Device ), 52 | Config: config, 53 | portMin: portMin, 54 | portMax: portMax, 55 | freePorts: []int{}, 56 | cf: cf, 57 | cfStop: cfStop, 58 | idList: idList, 59 | } 60 | 61 | bridgeCreator := NewIIFBridge 62 | bridgeCli := config.iosIfPath 63 | if config.bridge == "go-ios" { 64 | bridgeCreator = NewGIBridge 65 | bridgeCli = config.goIosPath 66 | } 67 | 68 | self.bridge = bridgeCreator( 69 | config, 70 | func( dev BridgeDev ) ProcTracker { return self.onDeviceConnect1( dev ) }, 71 | func( dev BridgeDev ) { self.onDeviceDisconnect1( dev ) }, 72 | bridgeCli, 73 | self, 74 | detect, 75 | ) 76 | if detect { 77 | cf.DevTracker = self 78 | } 79 | return self 80 | } 81 | 82 | func ( self *DeviceTracker ) isShuttingDown() bool { 83 | return self.shuttingDown; 84 | } 85 | 86 | func (self *DeviceTracker) startProc( proc *GenericProc ) { 87 | self.lock.Lock() 88 | self.process[ proc.name ] = proc 89 | self.lock.Unlock() 90 | } 91 | 92 | func ( self *DeviceTracker ) stopProc( procName string ) { 93 | self.lock.Lock() 94 | delete( self.process, procName ) 95 | self.lock.Unlock() 96 | } 97 | 98 | func (self *DeviceTracker) getPort() (int) { 99 | var port int 100 | self.lock.Lock() 101 | if len( self.freePorts ) > 0 { 102 | port = self.freePorts[0] 103 | self.freePorts = self.freePorts[1:] 104 | } else { 105 | port = self.portMin 106 | self.portMin++ 107 | } 108 | self.lock.Unlock() 109 | return port 110 | } 111 | 112 | func (self *DeviceTracker) freePort( port int ) { 113 | self.lock.Lock() 114 | self.freePorts = append( self.freePorts, port ) 115 | self.lock.Unlock() 116 | } 117 | 118 | func (self *DeviceTracker) getDevice( udid string ) (*Device) { 119 | return self.DevMap[ udid ] 120 | } 121 | 122 | func (self *DeviceTracker) cfReady() { 123 | fmt.Println("Starting delayed devices:") 124 | for _, bdev := range self.pendingDevs { 125 | fmt.Printf("Delayed device - udid: %s\n", bdev.getUdid() ) 126 | self.onDeviceConnect1( bdev ) 127 | } 128 | self.pendingDevs = []BridgeDev{} 129 | } 130 | 131 | func (self *DeviceTracker) onDeviceConnect1( bdev BridgeDev ) *Device { 132 | udid := bdev.getUdid() 133 | 134 | if len( self.idList ) > 0 { 135 | devFound := false 136 | for _,oneId := range( self.idList ) { 137 | if oneId == udid { 138 | devFound = true 139 | } 140 | } 141 | if !devFound { return nil } 142 | } 143 | 144 | if !self.cf.ready { 145 | self.pendingDevs = append( self.pendingDevs, bdev ) 146 | fmt.Printf("Device attached, but ControlFloor not ready.\n udid=%s\n", udid ) 147 | return nil 148 | } 149 | 150 | //fmt.Printf("udid: %s\n", udid) 151 | //dev := self.DevMap[ udid ] 152 | 153 | _, devConfOk := self.Config.devs[udid] 154 | 155 | clickWidth := 0 156 | clickHeight := 0 157 | width := 0 158 | height := 0 159 | 160 | 161 | var devConf *CDevice 162 | if devConfOk { 163 | devConfOb := self.Config.devs[udid] 164 | devConf = &devConfOb 165 | } else { 166 | fmt.Printf("Device not found in config.devices\n") 167 | } 168 | 169 | mgInfo := make( map[string]uj.JNode ) 170 | if devConfOk && devConf.uiWidth != 0 { 171 | devConf := self.Config.devs[ udid ] 172 | clickWidth = devConf.uiWidth 173 | clickHeight = devConf.uiHeight 174 | width = clickWidth 175 | height = clickHeight 176 | } else { 177 | mgInfo = bdev.gestaltnode( []string{ 178 | "AvailableDisplayZoomSizes", 179 | "main-screen-width", 180 | "main-screen-height", 181 | "ArtworkTraits", 182 | } ) 183 | width = mgInfo["main-screen-width"].Int() 184 | height = mgInfo["main-screen-height"].Int() 185 | 186 | sizeArr := mgInfo["AvailableDisplayZoomSizes"].Get("default") // zoomed also available 187 | clickWidth = sizeArr.GetAt(1).Int() 188 | clickHeight = sizeArr.GetAt(3).Int() 189 | } 190 | 191 | self.cf.notifyDeviceExists( udid, width, height, clickWidth, clickHeight ) 192 | dev := self.onDeviceConnect( udid, bdev ) 193 | self.cf.notifyDeviceInfo( dev, mgInfo["ArtworkTraits"] ) 194 | bdev.setProcTracker( self ) 195 | dev.startup() 196 | return dev 197 | } 198 | 199 | func (self *DeviceTracker) onDeviceDisconnect1( bdev BridgeDev ) { 200 | udid := bdev.getUdid() 201 | dev := self.DevMap[ udid ] 202 | 203 | self.onDeviceDisconnect( dev ) 204 | dev.stopEventLoop() 205 | dev.shutdown() 206 | 207 | dev.releasePorts() 208 | } 209 | 210 | func (self *DeviceTracker) shutdown() { 211 | self.shuttingDown = true 212 | 213 | for _,dev := range self.DevMap { 214 | dev.shuttingDown = true 215 | self.cf.notifyProvisionStopped( dev.udid ) 216 | } 217 | 218 | for _,dev := range self.DevMap { 219 | log.WithFields( log.Fields{ 220 | "type": "shutdown_device", 221 | "uuid": censorUuid( dev.udid ), 222 | } ).Info("Shutdown device") 223 | dev.shutdown() 224 | } 225 | 226 | for _,proc := range self.process { 227 | log.WithFields( log.Fields{ 228 | "type": "shutdown_proc", 229 | "proc": proc.name, 230 | "pid": proc.pid, 231 | } ).Info("Shutting down " + proc.name + " devproc") 232 | go func() { proc.Kill() }() 233 | } 234 | 235 | go func() { self.cfStop <- true }() 236 | } 237 | 238 | func (self *DeviceTracker) onDeviceConnect( uuid string, bdev BridgeDev ) (*Device){ 239 | log.WithFields( log.Fields{ 240 | "type": "dev_present", 241 | "uuid": censorUuid( uuid ), 242 | } ).Info("Device Present") 243 | 244 | dev := self.DevMap[ uuid ] 245 | if dev != nil { 246 | dev.connected = true 247 | return dev 248 | } 249 | dev = NewDevice( self.Config, self, uuid, bdev ) 250 | bdev.SetDevice( dev ) 251 | 252 | devInfo := getAllDeviceInfo( bdev ) 253 | log.WithFields( log.Fields{ 254 | "type": "dev_info_full", 255 | "uuid": censorUuid( uuid ), 256 | "info": devInfo, 257 | } ).Debug("Device Info") 258 | log.WithFields( log.Fields{ 259 | "type": "dev_info_basic", 260 | "uuid": censorUuid( uuid ), 261 | "ModelNumber": devInfo["ModelNumber"], 262 | "ProductType": devInfo["ProductType"], 263 | "ProductVersion": devInfo["ProductVersion"], 264 | } ).Info("Device Info") 265 | 266 | dev.info = devInfo 267 | dev.iosVersion = devInfo["ProductVersion"] 268 | versionParts := strings.Split( dev.iosVersion, "." ) 269 | 270 | majorStr := versionParts[0] 271 | dev.versionParts[0],_ = strconv.Atoi( majorStr ) 272 | 273 | if len( versionParts ) > 1 { 274 | medStr := versionParts[1] 275 | dev.versionParts[1],_ = strconv.Atoi( medStr ) 276 | } 277 | 278 | if len( versionParts ) > 2 { 279 | minStr := versionParts[2] 280 | dev.versionParts[2],_ = strconv.Atoi( minStr ) 281 | } 282 | 283 | self.DevMap[ uuid ] = dev 284 | return dev 285 | } 286 | 287 | func (self *DeviceTracker) onDeviceDisconnect( dev *Device ) { 288 | dev.connected = false 289 | } -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module main.go 2 | 3 | go 1.12 4 | 5 | //replace github.com/go-cmd/cmd => ../cmd 6 | //replace github.com/nanoscopic/ujsonin/v2 => ../ujsonin/v2 7 | 8 | require ( 9 | github.com/elastic/go-sysinfo v1.5.0 10 | github.com/go-cmd/cmd v1.3.0 11 | github.com/gorilla/websocket v1.4.2 12 | github.com/nanoscopic/uclop v1.1.0 13 | github.com/nanoscopic/ujsonin/v2 v2.0.6 14 | github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 15 | github.com/sirupsen/logrus v1.7.0 16 | go.nanomsg.org/mangos/v3 v3.1.3 17 | github.com/danielpaulus/go-ios v1.0.30 18 | ) 19 | -------------------------------------------------------------------------------- /http_server.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "net/http" 7 | "strconv" 8 | "strings" 9 | 10 | uj "github.com/nanoscopic/ujsonin/v2/mod" 11 | log "github.com/sirupsen/logrus" 12 | ) 13 | 14 | func coroHttpServer( devTracker *DeviceTracker ) { 15 | var listen_addr = fmt.Sprintf( "0.0.0.0:%d", devTracker.Config.httpPort ) 16 | startServer( devTracker, listen_addr ) 17 | } 18 | 19 | func startServer( devTracker *DeviceTracker, listen_addr string ) { 20 | log.WithFields( log.Fields{ 21 | "type": "http_start", 22 | } ).Debug("HTTP server started") 23 | 24 | frameClosure := func( w http.ResponseWriter, r *http.Request ) { 25 | onFrame( w, r, devTracker ) 26 | } 27 | backupFrameClosure := func( w http.ResponseWriter, r *http.Request ) { 28 | onBackupFrame( w, r, devTracker ) 29 | } 30 | 31 | http.HandleFunc( "/frame", frameClosure ) 32 | http.HandleFunc( "/backupFrame", backupFrameClosure ) 33 | 34 | err := http.ListenAndServe( listen_addr, nil ) 35 | log.WithFields( log.Fields{ 36 | "type": "http_server_fail", 37 | "error": err, 38 | } ).Debug("HTTP ListenAndServe Error") 39 | } 40 | 41 | func firstFrameJSON( devTracker *DeviceTracker, bytes []byte ) { 42 | root, _ := uj.Parse( bytes ) 43 | 44 | msgType := root.Get("type").String() 45 | 46 | if msgType == "frame1" { 47 | width := root.Get("width").Int() 48 | height := root.Get("height").Int() 49 | uuid := root.Get("uuid").String() 50 | devEvent := DevEvent{ 51 | action: DEV_VIDEO_START, 52 | width: width, 53 | height: height, 54 | } 55 | 56 | dev := devTracker.DevMap[ uuid ] 57 | dev.EventCh <- devEvent 58 | } 59 | } 60 | 61 | func onFrame( w http.ResponseWriter, r *http.Request, devTracker *DeviceTracker ) { 62 | body := new(bytes.Buffer) 63 | body.ReadFrom(r.Body) 64 | bytes := body.Bytes() 65 | str := string(bytes) 66 | i := strings.Index( str, "}" ) 67 | fmt.Printf("String to parse:%s\n", str[:i] ) 68 | 69 | firstFrameJSON( devTracker, bytes ) 70 | } 71 | 72 | func onBackupFrame( w http.ResponseWriter, r *http.Request, devTracker *DeviceTracker ) { 73 | r.ParseForm() 74 | udid := r.Form.Get("udid") 75 | if udid == "" { 76 | fmt.Fprintf(w, "Udid not set\n") 77 | return 78 | } 79 | 80 | fmt.Printf("Fetching backup frame: %s\n", udid ) 81 | 82 | dev := devTracker.getDevice( udid ) 83 | if dev == nil { 84 | w.Header().Set("Content-Type", "text/html") 85 | fmt.Fprintf(w, "Could not find device with udid: %s
", udid ) 86 | fmt.Fprintf(w, "Available UDID:
") 87 | for _, key := range devTracker.DevMap { 88 | fmt.Fprintf(w, "%s
", key ) 89 | } 90 | return 91 | } 92 | 93 | pngData, errText := dev.getBackupFrame() 94 | if errText != "" { 95 | fmt.Fprintf(w, "Error: %s
\n", errText ) 96 | return 97 | } 98 | 99 | if len( pngData ) == 0 { 100 | fmt.Fprintf(w, "No data from frame server\n" ) 101 | return 102 | } 103 | 104 | w.Header().Set("Content-Type", "image/png") 105 | w.Header().Set("Content-Length", strconv.Itoa( len( pngData ) ) ) 106 | w.Write( pngData ) 107 | } 108 | 109 | func deviceConnect( w http.ResponseWriter, r *http.Request, eventCh chan<- Event ) { 110 | // signal device loop of device connect 111 | r.ParseForm() 112 | uuid := r.Form.Get("uuid") 113 | fmt.Printf("Device connected: %s\n", uuid ) 114 | eventCh <- Event{ 115 | action: 0, 116 | uuid: uuid, 117 | } 118 | } 119 | 120 | func deviceDisconnect( w http.ResponseWriter, r *http.Request, eventCh chan<- Event ) { 121 | // signal device loop of device disconnect 122 | r.ParseForm() 123 | uuid := r.Form.Get("uuid") 124 | fmt.Printf("Device disconnected: %s\n", uuid ) 125 | eventCh <- Event{ 126 | action: 1, 127 | uuid: uuid, 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "os" 7 | "os/signal" 8 | //"runtime/pprof" 9 | "strings" 10 | "strconv" 11 | "syscall" 12 | "time" 13 | log "github.com/sirupsen/logrus" 14 | uc "github.com/nanoscopic/uclop/mod" 15 | "github.com/danielpaulus/go-ios/ios" 16 | ) 17 | 18 | func main() { 19 | uclop := uc.NewUclop() 20 | commonOpts := uc.OPTS{ 21 | uc.OPT("-debug","Use debug log level",uc.FLAG), 22 | uc.OPT("-warn","Use warn log level",uc.FLAG), 23 | uc.OPT("-config","Config file to use",0), 24 | uc.OPT("-defaults","Defaults config file to use",0), 25 | uc.OPT("-calculated","Path to calculated JSON values",0), 26 | uc.OPT("-cpuprofile","Output cpu profile data",uc.FLAG), 27 | } 28 | 29 | idOpt := uc.OPTS{ 30 | uc.OPT("-id","Udid of device",0), 31 | } 32 | 33 | runOpts := append( commonOpts, 34 | idOpt[0], 35 | uc.OPT("-nosanity","Skip sanity checks",uc.FLAG), 36 | ) 37 | 38 | uclop.AddCmd( "run", "Run ControlFloor", runMain, runOpts ) 39 | uclop.AddCmd( "register", "Register against ControlFloor", runRegister, commonOpts ) 40 | uclop.AddCmd( "cleanup", "Cleanup leftover processes", runCleanup, nil ) 41 | 42 | //uclop.AddCmd( "wda", "Just run WDA", runWDA, idOpt ) 43 | uclop.AddCmd( "cfa", "Just run CFA", runCFA, idOpt ) 44 | uclop.AddCmd( "winsize", "Get device window size", runWindowSize, idOpt ) 45 | uclop.AddCmd( "screenshot","Get screenshot", runScreenshot, idOpt ) 46 | uclop.AddCmd( "shottest", "Test video via screenshots", runShotTest, idOpt ) 47 | uclop.AddCmd( "at", "Activate assistiveTouch", runAt, idOpt ); 48 | 49 | sourceOpts := append( idOpt, 50 | uc.OPT("-bi","Bundle ID",0), 51 | uc.OPT("-pid","PID",0), 52 | ) 53 | uclop.AddCmd( "source", "Get device xml source", runSource, sourceOpts ) 54 | uclop.AddCmd( "wifiIp", "Get Wifi IP address", runWifiIp, idOpt ) 55 | uclop.AddCmd( "wifiMac", "Get Wifi Mac address", runWifiMac, idOpt ) 56 | uclop.AddCmd( "activeApps","Get pids of active apps", runActiveApps, idOpt ) 57 | uclop.AddCmd( "toLauncher","Return to launcher screen", runToLauncher, idOpt ) 58 | uclop.AddCmd( "alertinfo", "Get alert info", runAlertInfo, idOpt ) 59 | uclop.AddCmd( "islocked", "Check if device screen is locked", runIsLocked, idOpt ) 60 | uclop.AddCmd( "unlock", "Unlock device screen", runUnlock, idOpt ) 61 | uclop.AddCmd( "listen", "Test listening for devices", runListen, commonOpts ) 62 | 63 | clickButtonOpts := append( idOpt, 64 | uc.OPT("-label","Button label",uc.REQ), 65 | uc.OPT("-system","System element",uc.FLAG), 66 | ) 67 | uclop.AddCmd( "clickEl", "Click a named element", runClickEl, clickButtonOpts ) 68 | uclop.AddCmd( "forceTouchEl", "Force touch a named element", runForceTouchEl, clickButtonOpts ) 69 | uclop.AddCmd( "longTouchEl", "Long touch a named element", runLongTouchEl, clickButtonOpts ) 70 | uclop.AddCmd( "addRec", "Add Recording to Control Center", runAddRec, idOpt ) 71 | 72 | appAtOpts := append( idOpt, 73 | uc.OPT("-x","X",0), 74 | uc.OPT("-y","Y",0), 75 | ) 76 | uclop.AddCmd( "appAt", "App at point", runAppAtPoint, appAtOpts ) 77 | 78 | runAppOpts := append( idOpt, 79 | uc.OPT("-name","App name",uc.REQ), 80 | ) 81 | uclop.AddCmd( "runapp", "Run named app", runRunApp, runAppOpts ) 82 | 83 | siriOpts := append( idOpt, 84 | uc.OPT("-cmd","Siri command text",uc.REQ), 85 | ) 86 | uclop.AddCmd( "siri", "Run siri", runSiri, siriOpts ) 87 | 88 | elByPidOpts := append( idOpt, 89 | uc.OPT("-pid","PID",uc.REQ), 90 | ) 91 | uclop.AddCmd( "elByPid", "Get source of pid", runElByPid, elByPidOpts ) 92 | 93 | pidChildWithWidthOpts := append( idOpt, 94 | uc.OPT("-pid","PID",uc.REQ), 95 | uc.OPT("-width","With",uc.REQ), 96 | ) 97 | uclop.AddCmd( "pidChildWithWidth", "Get element that is a child of pid with specified width", runPidChildWithWidth, pidChildWithWidthOpts ) 98 | 99 | uclop.AddCmd( "vidtest", "Test backup video", runVidTest, idOpt ) 100 | 101 | uclop.Run() 102 | } 103 | 104 | func goIosGetOne( udid string, onDone func( ios.DeviceEntry ) ) { 105 | go func() { for { 106 | deviceConn, err := ios.NewDeviceConnection(ios.DefaultUsbmuxdSocket) 107 | defer deviceConn.Close() 108 | if err != nil { continue } 109 | muxConnection := ios.NewUsbMuxConnection(deviceConn) 110 | 111 | attachedReceiver, err := muxConnection.Listen() 112 | if err != nil { continue } 113 | 114 | for { 115 | msg, err := attachedReceiver() 116 | if err != nil { break } 117 | if msg.MessageType == "Attached" { 118 | audid := msg.Properties.SerialNumber 119 | if audid == udid { 120 | goIosDevice, _ := ios.GetDevice( udid ) 121 | fmt.Printf("Got it; id=%d\n",goIosDevice.DeviceID) 122 | 123 | onDone( goIosDevice ) 124 | } else { 125 | //fmt.Printf("%s != %s\n", audid, udid ) 126 | } 127 | } 128 | } 129 | time.Sleep( time.Second * 10 ) 130 | } }() 131 | } 132 | 133 | func cfaForDev( id string ) (*CFA,*DeviceTracker,*Device) { 134 | config := NewConfig( "config.json", "default.json", "calculated.json" ) 135 | 136 | tracker := NewDeviceTracker( config, false, []string{} ) 137 | 138 | devs := tracker.bridge.GetDevs( config ) 139 | dev1 := id 140 | if id == "" { 141 | dev1 = devs[0] 142 | } 143 | fmt.Printf("Dev id: %s\n", dev1) 144 | 145 | var bridgeDev BridgeDev 146 | if config.bridge == "go-ios" { 147 | bridgeDev = NewGIDev( tracker.bridge.(*GIBridge), dev1, "x", nil ) 148 | 149 | /*entry := ios.DeviceEntry{ 150 | DeviceID: 1, 151 | Properties: ios.DeviceProperties{ 152 | SerialNumber: dev1, 153 | }, 154 | } 155 | bridgeDev.SetCustom( "goIosdevice", entry )*/ 156 | 157 | /*wait := make( chan bool ) 158 | 159 | goIosGetOne( dev1, func( goIosDevice ios.DeviceEntry ) { 160 | bridgeDev.SetCustom( "goIosdevice", goIosDevice ) 161 | wait <- true 162 | } ) 163 | 164 | <- wait*/ 165 | } else { 166 | bridgeDev = NewIIFDev( tracker.bridge.(*IIFBridge), dev1, "x", nil ) 167 | } 168 | dev := NewDevice( config, tracker, dev1, bridgeDev ) 169 | bridgeDev.SetDevice( dev ) 170 | 171 | devConfig, hasDevConfig := config.devs[ dev1 ] 172 | if hasDevConfig { 173 | bridgeDev.SetConfig( &devConfig ) 174 | } 175 | 176 | bridgeDev.setProcTracker( tracker ) 177 | cfa := NewCFANoStart( config, tracker, dev ) 178 | dev.cfa = cfa 179 | return cfa,tracker,dev 180 | } 181 | 182 | func vidTestForDev( id string ) (*DeviceTracker) { 183 | config := NewConfig( "config.json", "default.json", "calculated.json" ) 184 | 185 | tracker := NewDeviceTracker( config, false, []string{} ) 186 | 187 | devs := tracker.bridge.GetDevs( config ) 188 | dev1 := id 189 | if id == "" { 190 | dev1 = devs[0] 191 | } 192 | fmt.Printf("Dev id: %s\n", dev1) 193 | 194 | var bridgeDev BridgeDev 195 | if config.bridge == "go-ios" { 196 | bridgeDev = NewGIDev( tracker.bridge.(*GIBridge), dev1, "x", nil ) 197 | } else { 198 | bridgeDev = NewIIFDev( tracker.bridge.(*IIFBridge), dev1, "x", nil ) 199 | } 200 | dev := NewDevice( config, tracker, dev1, bridgeDev ) 201 | bridgeDev.SetDevice( dev ) 202 | 203 | devConfig, hasDevConfig := config.devs[ dev1 ] 204 | if hasDevConfig { 205 | bridgeDev.SetConfig( &devConfig ) 206 | } 207 | 208 | tracker.DevMap[ dev1 ] = dev 209 | 210 | bridgeDev.setProcTracker( tracker ) 211 | 212 | dev.startBackupVideo() 213 | 214 | coroHttpServer( tracker ) 215 | 216 | return tracker 217 | } 218 | 219 | /*func runWDA( cmd *uc.Cmd ) { 220 | runCleanup( cmd ) 221 | 222 | id := "" 223 | idNode := cmd.Get("-id") 224 | if idNode != nil { 225 | id = idNode.String() 226 | } 227 | 228 | wda,tracker,_ := wdaForDev( id ) 229 | wda.start( nil ) 230 | 231 | dotLoop( cmd, tracker ) 232 | }*/ 233 | 234 | func runCFA( cmd *uc.Cmd ) { 235 | runCleanup( cmd ) 236 | 237 | id := "" 238 | idNode := cmd.Get("-id") 239 | if idNode != nil { 240 | id = idNode.String() 241 | } 242 | 243 | cfa,tracker,_ := cfaForDev( id ) 244 | cfa.start( nil ) 245 | 246 | dotLoop( cmd, tracker ) 247 | } 248 | 249 | func runVidTest( cmd *uc.Cmd ) { 250 | runCleanup( cmd ) 251 | 252 | id := "" 253 | idNode := cmd.Get("-id") 254 | if idNode != nil { 255 | id = idNode.String() 256 | } 257 | 258 | tracker := vidTestForDev( id ) 259 | 260 | dotLoop( cmd, tracker ) 261 | } 262 | 263 | func dotLoop( cmd *uc.Cmd, tracker *DeviceTracker ) { 264 | c := make(chan os.Signal) 265 | stop := make(chan bool) 266 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 267 | go func() { 268 | <- c 269 | stop <- true 270 | tracker.shutdown() 271 | }() 272 | 273 | exit := 0 274 | for { 275 | select { 276 | case <- stop: 277 | exit = 1 278 | break 279 | default: 280 | } 281 | if exit == 1 { break } 282 | fmt.Printf(". ") 283 | time.Sleep( time.Second * 1 ) 284 | } 285 | 286 | runCleanup( cmd ) 287 | } 288 | 289 | func runWindowSize( cmd *uc.Cmd ) { 290 | cfaWrapped( cmd, "", func( cfa *CFA, dev *Device ) { 291 | wid, heg := cfa.WindowSize() 292 | fmt.Printf("Width: %d, Height: %d\n", wid, heg ) 293 | } ) 294 | } 295 | 296 | func runAddRec( cmd *uc.Cmd ) { 297 | cfaWrapped( cmd, "", func( cfa *CFA, dev *Device ) { 298 | cfa.AddRecordingToCC() 299 | } ) 300 | } 301 | 302 | func cfaWrapped( cmd *uc.Cmd, appName string, doStuff func( cfa *CFA, dev *Device ) ) { 303 | config := NewConfig( "config.json", "default.json", "calculated.json" ) 304 | 305 | runCleanup( cmd ) 306 | 307 | id := "" 308 | idNode := cmd.Get("-id") 309 | if idNode != nil { 310 | id = idNode.String() 311 | } 312 | 313 | if id == "" { 314 | tracker := NewDeviceTracker( config, false, []string{} ) 315 | devs := tracker.bridge.GetDevs( config ) 316 | id = devs[0] 317 | } 318 | 319 | cfa,_,dev := cfaForDev( id ) 320 | fmt.Printf("id:[%s]\n", id ) 321 | devConfig := config.devs[ id ] 322 | fmt.Printf("%+v\n", devConfig ) 323 | 324 | startChan := make( chan int ) 325 | 326 | fmt.Printf("devCfaMethod:%s\n", devConfig.cfaMethod ) 327 | var stopChan chan bool 328 | if config.cfaMethod == "manual" || devConfig.cfaMethod == "manual" { 329 | fmt.Printf("Manual CFA; connecting...\n") 330 | go func() { 331 | cfa.startCfaNng( func( err int, AstopChan chan bool ) { 332 | stopChan = AstopChan 333 | fmt.Printf("Manual CFA; connected; err: %d\n", err) 334 | startChan <- err 335 | } ) 336 | }() 337 | } else { 338 | //cfa.startChan = startChan 339 | cfa.start( func( err int, AstopChan chan bool ) { 340 | stopChan = AstopChan 341 | startChan <- err 342 | } ) 343 | } 344 | 345 | err := <- startChan 346 | if err != 0 { 347 | fmt.Printf("Could not start/connect to CFA. Exiting") 348 | runCleanup( cmd ) 349 | return 350 | } 351 | 352 | fmt.Printf("appName = %s\n", appName) 353 | if appName == "" { 354 | fmt.Printf("Ensuring session\n") 355 | //cfa.ensureSession() 356 | fmt.Printf("Ensured session\n") 357 | } else { 358 | //cfa.create_session( appName ) 359 | } 360 | 361 | doStuff( cfa, dev ) 362 | 363 | stopChan <- true 364 | 365 | dev.shutdown() 366 | cfa.stop() 367 | 368 | runCleanup( cmd ) 369 | } 370 | 371 | func runClickEl( cmd *uc.Cmd ) { 372 | cfaWrapped( cmd, "", func( cfa *CFA, dev *Device ) { 373 | label := cmd.Get("-label").String() 374 | system := cmd.Get("-system").Bool() 375 | btnName := cfa.GetEl( "any", label, system, 5 ) 376 | cfa.ElClick( btnName ) 377 | } ) 378 | } 379 | 380 | func runForceTouchEl( cmd *uc.Cmd ) { 381 | cfaWrapped( cmd, "", func( cfa *CFA, dev *Device ) { 382 | label := cmd.Get("-label").String() 383 | system := cmd.Get("-system").Bool() 384 | btnName := cfa.GetEl( "any", label, system, 5 ) 385 | cfa.ElForceTouch( btnName, 1 ) 386 | } ) 387 | } 388 | 389 | func runLongTouchEl( cmd *uc.Cmd ) { 390 | cfaWrapped( cmd, "", func( cfa *CFA, dev *Device ) { 391 | label := cmd.Get("-label").String() 392 | system := cmd.Get("-system").Bool() 393 | btnName := cfa.GetEl( "any", label, system, 5 ) 394 | cfa.ElLongTouch( btnName ) 395 | } ) 396 | } 397 | 398 | func runRunApp( cmd *uc.Cmd ) { 399 | appName := cmd.Get("-name").String() 400 | cfaWrapped( cmd, appName, func( cfa *CFA, dev *Device ) { 401 | } ) 402 | } 403 | 404 | func runSource( cmd *uc.Cmd ) { 405 | bi := cmd.Get("-bi").String() 406 | pidStr := cmd.Get("-pid").String() 407 | pid := 0 408 | if pidStr != "" { 409 | pid, _ = strconv.Atoi( pidStr ) 410 | } 411 | cfaWrapped( cmd, "", func( cfa *CFA, dev *Device ) { 412 | xml := cfa.Source(bi,pid) 413 | fmt.Println( xml ) 414 | } ) 415 | } 416 | 417 | func runScreenshot( cmd *uc.Cmd ) { 418 | cfaWrapped( cmd, "", func( cfa *CFA, dev *Device ) { 419 | bytes := cfa.Screenshot() 420 | //os.Stdout.Write( bytes ) 421 | f, _ := os.Create( "test.jpg" ) 422 | f.Write( bytes ) 423 | fmt.Printf("Write %d bytes\n", len( bytes ) ) 424 | } ) 425 | } 426 | 427 | func runShotTest( cmd *uc.Cmd ) { 428 | cfaWrapped( cmd, "", shotServer ) 429 | } 430 | 431 | func shotServer( cfa *CFA, dev *Device ) { 432 | shotClosure := func( w http.ResponseWriter, r *http.Request ) { 433 | shotImg( w, r, cfa ) 434 | } 435 | http.HandleFunc( "/shot", shotClosure ) 436 | http.HandleFunc( "/", shotRoot ) 437 | http.ListenAndServe( "0.0.0.0:8081", nil ) 438 | } 439 | 440 | func shotRoot( w http.ResponseWriter, r *http.Request ) { 441 | w.Write( []byte(` 442 | 443 | 444 | 456 | 457 | 458 | 459 | 460 | 461 | `)) 462 | } 463 | 464 | func shotImg( w http.ResponseWriter, r *http.Request, cfa *CFA ) { 465 | bytes := cfa.Screenshot() 466 | w.Header().Set("Content-Type", "image/jpeg") 467 | w.Header().Set("Content-Length", strconv.Itoa( len( bytes ) ) ) 468 | w.Header().Set("Cache-Control", "no-cache, must-revalidate" ) 469 | 470 | w.Write( bytes ) 471 | } 472 | 473 | func runAlertInfo( cmd *uc.Cmd ) { 474 | cfaWrapped( cmd, "", func( cfa *CFA, dev *Device ) { 475 | _, json := cfa.AlertInfo() 476 | fmt.Println( json ) 477 | } ) 478 | } 479 | 480 | func runWifiIp( cmd *uc.Cmd ) { 481 | cfaWrapped( cmd, "", func( cfa *CFA, dev *Device ) { 482 | ip := cfa.WifiIp() 483 | fmt.Println( ip ) 484 | } ) 485 | } 486 | 487 | func runSiri( cmd *uc.Cmd ) { 488 | cmdT := cmd.Get("-cmd").String() 489 | cfaWrapped( cmd, "", func( cfa *CFA, dev *Device ) { 490 | cfa.Siri(cmdT) 491 | } ) 492 | } 493 | 494 | func runToLauncher( cmd *uc.Cmd ) { 495 | cfaWrapped( cmd, "", func( cfa *CFA, dev *Device ) { 496 | cfa.ToLauncher() 497 | } ) 498 | } 499 | 500 | func runElByPid( cmd *uc.Cmd ) { 501 | pid := cmd.Get("-pid").Int() 502 | cfaWrapped( cmd, "", func( cfa *CFA, dev *Device ) { 503 | source := cfa.ElByPid(pid,true) 504 | fmt.Println(source) 505 | } ) 506 | } 507 | 508 | func runPidChildWithWidth( cmd *uc.Cmd ) { 509 | pid := cmd.Get("-pid").Int() 510 | width := cmd.Get("-width").Int() 511 | 512 | cfaWrapped( cmd, "", func( cfa *CFA, dev *Device ) { 513 | source := cfa.PidChildWithWidth(pid,width) 514 | fmt.Println(source) 515 | } ) 516 | } 517 | 518 | func runAppAtPoint( cmd *uc.Cmd ) { 519 | x := cmd.Get("-x").Int() 520 | y := cmd.Get("-y").Int() 521 | cfaWrapped( cmd, "", func( cfa *CFA, dev *Device ) { 522 | app := cfa.AppAtPoint(x,y,true,false,true) 523 | fmt.Println( app ) 524 | } ) 525 | } 526 | 527 | func runWifiMac( cmd *uc.Cmd ) { 528 | cfaWrapped( cmd, "", func( cfa *CFA, dev *Device ) { 529 | ip := dev.WifiMac() 530 | fmt.Println( ip ) 531 | } ) 532 | } 533 | 534 | func runActiveApps( cmd *uc.Cmd ) { 535 | cfaWrapped( cmd, "", func( cfa *CFA, dev *Device ) { 536 | ids := cfa.ActiveApps() 537 | fmt.Println( ids ) 538 | } ) 539 | } 540 | 541 | func runAt( cmd *uc.Cmd ) { 542 | cfaWrapped( cmd, "", func( cfa *CFA, dev *Device ) { 543 | //cfa.AT() 544 | 545 | /*cfa.Siri("is assistivetouch active") 546 | el := cfa.GetEl("any","AssistiveTouch",false,300) 547 | cfa.ElClick(el) 548 | cfa.home()*/ 549 | 550 | /*cfa.Siri("activate assistivetouch") 551 | time.Sleep( time.Millisecond * 600 ) 552 | cfa.home()*/ 553 | dev.taskSwitcher() 554 | } ) 555 | } 556 | 557 | func runIsLocked( cmd *uc.Cmd ) { 558 | cfaWrapped( cmd, "", func( cfa *CFA, dev *Device ) { 559 | locked := cfa.IsLocked() 560 | if locked { 561 | fmt.Println("Device screen is locked") 562 | } else { 563 | fmt.Println("Device screen is unlocked") 564 | } 565 | } ) 566 | } 567 | 568 | func runUnlock( cmd *uc.Cmd ) { 569 | cfaWrapped( cmd, "", func( cfa *CFA, dev *Device ) { 570 | //cfa.Unlock() 571 | cfa.ioHid( 0x0c, 0x30 ) // power 572 | //time.Sleep(time.Second) 573 | //cfa.ioHid( 0x07, 0x4a ) // home keyboard button 574 | cfa.Unlock() 575 | } ) 576 | } 577 | 578 | func runListen( cmd *uc.Cmd ) { 579 | stopChan := make( chan bool ) 580 | listenForDevices( stopChan, 581 | func( id string, goIosDevice ios.DeviceEntry ) { 582 | fmt.Printf("Connected %s\n", id ) 583 | }, 584 | func( id string ) { 585 | fmt.Printf("Disconnected %s\n", id ) 586 | } ) 587 | 588 | c := make(chan os.Signal, syscall.SIGTERM) 589 | signal.Notify(c, os.Interrupt) 590 | <-c 591 | } 592 | 593 | func common( cmd *uc.Cmd ) *Config { 594 | debug := cmd.Get("-debug").Bool() 595 | warn := cmd.Get("-warn").Bool() 596 | 597 | configPath := cmd.Get("-config").String() 598 | if configPath == "" { configPath = "config.json" } 599 | 600 | defaultsPath := cmd.Get("-defaults").String() 601 | if defaultsPath == "" { defaultsPath = "default.json" } 602 | 603 | calculatedPath := cmd.Get("-calculated").String() 604 | if calculatedPath == "" { calculatedPath = "calculated.json" } 605 | 606 | setupLog( debug, warn ) 607 | 608 | config := NewConfig( configPath, defaultsPath, calculatedPath ) 609 | config.cpuProfile = cmd.Get("-cpuprofile").Bool() 610 | return config 611 | } 612 | 613 | func runCleanup( *uc.Cmd ) { 614 | config := NewConfig( "config.json", "default.json", "calculated.json" ) 615 | cleanup_procs( config ) 616 | } 617 | 618 | func runRegister( cmd *uc.Cmd ) { 619 | config := common( cmd ) 620 | 621 | doregister( config ) 622 | } 623 | 624 | func runMain( cmd *uc.Cmd ) { 625 | config := common( cmd ) 626 | 627 | // This seems to do nothing... what gives 628 | /*if config.cpuProfile { 629 | f, _ := os.Create("cpuprofile") 630 | if err == nil { 631 | pprof.StartCPUProfile( f ) 632 | defer pprof.StopCPUProfile() 633 | } 634 | }*/ 635 | 636 | idNode := cmd.Get("-id") 637 | ids := []string{} 638 | if idNode != nil { 639 | idString := idNode.String() 640 | if idString != "" { 641 | ids = strings.Split( idString, "," ) 642 | config.idList = ids 643 | } 644 | } 645 | 646 | cleanup_procs( config ) 647 | 648 | nosanity := cmd.Get("-nosanity").Bool() 649 | if !nosanity { 650 | sane := sanityChecks( config, cmd ) 651 | if !sane { 652 | fmt.Printf("Sanity checks failed. Exiting\n") 653 | return 654 | } 655 | } 656 | 657 | devTracker := NewDeviceTracker( config, true, ids ) 658 | coro_sigterm( config, devTracker ) 659 | 660 | coroHttpServer( devTracker ) 661 | } 662 | 663 | func setupLog( debug bool, warn bool ) { 664 | //log.SetFormatter(&log.JSONFormatter{}) 665 | log.SetFormatter(&log.TextFormatter{ 666 | DisableTimestamp: true, 667 | }) 668 | log.SetOutput(os.Stdout) 669 | if debug { 670 | log.SetLevel( log.DebugLevel ) 671 | } else if warn { 672 | log.SetLevel( log.WarnLevel ) 673 | } else { 674 | log.SetLevel( log.InfoLevel ) 675 | } 676 | } 677 | 678 | func censorUuid( uuid string ) (string) { 679 | return "***" + uuid[len(uuid)-4:] 680 | } -------------------------------------------------------------------------------- /proc_backoff.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "time" 4 | 5 | type Backoff struct { 6 | fails int 7 | start time.Time 8 | elapsedSeconds float64 9 | } 10 | 11 | func ( self *Backoff ) markStart() { 12 | self.start = time.Now() 13 | } 14 | 15 | func ( self *Backoff ) markEnd() ( float64 ) { 16 | elapsed := time.Since( self.start ) 17 | seconds := elapsed.Seconds() 18 | self.elapsedSeconds = seconds 19 | return seconds 20 | } 21 | 22 | func ( self *Backoff ) wait() { 23 | sleeps := []int{ 0, 0, 2, 5, 10 } 24 | numSleeps := len( sleeps ) 25 | if self.elapsedSeconds < 20 { 26 | self.fails = self.fails + 1 27 | index := self.fails 28 | if index >= numSleeps { 29 | index = numSleeps - 1 30 | } 31 | sleepLen := sleeps[ index ] 32 | if sleepLen != 0 { 33 | time.Sleep( time.Second * time.Duration( sleepLen ) ) 34 | } 35 | } else { 36 | self.fails = 0 37 | } 38 | } -------------------------------------------------------------------------------- /proc_generic.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | log "github.com/sirupsen/logrus" 6 | gocmd "github.com/go-cmd/cmd" 7 | "os" 8 | "time" 9 | ) 10 | 11 | type OutputHandler func( string, *log.Entry ) 12 | 13 | type ProcOptions struct { 14 | dev *Device 15 | procName string 16 | binary string 17 | args []string 18 | stderrHandler OutputHandler 19 | stdoutHandler OutputHandler 20 | startFields log.Fields 21 | startDir string 22 | env map[string]string 23 | noRestart bool 24 | noWait bool 25 | onStop func( interface{} ) 26 | } 27 | 28 | type ProcTracker interface { 29 | startProc( proc *GenericProc ) 30 | stopProc( procName string ) 31 | isShuttingDown() bool 32 | } 33 | 34 | type GPMsg struct { 35 | msgType int 36 | } 37 | 38 | type GenericProc struct { 39 | name string 40 | controlCh chan GPMsg 41 | backoff *Backoff 42 | pid int 43 | cmd *gocmd.Cmd 44 | } 45 | 46 | func (self *GenericProc) Kill() { 47 | if self.cmd == nil { return } 48 | self.controlCh <- GPMsg{ msgType: 1 } 49 | } 50 | 51 | func (self *GenericProc) Restart() { 52 | if self.cmd == nil { return } 53 | self.controlCh <- GPMsg{ msgType: 2 } 54 | } 55 | 56 | func restart_proc_generic( dev *Device, name string ) { 57 | genProc := dev.process[ name ] 58 | genProc.Restart() 59 | } 60 | 61 | func proc_generic( procTracker ProcTracker, wrapper interface{}, opt *ProcOptions ) ( *GenericProc ) { 62 | controlCh := make( chan GPMsg ) 63 | proc := GenericProc { 64 | controlCh: controlCh, 65 | name: opt.procName, 66 | } 67 | 68 | var plog *log.Entry 69 | 70 | plog = log.WithFields( log.Fields{ "proc": opt.procName } ) 71 | 72 | if procTracker != nil { 73 | procTracker.startProc( &proc ) 74 | } else { 75 | panic("procTracker not set") 76 | } 77 | 78 | backoff := Backoff{} 79 | proc.backoff = &backoff 80 | 81 | stop := false 82 | 83 | if opt.binary == "" { 84 | fmt.Printf("Binary not set\n") 85 | } 86 | 87 | _, ferr := os.Stat( opt.binary ) 88 | if ferr != nil { 89 | plog.WithFields( log.Fields{ 90 | "type": "proc_bin_missing", 91 | "error": ferr, 92 | "path": opt.binary, 93 | } ).Fatal("Binary path does not exist. Cannot start process") 94 | } 95 | 96 | startFields := log.Fields{ 97 | "type": "proc_start", 98 | "binary": opt.binary, 99 | } 100 | if opt.startFields != nil { 101 | for k, v := range opt.startFields { 102 | if v == nil { 103 | fmt.Printf("%s not set\n", k ) 104 | } 105 | //fmt.Printf("%s = %s\n", k, v ) 106 | startFields[k] = v 107 | } 108 | } 109 | 110 | plog.WithFields( log.Fields{ 111 | "type": "proc_fields", 112 | "fields": startFields, 113 | } ).Debug("Process starting fields") 114 | 115 | go func() { for { 116 | plog.WithFields( startFields ).Info("Process start - " + opt.procName) 117 | 118 | cmd := gocmd.NewCmdOptions( gocmd.Options{ Buffered: false, Streaming: true }, opt.binary, opt.args... ) 119 | proc.cmd = cmd 120 | 121 | if opt.startDir != "" { 122 | cmd.Dir = opt.startDir 123 | } 124 | 125 | if opt.env != nil { 126 | var envArr []string 127 | for k,v := range( opt.env ) { 128 | envArr = append( envArr, fmt.Sprintf("%s=%s", k, v ) ) 129 | } 130 | cmd.Env = envArr 131 | } 132 | 133 | backoff.markStart() 134 | 135 | statCh := cmd.Start() 136 | 137 | i := 0 138 | for { 139 | status := cmd.Status() 140 | 141 | if status.Error != nil { 142 | errStream := cmd.Stderr 143 | 144 | errText := "" 145 | for { 146 | select { 147 | case line, _ := <- errStream: 148 | errText = errText + line 149 | default: 150 | break 151 | } 152 | } 153 | 154 | plog.WithFields( log.Fields{ 155 | "type": "proc_err", 156 | "error": status.Error, 157 | "text": errText, 158 | } ).Error("Error starting - " + opt.procName) 159 | 160 | return 161 | } 162 | 163 | if status.Exit != -1 { 164 | errStream := cmd.Stderr 165 | errText := "" 166 | for { 167 | select { 168 | case line, _ := <- errStream: 169 | errText = errText + line 170 | default: 171 | break 172 | } 173 | } 174 | 175 | plog.WithFields( log.Fields{ 176 | "type": "proc_exit", 177 | "exit": status.Exit, 178 | "args": opt.args, 179 | "text": errText, 180 | } ).Error("Error starting - " + opt.procName) 181 | 182 | return 183 | } 184 | 185 | proc.pid = status.PID 186 | if proc.pid != 0 { 187 | break 188 | } 189 | time.Sleep(50 * time.Millisecond) 190 | if i > 4 { 191 | break 192 | } 193 | } 194 | 195 | plog.WithFields( log.Fields{ 196 | "type": "proc_pid", 197 | "pid": proc.pid, 198 | } ).Debug("Process pid") 199 | 200 | outStream := cmd.Stdout 201 | errStream := cmd.Stderr 202 | 203 | runDone := false 204 | for { 205 | select { 206 | case <- statCh: 207 | runDone = true 208 | case msg := <- controlCh: 209 | plog.Debug("Got stop request on control channel") 210 | if msg.msgType == 1 { // stop 211 | stop = true 212 | proc.cmd.Stop() 213 | } else if msg.msgType == 2 { // restart 214 | proc.cmd.Stop() 215 | } 216 | case line, _ := <- outStream: 217 | if line == "" { continue } 218 | if opt.stdoutHandler != nil { 219 | opt.stdoutHandler( line, plog ) 220 | } else { 221 | plog.WithFields( log.Fields{ "line": line } ).Info("") 222 | } 223 | case line, _ := <- errStream: 224 | if opt.stderrHandler != nil { 225 | opt.stderrHandler( line, plog ) 226 | } else { 227 | if line != "" { 228 | plog.WithFields( log.Fields{ "line": line, "iserr": true } ).Info("") 229 | } 230 | } 231 | } 232 | if runDone { break } 233 | } 234 | 235 | proc.cmd = nil 236 | 237 | backoff.markEnd() 238 | 239 | plog.WithFields( log.Fields{ "type": "proc_end" } ).Warn("Process end - "+ opt.procName) 240 | 241 | if opt.onStop != nil { 242 | opt.onStop( wrapper ) 243 | } 244 | 245 | if opt.noRestart { 246 | plog.Debug( "No restart requested" ) 247 | break 248 | } 249 | 250 | if stop { break } 251 | 252 | if !opt.noWait { 253 | backoff.wait() 254 | } else { 255 | plog.Debug("No wait requested") 256 | } 257 | 258 | if procTracker.isShuttingDown() { break } 259 | } }() 260 | 261 | return &proc 262 | } -------------------------------------------------------------------------------- /python/configure_cfa.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from pbxproj import XcodeProject 3 | import json 4 | 5 | jsonPath = "muxed.json" 6 | with open(jsonPath) as f: 7 | conf = json.load(f) 8 | 9 | project = XcodeProject.load('./repos/CFAgent/CFAgent.xcodeproj/project.pbxproj') 10 | 11 | def getflag(target,flag): 12 | for conf in project.objects.get_configurations_on_targets(target, "Debug"): 13 | cdict = conf["buildSettings"] 14 | return cdict[flag] 15 | 16 | def removeflag(target,flag): 17 | val = "" 18 | for conf in project.objects.get_configurations_on_targets(target, "Debug"): 19 | cdict = conf["buildSettings"] 20 | val = cdict[flag] 21 | if val is None: 22 | return 23 | project.remove_flags(flag, val, target, "Debug") 24 | 25 | l = "CFAgentLib" 26 | r = "CFAgent" 27 | 28 | cfa = conf["cfa"] 29 | idPrefix = cfa["bundleIdPrefix"] 30 | project.set_flags('DEVELOPMENT_TEAM', cfa["devTeamOu"], r, "Debug") 31 | project.set_flags('DEVELOPMENT_TEAM', cfa["devTeamOu"], l, "Debug") 32 | project.set_flags('CODE_SIGN_STYLE', cfa["lib"]["buildStyle"], l, "Debug") 33 | project.set_flags('CODE_SIGN_STYLE', cfa["runner"]["buildStyle"], r, "Debug") 34 | project.set_flags('PRODUCT_BUNDLE_IDENTIFIER', idPrefix + ".CFAgentLib", l, "Debug") 35 | project.set_flags('PRODUCT_BUNDLE_IDENTIFIER', idPrefix + ".CFAgent", r, "Debug") 36 | 37 | lProv = cfa["lib"]["provisioningProfile"] 38 | rProv = cfa["runner"]["provisioningProfile"] 39 | 40 | if lProv == "": 41 | removeflag(l, 'PROVISIONING_PROFILE_SPECIFIER') 42 | else: 43 | project.set_flags('PROVISIONING_PROFILE_SPECIFIER', lProv, l, "Debug") 44 | 45 | if rProv == "": 46 | removeflag(r, 'PROVISIONING_PROFILE_SPECIFIER') 47 | else: 48 | project.set_flags('PROVISIONING_PROFILE_SPECIFIER', rProv, r, "Debug") 49 | 50 | print("Lib:") 51 | print(" Style : " + ( getflag(l, "CODE_SIGN_STYLE") or "nil" ) ) 52 | print(" Dev Team : " + ( getflag(l, "DEVELOPMENT_TEAM") or "nil" ) ) 53 | print(" Bundle ID: " + getflag(l, "PRODUCT_BUNDLE_IDENTIFIER") ) 54 | print(" Prov Prof: " + ( getflag(l, "PROVISIONING_PROFILE_SPECIFIER") or "nil" ) ) 55 | 56 | print("Runner:") 57 | print(" Style : " + ( getflag(r, "CODE_SIGN_STYLE") or "nil" ) ) 58 | print(" Dev Team : " + ( getflag(r, "DEVELOPMENT_TEAM") or "nil" ) ) 59 | print(" Bundle ID: " + getflag(r, "PRODUCT_BUNDLE_IDENTIFIER") ) 60 | print(" Prov Prof: " + ( getflag(r, "PROVISIONING_PROFILE_SPECIFIER") or "nil" ) ) 61 | 62 | project.save() 63 | -------------------------------------------------------------------------------- /python/configure_vidstream.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from pbxproj import XcodeProject 3 | import json 4 | 5 | jsonPath = "muxed.json" 6 | with open(jsonPath) as f: 7 | conf = json.load(f) 8 | 9 | project = XcodeProject.load('./repos/vidapp/vidstream/vidstream.xcodeproj/project.pbxproj') 10 | 11 | mode = "Release" 12 | 13 | def getflag(target,flag): 14 | for conf in project.objects.get_configurations_on_targets(target, mode): 15 | cdict = conf["buildSettings"] 16 | return cdict[flag] 17 | 18 | def removeflag(target,flag): 19 | val = "" 20 | for conf in project.objects.get_configurations_on_targets(target, mode): 21 | cdict = conf["buildSettings"] 22 | val = cdict[flag] 23 | if val is None: 24 | return 25 | project.remove_flags(flag, val, target, "Debug") 26 | 27 | l = "vidstream" 28 | r = "vidstream_ext" 29 | 30 | vidstream = conf["vidapp"] 31 | idPrefix = vidstream["bundleIdPrefix"] 32 | project.set_flags('DEVELOPMENT_TEAM', vidstream["devTeamOu"], r, mode) 33 | project.set_flags('DEVELOPMENT_TEAM', vidstream["devTeamOu"], l, mode) 34 | project.set_flags('CODE_SIGN_STYLE', vidstream["main"]["buildStyle"], l, mode) 35 | project.set_flags('CODE_SIGN_STYLE', vidstream["extension"]["buildStyle"], r, mode) 36 | project.set_flags('PRODUCT_BUNDLE_IDENTIFIER', idPrefix + ".vidstream_ext", l, mode) 37 | project.set_flags('PRODUCT_BUNDLE_IDENTIFIER', idPrefix + ".vidstream_ext.extension", r, mode) 38 | 39 | lProv = vidstream["main"]["provisioningProfile"] 40 | rProv = vidstream["extension"]["provisioningProfile"] 41 | 42 | if lProv == "": 43 | removeflag(l, 'PROVISIONING_PROFILE_SPECIFIER') 44 | else: 45 | project.set_flags('PROVISIONING_PROFILE_SPECIFIER', lProv, l, mode) 46 | 47 | if rProv == "": 48 | removeflag(r, 'PROVISIONING_PROFILE_SPECIFIER') 49 | else: 50 | project.set_flags('PROVISIONING_PROFILE_SPECIFIER', rProv, r, mode) 51 | 52 | print("vidstream:") 53 | print(" Style : " + ( getflag(l, "CODE_SIGN_STYLE") or "nil" ) ) 54 | print(" Dev Team : " + ( getflag(l, "DEVELOPMENT_TEAM") or "nil" ) ) 55 | print(" Bundle ID: " + getflag(l, "PRODUCT_BUNDLE_IDENTIFIER") ) 56 | print(" Prov Prof: " + ( getflag(l, "PROVISIONING_PROFILE_SPECIFIER") or "nil" ) ) 57 | 58 | print("vidstream_ext:") 59 | print(" Style : " + ( getflag(r, "CODE_SIGN_STYLE") or "nil" ) ) 60 | print(" Dev Team : " + ( getflag(r, "DEVELOPMENT_TEAM") or "nil" ) ) 61 | print(" Bundle ID: " + getflag(r, "PRODUCT_BUNDLE_IDENTIFIER") ) 62 | print(" Prov Prof: " + ( getflag(r, "PROVISIONING_PROFILE_SPECIFIER") or "nil" ) ) 63 | 64 | project.save() -------------------------------------------------------------------------------- /python/configure_wda.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | from pbxproj import XcodeProject 3 | import json 4 | 5 | jsonPath = "muxed.json" 6 | with open(jsonPath) as f: 7 | conf = json.load(f) 8 | 9 | project = XcodeProject.load('./repos/WebDriverAgent/WebDriverAgent.xcodeproj/project.pbxproj') 10 | 11 | def getflag(target,flag): 12 | for conf in project.objects.get_configurations_on_targets(target, "Debug"): 13 | cdict = conf["buildSettings"] 14 | return cdict[flag] 15 | 16 | def removeflag(target,flag): 17 | val = "" 18 | for conf in project.objects.get_configurations_on_targets(target, "Debug"): 19 | cdict = conf["buildSettings"] 20 | val = cdict[flag] 21 | if val is None: 22 | return 23 | project.remove_flags(flag, val, target, "Debug") 24 | 25 | l = "WebDriverAgentLib" 26 | r = "WebDriverAgentRunner" 27 | 28 | wda = conf["wda"] 29 | idPrefix = wda["bundleIdPrefix"] 30 | project.set_flags('DEVELOPMENT_TEAM', wda["devTeamOu"], r, "Debug") 31 | project.set_flags('DEVELOPMENT_TEAM', wda["devTeamOu"], l, "Debug") 32 | project.set_flags('CODE_SIGN_STYLE', wda["lib"]["buildStyle"], l, "Debug") 33 | project.set_flags('CODE_SIGN_STYLE', wda["runner"]["buildStyle"], r, "Debug") 34 | project.set_flags('PRODUCT_BUNDLE_IDENTIFIER', idPrefix + ".WebDriverAgentLib", l, "Debug") 35 | project.set_flags('PRODUCT_BUNDLE_IDENTIFIER', idPrefix + ".WebDriverAgentRunner", r, "Debug") 36 | 37 | lProv = wda["lib"]["provisioningProfile"] 38 | rProv = wda["runner"]["provisioningProfile"] 39 | 40 | if lProv == "": 41 | removeflag(l, 'PROVISIONING_PROFILE_SPECIFIER') 42 | else: 43 | project.set_flags('PROVISIONING_PROFILE_SPECIFIER', lProv, l, "Debug") 44 | 45 | if rProv == "": 46 | removeflag(r, 'PROVISIONING_PROFILE_SPECIFIER') 47 | else: 48 | project.set_flags('PROVISIONING_PROFILE_SPECIFIER', rProv, r, "Debug") 49 | 50 | print("Lib:") 51 | print(" Style : " + ( getflag(l, "CODE_SIGN_STYLE") or "nil" ) ) 52 | print(" Dev Team : " + ( getflag(l, "DEVELOPMENT_TEAM") or "nil" ) ) 53 | print(" Bundle ID: " + getflag(l, "PRODUCT_BUNDLE_IDENTIFIER") ) 54 | print(" Prov Prof: " + ( getflag(l, "PROVISIONING_PROFILE_SPECIFIER") or "nil" ) ) 55 | 56 | print("Runner:") 57 | print(" Style : " + ( getflag(r, "CODE_SIGN_STYLE") or "nil" ) ) 58 | print(" Dev Team : " + ( getflag(r, "DEVELOPMENT_TEAM") or "nil" ) ) 59 | print(" Bundle ID: " + getflag(r, "PRODUCT_BUNDLE_IDENTIFIER") ) 60 | print(" Prov Prof: " + ( getflag(r, "PROVISIONING_PROFILE_SPECIFIER") or "nil" ) ) 61 | 62 | project.save() 63 | -------------------------------------------------------------------------------- /python/pbxproj: -------------------------------------------------------------------------------- 1 | ../repos/mod-pbxproj/pbxproj -------------------------------------------------------------------------------- /python/requires.txt: -------------------------------------------------------------------------------- 1 | openstep_parser>=1.5.1 2 | docopt 3 | -------------------------------------------------------------------------------- /repos/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dryark/ios_remote_provider/8fe86caf15dcdc96cf7bf8f7446cd6344680fcba/repos/.gitkeep -------------------------------------------------------------------------------- /repos/versionMarkers/CFAgent: -------------------------------------------------------------------------------- 1 | 2 2 | -------------------------------------------------------------------------------- /repos/versionMarkers/WebDriverAgent: -------------------------------------------------------------------------------- 1 | 5 2 | -------------------------------------------------------------------------------- /repos/versionMarkers/calculated_json: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dryark/ios_remote_provider/8fe86caf15dcdc96cf7bf8f7446cd6344680fcba/repos/versionMarkers/calculated_json -------------------------------------------------------------------------------- /repos/versionMarkers/go-ios: -------------------------------------------------------------------------------- 1 | 2 2 | -------------------------------------------------------------------------------- /repos/versionMarkers/iosif: -------------------------------------------------------------------------------- 1 | 5 2 | -------------------------------------------------------------------------------- /repos/versionMarkers/ujsonin: -------------------------------------------------------------------------------- 1 | 3 2 | -------------------------------------------------------------------------------- /repos/versionMarkers/vidapp: -------------------------------------------------------------------------------- 1 | 4 2 | -------------------------------------------------------------------------------- /sanity.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | uc "github.com/nanoscopic/uclop/mod" 8 | ) 9 | 10 | func sanityChecks( config *Config, cmd *uc.Cmd ) bool { 11 | // Verify iosif has been built / exists 12 | bridge := config.bridge 13 | 14 | if bridge == "go-ios" { 15 | goIosPath := config.goIosPath 16 | goIosPath, _ = filepath.Abs( goIosPath ) 17 | if _, err := os.Stat( goIosPath ); os.IsNotExist( err ) { 18 | fmt.Fprintf(os.Stderr,"%s does not exist. Rerun `make` to build go-ios\n",goIosPath) 19 | return false 20 | } 21 | } 22 | 23 | if bridge == "iosif" { 24 | iosIfPath := config.iosIfPath 25 | iosIfPath, _ = filepath.Abs( iosIfPath ) 26 | if _, err := os.Stat( iosIfPath ); os.IsNotExist( err ) { 27 | fmt.Fprintf(os.Stderr,"%s does not exist. Rerun `make` to build iosif\n",iosIfPath) 28 | return false 29 | } 30 | } 31 | 32 | /*if config.wdaSanityCheck { 33 | wdaPath := config.wdaPath 34 | wdaPath, _ = filepath.Abs( "./" + wdaPath ) 35 | if _, err := os.Stat( cfaPath ); os.IsNotExist( err ) { 36 | fmt.Fprintf(os.Stderr,"%s does not exist. Rerun `make` to build WebDriverAgent\n",wdaPath) 37 | return false 38 | } 39 | }*/ 40 | 41 | if config.cfaSanityCheck { 42 | cfaPath := config.cfaPath 43 | cfaPath, _ = filepath.Abs( "./" + cfaPath ) 44 | if _, err := os.Stat( cfaPath ); os.IsNotExist( err ) { 45 | fmt.Fprintf(os.Stderr,"%s does not exist. Rerun `make` to build CFAgent\n",cfaPath) 46 | return false 47 | } 48 | } 49 | 50 | return true 51 | } 52 | -------------------------------------------------------------------------------- /shutdown.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "os/exec" 7 | "os/signal" 8 | "syscall" 9 | "strings" 10 | "strconv" 11 | "time" 12 | log "github.com/sirupsen/logrus" 13 | si "github.com/elastic/go-sysinfo" 14 | ) 15 | 16 | func coro_sigterm( config *Config, devTracker *DeviceTracker ) { 17 | c := make(chan os.Signal, 2) 18 | signal.Notify(c, os.Interrupt, syscall.SIGTERM) 19 | go func() { 20 | <- c 21 | do_shutdown( config, devTracker ) 22 | }() 23 | } 24 | 25 | func do_shutdown( config *Config, devTracker *DeviceTracker ) { 26 | log.WithFields( log.Fields{ 27 | "type": "sigterm", 28 | "state": "begun", 29 | } ).Info("Shutdown started") 30 | 31 | devTracker.shutdown() 32 | 33 | log.WithFields( log.Fields{ 34 | "type": "sigterm", 35 | "state": "stopped", 36 | } ).Info("Normal stop done... cleaning up leftover procs") 37 | 38 | cleanup_procs( config ) 39 | 40 | log.WithFields( log.Fields{ 41 | "type": "sigterm", 42 | "state": "done", 43 | } ).Info("Shutdown finished") 44 | 45 | os.Exit(0) 46 | } 47 | 48 | type Aproc struct { 49 | pid int 50 | cmd string 51 | args []string 52 | } 53 | 54 | func get_procs() []Aproc { 55 | out, _ := exec.Command( "ps", "-eo", "pid,args" ).Output() 56 | lines := strings.Split( string(out), "\n" ) 57 | 58 | procs := []Aproc{} 59 | for _,line := range lines { 60 | if line == "" { continue } 61 | 62 | i := 0 63 | for ; i 2 { 76 | args = parts[2:] 77 | } 78 | procs = append( procs, Aproc{ pid: pid, cmd: parts[1], args: args } ) 79 | } 80 | return procs 81 | } 82 | 83 | func cleanup_procs( config *Config ) { 84 | plog := log.WithFields( log.Fields{ 85 | "type": "proc_cleanup", 86 | } ) 87 | 88 | procMap := map[string]string { 89 | "iosif": config.iosIfPath, 90 | "xcodebuild": "/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild", 91 | } 92 | 93 | procs := get_procs() 94 | 95 | var hangingPids []int 96 | 97 | for _,proc := range procs { 98 | for k,v := range procMap { 99 | if proc.cmd == v { 100 | pid := proc.pid //proc.PID() 101 | plog.WithFields( log.Fields{ 102 | "proc": k, 103 | "pid": pid, 104 | } ).Warn("Leftover " + k + " - Sending SIGTERM") 105 | 106 | syscall.Kill( pid, syscall.SIGTERM ) 107 | hangingPids = append( hangingPids, pid ) 108 | } 109 | } 110 | } 111 | 112 | if len( config.idList ) > 0 { 113 | fmt.Printf("Running in singleId mode; killing procs with id %s\n", strings.Join( config.idList, "," ) ) 114 | } 115 | 116 | // Death to all tidevice processes! *rage* 117 | for _,proc := range procs { 118 | if strings.Contains( proc.cmd, "tidevice" ) || 119 | ( 120 | strings.HasSuffix( proc.cmd, "Python" ) && 121 | proc.args[0] == "-m" && 122 | proc.args[1] == "tidevice" ) || 123 | ( 124 | strings.HasSuffix( proc.cmd, "Python" ) && 125 | strings.HasSuffix( proc.args[0], "tidevice" ) ) { 126 | pid := proc.pid //proc.PID() 127 | plog.WithFields( log.Fields{ 128 | "proc": "tidevice", 129 | "pid": pid, 130 | } ).Warn("Leftover tidevice - Sending SIGTERM") 131 | 132 | syscall.Kill( pid, syscall.SIGTERM ) 133 | hangingPids = append( hangingPids, pid ) 134 | } 135 | if strings.Contains( proc.cmd, "go-ios" ) { 136 | pid := proc.pid //proc.PID() 137 | doKill := false 138 | /* 139 | If using a singleId ( udid specified on CLI ), then we don't want to get rid of all 140 | go-ios procs, only the ones associated with that ID. 141 | */ 142 | if len( config.idList ) > 0 { 143 | for _, arg := range( proc.args ) { 144 | for _, oneId := range( config.idList ) { 145 | if strings.Contains( arg, oneId ) { 146 | doKill = true 147 | } 148 | } 149 | } 150 | } else { 151 | doKill = true 152 | } 153 | 154 | if doKill == true { 155 | syscall.Kill( pid, syscall.SIGTERM ) 156 | hangingPids = append( hangingPids, pid ) 157 | 158 | plog.WithFields( log.Fields{ 159 | "proc": "go-ios", 160 | "pid": pid, 161 | "args": proc.args, 162 | } ).Warn("Leftover go-ios - Sending SIGTERM") 163 | } 164 | } 165 | if strings.Contains( proc.cmd, "iosif" ) { 166 | pid := proc.pid //proc.PID() 167 | doKill := false 168 | /* 169 | If using a singleId ( udid specified on CLI ), then we don't want to get rid of all 170 | iosif procs, only the ones associated with that ID. 171 | */ 172 | if len( config.idList ) > 0 { 173 | for _, arg := range( proc.args ) { 174 | for _, oneId := range( config.idList ) { 175 | if strings.Contains( arg, oneId ) { 176 | doKill = true 177 | } 178 | } 179 | } 180 | } else { 181 | doKill = true 182 | } 183 | 184 | if doKill == true { 185 | syscall.Kill( pid, syscall.SIGTERM ) 186 | hangingPids = append( hangingPids, pid ) 187 | 188 | plog.WithFields( log.Fields{ 189 | "proc": "iosif", 190 | "pid": pid, 191 | "args": proc.args, 192 | } ).Warn("Leftover iosif - Sending SIGTERM") 193 | } 194 | } 195 | } 196 | 197 | if len( hangingPids ) > 0 { 198 | // Give the processes half a second to shudown cleanly 199 | time.Sleep( time.Millisecond * 500 ) 200 | 201 | // Send kill to processes still around 202 | for _, pid := range( hangingPids ) { 203 | proc, _ := si.Process( pid ) 204 | if proc != nil { 205 | info, infoErr := proc.Info() 206 | arg0 := "unknown" 207 | if infoErr == nil { 208 | args := info.Args 209 | arg0 = args[0] 210 | } else { 211 | // If the process vanished before here; it errors out fetching info 212 | continue 213 | } 214 | 215 | plog.WithFields( log.Fields{ 216 | "arg0": arg0, 217 | } ).Warn("Leftover Proc - Sending SIGKILL") 218 | syscall.Kill( pid, syscall.SIGKILL ) 219 | } 220 | } 221 | 222 | // Spend up to 500 ms waiting for killed processes to vanish 223 | i := 0 224 | for { 225 | i = i + 1 226 | time.Sleep( time.Millisecond * 100 ) 227 | allGone := 1 228 | for _, pid := range( hangingPids ) { 229 | proc, _ := si.Process( pid ) 230 | if proc != nil { 231 | _, infoErr := proc.Info() 232 | if infoErr != nil { 233 | continue 234 | } 235 | allGone = 0 236 | } 237 | } 238 | if allGone == 1 && i > 5 { 239 | break 240 | } 241 | } 242 | 243 | // Write out error messages for processes that could not be killed 244 | for _, pid := range( hangingPids ) { 245 | proc, _ := si.Process( pid ) 246 | if proc != nil { 247 | info, infoErr := proc.Info() 248 | arg0 := "unknown" 249 | if infoErr != nil { 250 | continue 251 | } 252 | args := info.Args 253 | arg0 = args[0] 254 | 255 | plog.WithFields( log.Fields{ 256 | "arg0": arg0, 257 | } ).Error("Kill attempted and failed") 258 | } 259 | } 260 | } 261 | } -------------------------------------------------------------------------------- /util/signers.pl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/perl -w 2 | use strict; 3 | use File::Temp qw/tempfile/; 4 | use Data::Dumper; 5 | 6 | my $action = $ARGV[0] || ''; 7 | 8 | my $goalou = ""; 9 | my $tosign = ""; 10 | if( $action eq "sign" ) { 11 | $goalou = $ARGV[1]; 12 | $tosign = $ARGV[2]; 13 | } 14 | my $found = 0; 15 | 16 | my $rawcerts = `security find-certificate -a -p -Z`; 17 | $rawcerts .= "SHA-1 hash: 000"; 18 | 19 | my %certs; 20 | my $cert = ""; 21 | my $hash = ""; 22 | for my $line ( split( '\n', $rawcerts ) ) { 23 | if( $line =~ m/^SHA-1 hash: ([0-9A-F]+)$/ ) { 24 | my $linehash = $1; 25 | if( $hash ) { 26 | $certs{ $hash } = $cert; 27 | } 28 | $cert = ""; 29 | $hash = $linehash; 30 | next; 31 | } 32 | $cert .= "$line\n"; 33 | } 34 | 35 | my $signers = `security find-identity -v -p codesigning`; 36 | my $type = "Apple Development"; 37 | for my $line ( split( '\n', $signers ) ) { 38 | if( $line =~ m/[0-9]+\) ([A-Z0-9]+) "$type: (.+)"$/ ) { 39 | my $linehash = $1; 40 | my $linename = $2; 41 | my $cert = $certs{ $linehash }; 42 | decode_cert( $cert ); 43 | } 44 | } 45 | 46 | if( $action eq 'sign' ) { 47 | if( !$found ) { 48 | print STDERR "Could not find Mac Developer cert for developer OU $goalou\n"; 49 | exit 1; 50 | } 51 | `codesign -fs "$found" "$tosign"`; 52 | print `codesign -d -r- "$tosign"`; 53 | } 54 | exit 0; 55 | 56 | sub decode_cert { 57 | my $cert = shift; 58 | 59 | my ( $fh, $fname ) = tempfile( UNLINK => 1 ); 60 | print $fh $cert; 61 | close( $fh ); 62 | 63 | my $text = `openssl x509 -text -in $fname -noout`; 64 | for my $line ( split( '\n', $text ) ) { 65 | if( $line =~ m/Subject:.+CN=$type: (.+), OU=([A-Z0-9]+),/ ) { 66 | my $name = $1; 67 | my $ou = $2; 68 | if( !$action ) { 69 | print "Name: $name, OU: $ou\n"; 70 | } 71 | else { 72 | if( $goalou eq $ou ) { 73 | $found = "$type: $name"; 74 | } 75 | } 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /video_app_stream.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "sync" 7 | "time" 8 | 9 | "go.nanomsg.org/mangos/v3" 10 | nanoPull "go.nanomsg.org/mangos/v3/protocol/pull" 11 | nanoReq "go.nanomsg.org/mangos/v3/protocol/req" 12 | 13 | // register transports 14 | _ "go.nanomsg.org/mangos/v3/transport/all" 15 | 16 | log "github.com/sirupsen/logrus" 17 | ) 18 | 19 | type VideoStreamer interface { 20 | mainLoop() 21 | getControlChan() ( chan int ) 22 | setImageConsumer( imgConsumer *ImageConsumer ) 23 | forceOneFrame() 24 | } 25 | 26 | type AppStream struct { 27 | stopChan chan bool 28 | imgHandler *ImgHandler 29 | controlSpec string 30 | vidSpec string 31 | logSpec string 32 | udid string 33 | controlSocket mangos.Socket 34 | logSocket mangos.Socket 35 | device *Device 36 | controlMutex *sync.Mutex 37 | } 38 | 39 | func NewAppStream( stopChan chan bool, controlPort int, vidPort int, vidLogPort int, udid string, device *Device ) (*AppStream) { 40 | self := &AppStream{ 41 | stopChan: stopChan, 42 | imgHandler: NewImgHandler( stopChan, udid, device ), 43 | controlSpec: fmt.Sprintf( "tcp://127.0.0.1:%d", controlPort ), 44 | vidSpec: fmt.Sprintf( "tcp://127.0.0.1:%d", vidPort ), 45 | logSpec: fmt.Sprintf( "tcp://127.0.0.1:%d", vidLogPort ), 46 | udid: udid, 47 | device: device, 48 | controlMutex: &sync.Mutex{}, 49 | } 50 | return self 51 | } 52 | 53 | func (self *AppStream) setImageConsumer( imgConsumer *ImageConsumer ) { 54 | self.imgHandler.setImageConsumer( imgConsumer ) 55 | if self.controlSocket != nil { 56 | self.controlMutex.Lock() 57 | self.controlSocket.Send([]byte(`{"action": "oneframe"}`)) 58 | self.controlSocket.Recv() 59 | self.controlMutex.Unlock() 60 | } 61 | } 62 | 63 | func (self *AppStream) forceOneFrame() { 64 | if self.controlSocket != nil { 65 | self.controlMutex.Lock() 66 | self.controlSocket.Send([]byte(`{"action": "oneframe"}`)) 67 | self.controlSocket.Recv() 68 | self.controlMutex.Unlock() 69 | } 70 | } 71 | 72 | func (self *AppStream) getControlChan() ( chan int ) { 73 | return self.imgHandler.mainCh 74 | } 75 | 76 | func (self *AppStream) openControl() (mangos.Socket,bool,chan bool) { 77 | var controlSocket mangos.Socket 78 | var controlStopChan chan bool 79 | failures := 0 80 | 81 | for { 82 | select { 83 | case <- self.stopChan: return nil,true,nil 84 | default: 85 | } 86 | var res int 87 | controlSocket, res, controlStopChan = self.dialAppControl() 88 | if res == 0 { break } 89 | time.Sleep( time.Second * 10 ) 90 | failures = failures + 1 91 | if failures >= 1 { 92 | fmt.Printf("Failed to connect video app control 5 times. Giving up.") 93 | return nil,true,nil 94 | } 95 | } 96 | 97 | log.WithFields( log.Fields{ 98 | "type": "vidapp_control_connect", 99 | "udid": censorUuid( self.udid ), 100 | } ).Info("Vidapp - Control Connected") 101 | 102 | // Health check 103 | go func() { 104 | for { 105 | self.controlMutex.Lock() 106 | if self.controlSocket == nil { 107 | self.controlMutex.Unlock() 108 | time.Sleep( time.Second * 1 ) 109 | continue 110 | } 111 | err := self.controlSocket.Send([]byte(`{"action": "ping"}`)) 112 | if err != nil { 113 | fmt.Printf("video ping -> fail\n" ) 114 | self.controlMutex.Unlock() 115 | controlStopChan <- true 116 | break 117 | } 118 | 119 | _, err = self.controlSocket.Recv() 120 | self.controlMutex.Unlock() 121 | 122 | if err != nil { 123 | fmt.Printf("video ping recv -> fail\n" ) 124 | controlStopChan <- true 125 | break 126 | } 127 | 128 | //fmt.Printf("video ping -> %s\n", msg ) 129 | 130 | time.Sleep( time.Second * 2 ) 131 | } 132 | }() 133 | 134 | return controlSocket,false,controlStopChan 135 | } 136 | 137 | func (self *AppStream) openLog() (mangos.Socket,bool,chan bool) { 138 | var logSocket mangos.Socket 139 | var logStopChan chan bool 140 | failures := 0 141 | for { 142 | select { 143 | case <- self.stopChan: return nil,true,nil 144 | default: 145 | } 146 | var res int 147 | logSocket, res, logStopChan = self.dialAppLog() 148 | if res == 0 { break } 149 | time.Sleep( time.Second * 10 ) 150 | failures = failures + 1 151 | if failures >= 1 { 152 | fmt.Printf("Failed to connect video app log 5 times. Giving up.") 153 | return nil,true,nil 154 | } 155 | } 156 | log.WithFields( log.Fields{ 157 | "type": "vidapp_log_connect", 158 | "udid": censorUuid( self.udid ), 159 | } ).Info("Vidapp - Log Connected") 160 | 161 | go func() { 162 | for { 163 | msg, err := self.logSocket.RecvMsg() 164 | if err != nil { break } 165 | log.WithFields( log.Fields{ 166 | "type": "vidapp_log", 167 | "udid": censorUuid( self.udid ), 168 | "msg": string(msg.Body), 169 | } ).Info("Vidapp - Log") 170 | } 171 | }() 172 | 173 | return logSocket,false,logStopChan 174 | } 175 | 176 | func (self *AppStream) openVideo() (mangos.Socket,bool,chan bool) { 177 | var imgSocket mangos.Socket 178 | var vidStopChan chan bool 179 | //fmt.Printf("Attempting to connect to video\n") 180 | failures := 0 181 | for { 182 | select { 183 | case <- self.stopChan: return nil,true,nil 184 | default: 185 | } 186 | var res int 187 | imgSocket, res, vidStopChan = self.dialAppVideo() 188 | if res == 0 { break } 189 | time.Sleep( time.Second * 1 ) 190 | failures = failures + 1 191 | if failures >= 1 { 192 | fmt.Printf("Failed to connect video app stream 5 times. Giving up.") 193 | return nil,true,nil 194 | } 195 | } 196 | log.WithFields( log.Fields{ 197 | "type": "vidapp_control_connect", 198 | "udid": censorUuid( self.udid ), 199 | } ).Info("Vidapp - Video Connected") 200 | return imgSocket,false,vidStopChan 201 | } 202 | 203 | func (self *AppStream) mainLoop() { 204 | go func() { 205 | //var controlSocket mangos.Socket 206 | var imgSocket mangos.Socket 207 | var controlStopChan chan bool 208 | var vidStopChan chan bool 209 | var logStopChan chan bool 210 | 211 | //imgHandler := NewImgHandler( self.stopChan, self.udid ) 212 | self.imgHandler.setEnableStream( func() { 213 | fmt.Printf("Sending start to vidapp\n") 214 | self.controlMutex.Lock() 215 | self.controlSocket.Send([]byte(`{"action": "start"}`)) 216 | self.controlSocket.Recv() 217 | self.controlMutex.Unlock() 218 | } ) 219 | self.imgHandler.setDisableStream( func() { 220 | fmt.Printf("Sending stop to vidapp\n") 221 | self.controlMutex.Lock() 222 | self.controlSocket.Send([]byte(`{"action": "stop"}`)) 223 | self.controlSocket.Recv() 224 | self.controlMutex.Unlock() 225 | } ) 226 | 227 | firstConnect := true 228 | 229 | for { 230 | var done bool 231 | if self.controlSocket == nil { 232 | self.controlSocket,done,controlStopChan = self.openControl() 233 | if done { break } 234 | } 235 | if self.logSocket == nil { 236 | self.logSocket,done,logStopChan = self.openLog() 237 | if done { break } 238 | } 239 | if imgSocket == nil { 240 | imgSocket,done,vidStopChan = self.openVideo() 241 | if done { break } 242 | } 243 | 244 | self.imgHandler.setSource( imgSocket ) 245 | if firstConnect { 246 | self.controlMutex.Lock() 247 | self.controlSocket.Send([]byte(`{"action": "start"}`)) 248 | self.controlSocket.Recv() 249 | self.controlMutex.Unlock() 250 | firstConnect = false 251 | } 252 | res := self.imgHandler.mainLoop( vidStopChan, controlStopChan, logStopChan ) 253 | if res == 1 { break } // stopChan 254 | if res == 2 { imgSocket = nil } // imgSocket connection lost 255 | if res == 3 { // controlSocket connection lost 256 | // Either the app has died or network to it has been lost 257 | 258 | // Check if the app is still alive 259 | alive := self.device.vidAppIsAlive() 260 | // If not restart it 261 | if !alive { 262 | fmt.Printf("Video broadcast died. Restarting\n") 263 | config := self.device.config 264 | res := self.device.bridge.LaunchApp( config.vidAppBidPrefix + "." + config.vidAppBid ) // com.dryark.vidstream 265 | if res == false { 266 | appPath := "bin/vidstream/vidstream.app" 267 | if _, err := os.Stat(appPath); err == nil { 268 | self.device.bridge.InstallApp( appPath ) 269 | } else if os.IsNotExist(err) { 270 | // TODO: panic. 271 | } 272 | } 273 | self.device.justStartBroadcast() 274 | self.controlSocket = nil 275 | imgSocket = nil 276 | self.logSocket = nil 277 | } else { 278 | self.controlSocket = nil 279 | } 280 | } 281 | if res == 4 { // lost send socket 282 | fmt.Printf("Lost send socket; disabling video stream\n") 283 | self.controlMutex.Lock() 284 | self.controlSocket.Send([]byte(`{"action": "stop"}`)) 285 | self.controlSocket.Recv() 286 | self.controlMutex.Unlock() 287 | } 288 | if res == 5 { 289 | self.logSocket = nil 290 | } 291 | } 292 | 293 | if self.controlSocket != nil { self.controlSocket.Close() } 294 | if imgSocket != nil { imgSocket.Close() } 295 | if self.logSocket != nil { self.logSocket.Close() } 296 | }() 297 | } 298 | 299 | func ( self *AppStream) dialAppVideo() ( mangos.Socket, int, chan bool ) { 300 | vidSpec := self.vidSpec 301 | 302 | var err error 303 | var pullSock mangos.Socket 304 | 305 | if pullSock, err = nanoPull.NewSocket(); err != nil { 306 | log.WithFields( log.Fields{ 307 | "type": "err_socket_new", 308 | "zmq_spec": vidSpec, 309 | "err": err, 310 | } ).Info("Socket new error") 311 | return nil, 1, nil 312 | } 313 | 314 | sec1, _ := time.ParseDuration( "2s" ) 315 | setError := pullSock.SetOption( mangos.OptionRecvDeadline, sec1 ) 316 | if setError != nil { 317 | fmt.Printf("Set timeout error %s\n", setError ) 318 | os.Exit(0) 319 | } 320 | 321 | if err = pullSock.Dial(vidSpec); err != nil { 322 | log.WithFields( log.Fields{ 323 | "type": "err_socket_dial", 324 | "spec": vidSpec, 325 | "err": err, 326 | } ).Info("Socket dial error") 327 | return nil, 2, nil 328 | } 329 | 330 | vidStopChan := make( chan bool ) 331 | 332 | pullSock.SetPipeEventHook( func( action mangos.PipeEvent, pipe mangos.Pipe ) { 333 | //fmt.Printf("Pipe action %d\n", action ) 334 | if action == 2 { vidStopChan <- true } 335 | } ) 336 | 337 | return pullSock, 0, vidStopChan 338 | } 339 | 340 | func (self *AppStream) dialAppControl() ( mangos.Socket, int, chan bool ) { 341 | controlSpec := self.controlSpec 342 | 343 | var err error 344 | var reqSock mangos.Socket 345 | 346 | if reqSock, err = nanoReq.NewSocket(); err != nil { 347 | log.WithFields( log.Fields{ 348 | "type": "err_socket_new", 349 | "zmq_spec": controlSpec, 350 | "err": err, 351 | } ).Info("Socket new error") 352 | return nil, 1, nil 353 | } 354 | 355 | sec1, _ := time.ParseDuration( "1s" ) 356 | setError := reqSock.SetOption( mangos.OptionRecvDeadline, sec1 ) 357 | if setError != nil { 358 | fmt.Printf("Set timeout error %s\n", setError ) 359 | os.Exit(0) 360 | } 361 | setError = reqSock.SetOption( mangos.OptionSendDeadline, sec1 ) 362 | if setError != nil { 363 | fmt.Printf("Set timeout error %s\n", setError ) 364 | os.Exit(0) 365 | } 366 | 367 | if err = reqSock.Dial( controlSpec ); err != nil { 368 | log.WithFields( log.Fields{ 369 | "type": "err_socket_dial", 370 | "spec": controlSpec, 371 | "err": err, 372 | } ).Info("Socket dial error") 373 | return nil, 2, nil 374 | } 375 | 376 | controlStopChan := make( chan bool ) 377 | 378 | reqSock.SetPipeEventHook( func( action mangos.PipeEvent, pipe mangos.Pipe ) { 379 | //fmt.Printf("Pipe action %d\n", action ) 380 | if action == 2 { controlStopChan <- true } 381 | } ) 382 | 383 | return reqSock, 0, controlStopChan 384 | } 385 | 386 | func (self *AppStream) dialAppLog() ( mangos.Socket, int, chan bool ) { 387 | logSpec := self.logSpec 388 | 389 | var err error 390 | var reqSock mangos.Socket 391 | 392 | if reqSock, err = nanoPull.NewSocket(); err != nil { 393 | log.WithFields( log.Fields{ 394 | "type": "err_socket_new", 395 | "zmq_spec": logSpec, 396 | "err": err, 397 | } ).Info("Socket new error") 398 | return nil, 1, nil 399 | } 400 | 401 | if err = reqSock.Dial( logSpec ); err != nil { 402 | log.WithFields( log.Fields{ 403 | "type": "err_socket_dial", 404 | "spec": logSpec, 405 | "err": err, 406 | } ).Info("Socket dial error") 407 | return nil, 2, nil 408 | } 409 | 410 | logStopChan := make( chan bool ) 411 | 412 | reqSock.SetPipeEventHook( func( action mangos.PipeEvent, pipe mangos.Pipe ) { 413 | //fmt.Printf("Pipe action %d\n", action ) 414 | if action == 2 { logStopChan <- true } 415 | } ) 416 | 417 | return reqSock, 0, logStopChan 418 | } 419 | 420 | -------------------------------------------------------------------------------- /video_img_consumer.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type ImageConsumer struct { 4 | consumer func( string, []byte ) (error) 5 | noframesf func() 6 | udid string 7 | } 8 | 9 | func NewImageConsumer( consumer func( string, []byte ) (error), noframes func() ) (*ImageConsumer) { 10 | self := &ImageConsumer{ 11 | consumer: consumer, 12 | noframesf: noframes, 13 | } 14 | return self 15 | } 16 | 17 | func (self *ImageConsumer) consume( text string, bytes []byte ) (error) { 18 | return self.consumer( text, bytes ) 19 | } 20 | 21 | func (self *ImageConsumer) noframes() { 22 | self.noframesf() 23 | } -------------------------------------------------------------------------------- /video_img_handler.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | 7 | "go.nanomsg.org/mangos/v3" 8 | 9 | // register transports 10 | _ "go.nanomsg.org/mangos/v3/transport/all" 11 | 12 | uj "github.com/nanoscopic/ujsonin/v2/mod" 13 | log "github.com/sirupsen/logrus" 14 | ) 15 | 16 | type ImgHandler struct { 17 | inSock mangos.Socket 18 | stopChan chan bool 19 | imgConsumer *ImageConsumer 20 | mainCh chan int 21 | discard bool 22 | imgNum int 23 | sentSize bool 24 | enableStream func() 25 | disableStream func() 26 | udid string 27 | device *Device 28 | isUp bool 29 | } 30 | 31 | func NewImgHandler( stopChan chan bool, udid string, device *Device ) ( *ImgHandler ) { 32 | self := ImgHandler { 33 | inSock: nil, 34 | stopChan: stopChan, 35 | mainCh: make( chan int ), 36 | discard: true, 37 | imgNum: 1, 38 | udid: udid, 39 | device: device, 40 | isUp: false, 41 | } 42 | return &self 43 | } 44 | 45 | func ( self *ImgHandler ) setImageConsumer( imgConsumer *ImageConsumer ) { 46 | self.imgConsumer = imgConsumer 47 | } 48 | 49 | func ( self *ImgHandler ) setEnableStream( enableStream func() ) { 50 | self.enableStream = enableStream 51 | } 52 | 53 | func ( self *ImgHandler ) setDisableStream( disableStream func() ) { 54 | self.disableStream = disableStream 55 | } 56 | 57 | func ( self *ImgHandler ) setSource( socket mangos.Socket ) { 58 | self.inSock = socket 59 | } 60 | 61 | func ( self *ImgHandler ) processImgMsg() (int) { 62 | msg, err := self.inSock.RecvMsg() 63 | 64 | if err != nil { 65 | if err == mangos.ErrRecvTimeout { 66 | return 2 67 | } else if err == mangos.ErrClosed { 68 | fmt.Printf("Connection to video closed\n") 69 | return 1 70 | } else { 71 | fmt.Printf( "Other error: %s", err ) 72 | return 0 73 | } 74 | } 75 | 76 | self.imgNum = self.imgNum + 1 77 | if ( self.imgNum % 30 ) == 0 { 78 | fmt.Printf("Got incoming frame %d\n", self.imgNum) 79 | } 80 | 81 | if self.discard && self.sentSize { 82 | msg.Free() 83 | return 0 84 | } 85 | 86 | text := "" 87 | data := []byte{} 88 | // image is prepended by some JSON metadata 89 | 90 | if len(msg.Body) == 0 { 91 | fmt.Printf("nil message from video app\n") 92 | return 0 93 | } 94 | 95 | if msg.Body[0] == '{' { 96 | endi := strings.Index( string(msg.Body), "}" ) 97 | root, left := uj.Parse( msg.Body ) 98 | lenLeft := len( left ) 99 | if ( len(msg.Body ) - lenLeft - 1 ) != endi { 100 | fmt.Printf( "size mistmatched what was parsed: %d != %d\n", endi, len( msg.Body ) - len(left) - 1 ) 101 | } 102 | data = left 103 | 104 | if lenLeft < 10 { 105 | // it's just a text message 106 | msgNode := root.Get("msg") 107 | if msgNode != nil { 108 | msg := msgNode.String() 109 | if msg == "noframes" { 110 | self.imgConsumer.noframes() 111 | } 112 | } 113 | return 0 114 | } 115 | 116 | //ow := root.Get("ow").Int() 117 | //oh := root.Get("oh").Int() 118 | dw := root.Get("dw").Int() 119 | dh := root.Get("dh").Int() 120 | 121 | //fmt.Printf("ow=%d, oh=%d, dw=%d, dh=%d\n", ow, oh, dw, dh ) 122 | 123 | causeNode := root.Get("c") 124 | cause := -1 125 | if causeNode != nil { cause = causeNode.Int() } 126 | 127 | crcNode := root.Get("crc") 128 | crc := "n/a" 129 | if crcNode != nil { crc = crcNode.String() } 130 | 131 | text = fmt.Sprintf("{\"Width\": %d, \"Height\": %d, \"Size\": %d, \"Cause\": %d, \"Crc\": \"%s\"}", 132 | dw, dh, len( msg.Body ), cause, crc ) 133 | 134 | if !self.isUp { 135 | self.isUp = true 136 | self.device.EventCh <- DevEvent{ action: DEV_VIDEO_START } 137 | } 138 | 139 | if !self.sentSize { 140 | //json := fmt.Sprintf( `{"type":"frame1","width":%d,"height":%d,"uuid":"%s"}`, dw, dh, self.udid ) 141 | //fmt.Printf("FIRSTFRAME%s\n",json) 142 | self.sentSize = true 143 | } 144 | } else { 145 | data = msg.Body 146 | } 147 | 148 | if !self.discard { 149 | err := self.imgConsumer.consume( text, data ) 150 | msg.Free() 151 | if err != nil { 152 | // might as well begin discarding since we can't send 153 | self.discard = true 154 | return 3 155 | } 156 | } else { 157 | msg.Free() 158 | } 159 | 160 | return 0 161 | } 162 | 163 | func ( self *ImgHandler ) mainLoop( vidStopChan chan bool, controlStopChan chan bool, logStopChan chan bool) (int) { 164 | var res int 165 | reason := "unknown" 166 | 167 | log.WithFields( log.Fields{ 168 | "type": "video_handler_start", 169 | "udid": censorUuid( self.udid ), 170 | } ).Info("Video handler start") 171 | for { 172 | select { 173 | case <- controlStopChan: 174 | fmt.Printf("Lost connection to control socket\n") 175 | res = 3 176 | goto DONE 177 | case <- logStopChan: 178 | fmt.Printf("Lost connection to log socket\n") 179 | res = 5 180 | goto DONE 181 | case <- vidStopChan: 182 | fmt.Printf("Lost connection to video stream\n") 183 | self.isUp = false 184 | self.device.EventCh <- DevEvent{ action: DEV_VIDEO_STOP } 185 | res = 2 186 | goto DONE 187 | case <- self.stopChan: 188 | //fmt.Printf("Server channel got stop message\n") 189 | res = 1 190 | reason = "Got stop message" 191 | goto DONE 192 | case msg := <- self.mainCh: 193 | if msg == 1 { 194 | self.enableStream() 195 | fmt.Printf("Setting discard to false\n") 196 | self.discard = false 197 | } 198 | if msg == 2 { 199 | fmt.Printf("Setting discard to true\n") 200 | self.discard = true 201 | } 202 | default: // this makes the above read from stopChannel non-blocking 203 | } 204 | pres := self.processImgMsg() 205 | if pres == 1 { 206 | res = 2 207 | goto DONE 208 | } 209 | if pres == 3 { // lost send socket 210 | res = 4 211 | reason = "Lost send socket" 212 | goto DONE 213 | } 214 | self.imgNum++ 215 | } 216 | 217 | DONE: 218 | log.WithFields( log.Fields{ 219 | "type": "video_handler_stop", 220 | "udid": censorUuid( self.udid ), 221 | "reason": reason, 222 | } ).Info("Video handler stop") 223 | self.disableStream() 224 | return res 225 | } -------------------------------------------------------------------------------- /wda.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | log "github.com/sirupsen/logrus" 6 | ) 7 | 8 | type WDA struct { 9 | udid string 10 | devTracker *DeviceTracker 11 | dev *Device 12 | config *Config 13 | base string 14 | startChan chan int 15 | wdaPort int 16 | } 17 | 18 | func NewWDA( config *Config, devTracker *DeviceTracker, dev *Device ) (*WDA) { 19 | self := NewWDANoStart( config, devTracker, dev ) 20 | 21 | fmt.Printf("WDA Method:%s\n", dev.devConfig.wdaMethod ) 22 | 23 | if dev.devConfig.wdaMethod == "" { 24 | return self 25 | } 26 | 27 | //if config.wdaMethod != "manual" { 28 | self.start( nil ) 29 | //} else { 30 | // self.forward( func( x int, stopChan chan bool ) { 31 | // } ) 32 | //} 33 | return self 34 | } 35 | 36 | func NewWDANoStart( config *Config, devTracker *DeviceTracker, dev *Device ) (*WDA) { 37 | self := WDA{ 38 | udid: dev.udid, 39 | wdaPort: dev.wdaPort, 40 | devTracker: devTracker, 41 | dev: dev, 42 | config: config, 43 | base: fmt.Sprintf("http://127.0.0.1:%d",dev.wdaPort), 44 | } 45 | 46 | return &self 47 | } 48 | 49 | func (self *WDA) forward( onready func( int, chan bool ) ) { 50 | pairs := []TunPair{ 51 | TunPair{ from: self.wdaPort, to: 8100 }, 52 | } 53 | 54 | stopChan := make( chan bool ) 55 | self.dev.bridge.tunnel( pairs, func() { 56 | if onready != nil { 57 | onready( 0, stopChan ) 58 | } 59 | } ) 60 | } 61 | 62 | func (self *WDA) start( started func( int, chan bool ) ) { 63 | self.forward( func( x int, stopChan chan bool ) { 64 | self.dev.bridge.wda( 65 | func() { // onStart 66 | log.WithFields( log.Fields{ 67 | "type": "wda_start", 68 | "udid": censorUuid(self.udid), 69 | "port": self.wdaPort, 70 | } ).Info("[WDA] successfully started") 71 | 72 | //if self.startChan != nil { 73 | // self.startChan <- 0 74 | //} 75 | 76 | self.dev.EventCh <- DevEvent{ action: DEV_WDA_START } 77 | }, 78 | func(interface{}) { // onStop 79 | self.dev.EventCh <- DevEvent{ action: DEV_WDA_STOP } 80 | }, 81 | ) 82 | } ) 83 | } --------------------------------------------------------------------------------