├── .gitignore
├── LICENSE
├── Makefile
├── README.md
├── bin
├── .gitkeep
└── bins.json
├── cache
└── .gitkeep
├── check-versions.pl
├── config.json.example
├── coordinator
├── .gitignore
├── Makefile
├── app
│ └── Info.plist
├── config.go
├── coordinator.go
├── firewall.go
├── go.mod
├── heartbeat.go
├── http_server.go
├── idevice.go
├── launchctl.go
├── log.go
├── network.go
├── periodic.go
├── ports.go
├── proc_backoff.go
├── proc_device_trigger.go
├── proc_device_unit.go
├── proc_generic.go
├── proc_h264_to_jpeg.go
├── proc_ios_video_pull.go
├── proc_ios_video_stream.go
├── proc_ivf.go
├── proc_stf_provider.go
├── proc_video_enabler.go
├── proc_vnc_proxy.go
├── proc_wdaproxy.go
├── shutdown.go
├── stf_control.go
├── video_app.go
├── vpn.go
├── wda.go
└── zmq.go
├── get-version-info.sh
├── get-wda-build-path.sh
├── init.sh
├── logs
└── .gitkeep
├── makefile_preflight.pl
├── offline
└── Makefile
├── run
├── runner
├── Makefile
├── gencert.pl
├── go.mod
├── go.sum
├── http_server.go
├── pass_check.go
├── proc_backoff.go
├── runner.go
├── runner.json
├── runnercert.tmpl
├── shutdown.go
├── update.go
└── versions.go
├── server
├── .env
├── README.md
├── cert
│ ├── gencert.sh
│ ├── server.conf
│ ├── server.crt
│ └── server.key
├── docker-compose.yml
├── nginx
│ ├── Dockerfile
│ ├── entrypoint.sh
│ └── nginx.conf
├── runcli.js
└── storage-temp
│ └── Dockerfile
├── stf_ios_support.rb
├── tblick-info.sh
├── temp
└── .gitkeep
├── update_server
├── Makefile
├── go.mod
├── main.go
└── updates
│ ├── index.html
│ ├── remote.pl
│ ├── updates.json
│ └── v1.json
├── util
├── alf2.pl
├── brewser.pl
├── signers.pl
└── tcc.pl
├── version.json
├── video_pipes
└── .gitkeep
├── view_log.go
└── wda_wrapper
├── Makefile
├── go.mod
└── wda_wrapper.go
/.gitignore:
--------------------------------------------------------------------------------
1 | 10
2 | .DS_Store
3 | repos
4 | bin
5 | offline
6 | config.json
7 | logs
8 | view_log
9 | wda_wrapper/go.sum
10 | temp
11 | dist.tgz
12 | cache/*
13 | runner/*.crt
14 | runner/*.key
15 | runner/runnercert.conf
16 | runner/runner
17 | update_server/server
18 | **/*~
19 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## STF IOS Support
2 |
3 | ### Prerequisites
4 | 1. A machine running MacOS ( to build and run the "provider" )
5 | 1. A machine running Linux with Docker container support ( to run the STF server )
6 |
7 | ### Build machine setup
8 | 1. Clone this repo down to your build machine
9 | 1. Install XCode
10 | 1. Add your developer Apple ID to XCode
11 |
12 | 1. XCode -> XCode menu -> Preferences -> Accounts Tab
13 | 1. Click `+` under `Apple IDs` list
14 | 1. Choose `Apple ID`
15 | 1. Login to your account
16 | 1. Download a "Apple Development certificate" for your user
17 |
18 | 1. Continue from previous step, right after logging into your Developer account in Xcode
19 | 1. Select `Manage Certificates`
20 | 1. Click `+` in the lower left corner
21 | 1. Select `Apple Development`
22 | 1. Clone the various needed repos ( includes WebDriverAgent )
23 |
24 | 1. Run `make clone`
25 | 1. Configure WebDriverAgent to use your identity for signing
26 |
27 | 1. Open `repos/WebDriverAgent/WebDriverAgent.xcodeproj` in XCode
28 | 1. Select the WebDriverAgentLib target
29 | 1. Go to the `Signing & Capabilities` tab
30 | 1. Select your team under `Team`
31 | 1. Select the WebDriverAgentRunner target
32 | 1. Go to the `Signing & Capabilities` tab
33 | 1. Select your team under `Team`
34 | 1. Run `./init.sh`
35 |
36 | ### Deploy server side:
37 | 1. On your Linux machine
38 | 1. Copy `server` folder to your Linux machine
39 | 1. Run `server/cert/gencert.sh` to generate a self-signed cert ( or use your own )
40 | 1. Note! Plain http STF server is not supported. It can be done, but it shouldn't and tickets to make it so will be closed.
41 | 1. Update `server/.env` to reflect the IP and hostname for your server
42 | 1. Start STF
43 |
44 | 1. docker-compose up
45 |
46 | ### Using a standard OpenSTF server:
47 | 1. Setup your server as normal following upstream instructions
48 | 1. Create an SSL certificate for your server using the method you desire.
49 | 1. Configure the OpenSTF server for SSL
50 | 1. Alter stf_ios_support/coordinator/proc_stf_provider --connect-sub and --connect-push lines to match your server config
51 |
52 | ### Build provider files:
53 | 1. Copy the first {} block from `config.json.example` into `config.json`. Do not include any comment lines starting with //
54 | 1. Update config.json
55 |
56 | 1. Update `xcode_dev_team_id` to be the OU of your developer account. If you add your account into Xcode first, you can then run
57 | `make ou` to display what the OU is. You can also find it by opening the keychain, selecting the Apple Development certificate
58 | for your account, and then looking at what the `Organization Unit` is.
59 | 1. Update `root_path` to be where provider code should be installed, such as `/Users/user/stf`
60 | 1. Update `config_path` to match that, such as `/Users/user/stf/config.json`
61 | 1. Run `make` then `make dist`
62 |
63 | 1. dist.tgz will be created
64 |
65 | ### Deploy provider setup:
66 | 1. Copy `dist.tgz` from build machine
67 | 1. Run `tar -xf dist.tgz`
68 | 1. Tweak `config.json` as desired
69 |
70 | ### Starting provider
71 | 1. Register(provision) your IOS device to your developer account as a developer device.
72 |
73 | 1. Use the API -or-
74 |
75 | 1. Follow https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api to create
76 | an app store connect API key. Give it Developer access.
77 | 1. Gain a session using JSON Web Tokens
78 | 1. Create a provisioning profile if none exist using profiles: https://developer.apple.com/documentation/appstoreconnectapi/profiles
79 | See also https://github.com/cidertool/asc-go/blob/f08b8151f7fd92ff54924480338dafbf8a383255/asc/provisioning_profiles.go
80 | 1. Post to the devices endpoint to register a device: https://developer.apple.com/documentation/appstoreconnectapi/devices
81 | See also https://github.com/cidertool/asc-go/blob/f08b8151f7fd92ff54924480338dafbf8a383255/asc/provisioning_devices.go
82 | 1. Follow these instructions: https://www.telerik.com/blogs/how-to-add-ios-devices-to-your-developer-profile
83 | I couldn't find updated instructions on Apple's website. If you find them please let me know so I can link to them.
84 | 1. Plug your IOS device in
85 | 1. Pair it with your system
86 |
87 | 1. Run `idevicepair pair`
88 | 1. Accept pairing on IOS device screen
89 | 1. Have Xcode setup the "developer image" on your IOS device:
90 |
91 | 1. Open Xcode
92 | 1. Go to Windows... Devices and Simulators
93 | 1. Wait while Developer Image is installed to your phone
94 | 1. Run `./bin/ios_video_pull -devices -decimal` to determine the PID ( product ID ) of your IOS device in decimal
95 | 1. Run `./bin/devreset [decimal product ID] 1452` to reset the video streaming status of your IOS device
96 | 1. Run `./run` ( and leave it running )
97 | 1. Permissions dialog boxes appear for coordinator to listen on various ports; select accept for all of them
98 | 1. Device shows up in STF with video and can be controlled. Yay
99 |
100 | ### Using runner
101 | Runner is a command that runs coordinator in a loop and also enables installation/update of a distribution.
102 | Runner is not necessary to use stf_ios_support. It is provided to make it easier to remotely maintain a cluster
103 | of providers.
104 | To use it:
105 | 1. Run `make` to build all the things
106 | 1. Run `runner/runner -pass [some password]` to generate crypted password of your choice
107 | 1. Adjust `runner/runner.json`
108 |
109 | 1. Update user password with the crypted output of previous step
110 | 1. Update user to something else ( default user/pass are both replaceme )
111 | 1. Update update_server to be host/IP address of the server you will use to run update_server
112 | 1. Update updates path to be path on a provider machine where you want downloaded updates to be saved/cached
113 | 1. Update install_dir path to be the path where you want `coordinator` to be installed
114 | 1. Update config path to be a path where `config.json` for `coordinator` will be located on provider machine
115 | 1. Rerun `make` to rebuild `runner.tgz`
116 | 1. Run `make updatedist` to build `update_server.tgz`
117 | 1. Copy `update_server.tgz` to a server
118 | 1. Extract it
119 |
120 | 1. `tar -xf update_server`
121 | 1. Run it and leave it running
122 |
123 | 1. `update_server/server`
124 | 1. Copy `runner.tgz` to a provider machine
125 | 1. Extract it
126 |
127 | 1. `tar -xf runner.tgz`
128 | 1. Stop any instance you may be running of `coordinator` already on the provider
129 | 1. Run it in a visual GUI MacOS session
130 | 1. Go to `https://[provider ip/host]:8021` in your browser
131 | 1. Accept the self-signed cert ( or make your own non-self signed cert and adjust updaet_server config )
132 | 1. Click the update button to download/install/start `coordinator` on the provider
133 |
134 | ### Known Issues
135 | 1. libimobiledevice won't install properly right now from brew
136 |
137 | 1. `./init.sh`
138 | 1. `make libimd` ( init.sh actually already runs this )
139 | 1. Video streaming will sometimes be left in a "stuck" state
140 |
141 | 1. ios_video_pull sub-process of coordinator depends on quicktime_video_hack upstream repo/library. That library
142 | does not properly "stop" itself if you start and then stop reading video from an IOS device. As a result, if
143 | you run coordinator, stop it, then start it again, it won't be able to start up again correctly.
144 | 1. To fix this you can use devreset. This is why the devreset command is mentioned above currently to run before
145 | starting coordinator. devreset effectively stops the video streaming entirely, resetting it so that it can
146 | be started up again.
147 |
148 | ### Setting up with VPN
149 | 1. Install openvpn-server on your STF server machine
150 | 1. Create client certificate(s) using your favorite process...
151 | 1. Create ovpn file(s) with those client certs
152 | 1. Deploy those cert(s) to your provider machines; setting them up in Tunnelblick
153 | 1. Alter config.json on each provider to have the name of the cert setup in Tunnelblick
154 | 1. Start openvpn server on STF server
155 | 1. Start coordinator/provider on each provider machine
156 |
157 | ### Handling video not working
158 | 1. Run `./view_log proc ios_video_pull` to check for errors fetching h264 data from the IOS device
159 | 1. Run `./view_log -proc ios_video_stream` to check for errors streaming jpegs via websocket to browser
160 | 1. Reboot your IOS device and try again
161 |
162 | ### Debugging
163 | 1. run `./view_log` to see list of things that log
164 | 1. run `./view_log -proc [one from list]`
165 |
166 | ### FAQ
167 | See https://github.com/devicefarmer/stf_ios_support/wiki/FAQ
--------------------------------------------------------------------------------
/bin/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dryark/stf_ios_support/2f32cbbf1506a740eb21a496c1cdc11d875558f7/bin/.gitkeep
--------------------------------------------------------------------------------
/bin/bins.json:
--------------------------------------------------------------------------------
1 | {
2 | "bins": [
3 | {
4 | "short": "coordinator",
5 | "name": "Coordinator",
6 | "cmd": "coordinator -version"
7 | },
8 | {
9 | "short": "ivf_pull",
10 | "name": "IOS AVF Pull",
11 | "cmd": "ivf_pull version"
12 | }
13 | ]
14 | }
--------------------------------------------------------------------------------
/cache/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dryark/stf_ios_support/2f32cbbf1506a740eb21a496c1cdc11d875558f7/cache/.gitkeep
--------------------------------------------------------------------------------
/check-versions.pl:
--------------------------------------------------------------------------------
1 | #!/usr/bin/perl -w
2 | use strict;
3 | use Data::Dumper;
4 |
5 | my $main = `git log -1 --date=unix`;
6 | my $mainT = 0;
7 | if( $main =~ m/Date:\s+([0-9]+)/ ) {
8 | $mainT = $1;
9 | }
10 | my $arg = $ARGV[0];
11 | if( -e "temp/check-ok-$mainT" && ( !$arg || $arg ne 'force' ) ) {
12 | print "Versions already checked; skipping version check\n";
13 | exit;
14 | }
15 |
16 | my $versions = `./get-version-info.sh --unix --wdasource`;
17 | open( my $vfile, ">temp/current_versions.json" );
18 | print $vfile $versions;
19 | close( $vfile );
20 | $versions =~ s/:/=>/g;
21 | $versions =~ s/"/'/g;
22 |
23 | my $ob = eval( $versions );
24 |
25 | my $have_issues = 0;
26 | my $reqs = {
27 | # h264_to_jpeg is no longer the primary video mechanism
28 | # h264_to_jpeg => { min => 1588831486 },
29 |
30 | ios_video_stream => { min => 1597980831, message => "Then run `make cleanivs` them `make`" },
31 | wdaproxy => { min => 1594408035, message => "Then run `make cleanwdaproxy` then `make`" },
32 | wda => { min => 1596738353, message => "Then run `make cleanwda` them `make`", name => 'WebDriverAgent' },
33 | ios_avf_pull => { min => 1597380907 },
34 | stf => { min => 1597980993, name => 'stf-ios-provider' },
35 | device_trigger => { min => 1578609558, name => 'osx_ios_device_trigger' }
36 | };
37 | for my $name ( keys %$reqs ) {
38 | my $repo = $ob->{ $name };
39 | if( !$repo ) {
40 | $have_issues = 1;
41 | print "repos/$name is missing\n";
42 | next;
43 | }
44 | my $error = $repo->{error};
45 | if( $error ) {
46 | $have_issues = 1;
47 | print "$name; error: $error\n";
48 | next;
49 | }
50 | my $remote = $repo->{remote};
51 | my $date = $repo->{date};
52 | my $dirname = $repo->{name} || $name;
53 | $remote =~ s/=>/:/;
54 | my $req = $reqs->{ $name };
55 | if( $req->{ min } ) {
56 | my $min = $req->{ min };
57 | if( $date < $min ) {
58 | my $msg = $req->{ message } || '';
59 | print STDERR "repos/$name is out of date. Please git pull it. $msg\n";
60 | $have_issues = 1;
61 | }
62 | }
63 | }
64 | if( !$have_issues ) {
65 | open( my $fh, ">temp/check-ok-$mainT" );
66 | print $fh 1;
67 | close( $fh );
68 | }
--------------------------------------------------------------------------------
/config.json.example:
--------------------------------------------------------------------------------
1 | // Minimal Example; DO NOT COPY THIS LINE OR ANY LINE STARTING WITH //
2 | // To get your xcode dev org, view "Apple Development" cert in keychain, and look at "Organizational Unit"
3 | {
4 | "xcode_dev_team_id": "[your xcode developer org; ~10 char alphanumeric]",
5 | "stf": {
6 | "ip": "[your stf server ip]",
7 | "hostname": "[your stf server hostname]"
8 | },
9 | "video": {
10 | "enabled": true,
11 | "use_vnc": true,
12 | "vnc_scale": 2,
13 | "vnc_password": "",
14 | "frame_rate": 10
15 | },
16 | "install": {
17 | "root_path": "[desired stf provider install folder]",
18 | "config_path": "[desired stf provider install folder]/config.json",
19 | "set_working_dir": false
20 | }
21 | }
22 |
23 | // HERE AND BELOW IS TO SHOW DEFAULTS AND ALL OPTIONS
24 | // DO NOT COPY THIS INTO YOUR config.json FILE
25 | // Defaults
26 | {
27 | "wda_folder": "./bin/wda",
28 | "xcode_dev_team_id": "",
29 | "network": {
30 | "coordinator_port": 8027,
31 | "video": "8000-8005",
32 | "dev_ios": "9240-9250",
33 | "vnc": "5901-5911",
34 | "wda": "8100-8105",
35 | "interface": "auto"
36 | },
37 | "stf": {
38 | "ip": "",
39 | "hostname": ""
40 | },
41 | "video": {
42 | "enabled": true,
43 | "use_vnc": false,
44 | "vnc_scale": 2,
45 | "vnc_password": "",
46 | "frame_rate": 5
47 | },
48 | "install": {
49 | "root_path": "",
50 | "config_path": "",
51 | "set_working_dir": false
52 | },
53 | "log": {
54 | "main": "logs/coordinator",
55 | "proc_lines": "logs/procs",
56 | "wda_wrapper_stdout": "./logs/wda_wrapper_stdout",
57 | "wda_wrapper_stderr": "./logs/wda_wrapper_stderr"
58 | },
59 | "vpn": {
60 | "type": "none",
61 | "ovpn_working_dir": "/usr/local/etc/openvpn",
62 | "tblick_name": ""
63 | },
64 | "bin_paths": {
65 | "wdaproxy": "bin/wdaproxy",
66 | "device_trigger": "bin/osx_ios_device_trigger",
67 | "video_enabler": "bin/osx_ios_video_enabler",
68 | "mirrorfeed": "bin/mirrorfeed",
69 | "openvpn": "/usr/local/opt/openvpn/sbin/openvpn",
70 | "iproxy": "/usr/local/bin/iproxy",
71 | "wdawrapper": "bin/wda_wrapper",
72 | "ffmpeg": "bin/ffmpeg"
73 | },
74 | "repos": {
75 | "stf": "https://github.com/nanoscopic/stf-ios-provider.git",
76 | "wda": "https://github.com/appium/WebDriverAgent.git"
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/coordinator/.gitignore:
--------------------------------------------------------------------------------
1 | go.mod
2 | go.sum
3 |
--------------------------------------------------------------------------------
/coordinator/Makefile:
--------------------------------------------------------------------------------
1 | TARGET = ../bin/coordinator
2 |
3 | all: $(TARGET)
4 |
5 | coordinator_sources := $(wildcard *.go)
6 |
7 | $(TARGET): $(coordinator_sources) go.sum
8 | go build -o $(TARGET) -ldflags "-X main.GitCommit=$(GIT_COMMIT) -X main.GitDate=$(GIT_DATE) -X main.GitRemote=$(GIT_REMOTE) -X main.EasyVersion=$(EASY_VERSION)" .
9 |
10 | go.sum:
11 | go get
12 | go get .
13 |
14 | clean:
15 | $(RM) $(TARGET) go.sum
--------------------------------------------------------------------------------
/coordinator/app/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleName
6 | STF Coordinator
7 |
8 | CFBundleExecutable
9 | coordinator
10 |
11 | CFBundleIconFile
12 | icon.icns
13 |
14 | CFBundleIdentifier
15 | com.tmobile.coordinator
16 |
17 | NSHighResolutionCapable
18 |
19 |
20 | LSUIElement
21 |
22 |
23 | CFBundleInfoDictionaryVersion
24 | 6.0
25 |
26 | CFBundlePackageType
27 | APPL
28 |
29 |
--------------------------------------------------------------------------------
/coordinator/config.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "encoding/json"
5 | "fmt"
6 | "io/ioutil"
7 | "os"
8 |
9 | log "github.com/sirupsen/logrus"
10 | uj "github.com/nanoscopic/ujsonin/mod"
11 | )
12 |
13 | type Config struct {
14 | WdaFolder string `json:"wda_folder"`
15 | Network NetConfig `json:"network"`
16 | Stf STFConfig `json:"stf"`
17 | Video VideoConfig `json:"video"`
18 | FrameServer FrameServerConfig `json:"frameserver"`
19 | Install InstallConfig `json:"install"`
20 | Log LogConfig `json:"log"`
21 | BinPaths BinPathConfig `json:"bin_paths"`
22 | Vpn VPNConfig `json:"vpn"`
23 | Timing TimingConfig `json:"timing"`
24 | ConfigPath string `json:"config_path"`
25 | DeviceDetector string `json:"device_detector"`
26 | IosCLI string `json:"ios_cli"`
27 | // The following are only used internally
28 | WDAProxyPort int
29 | MirrorFeedPort int
30 | DevIosPort int
31 | VncPort int
32 | UsbmuxdPort int
33 | DecodeInPort int
34 | DecodeOutPort int
35 | ujson * uj.JNode
36 | }
37 |
38 | type NetConfig struct {
39 | Coordinator int `json:"coordinator_port"`
40 | Mirrorfeed int `json:"mirrorfeed_port"`
41 | Video string `json:"video_ports"`
42 | DevIos string `json:"dev_ios_ports"`
43 | Vnc string `json:"vnc_ports"`
44 | Wda string `json:"proxy_ports"`
45 | Decode string `json:"decode_ports"`
46 | Usbmuxd string `json:"usbmuxd_ports"`
47 | Iface string `json:"interface"`
48 | }
49 |
50 | type STFConfig struct {
51 | Ip string `json:"ip"`
52 | HostName string `json:"hostname"`
53 | Location string `json:"location"`
54 | AdminToken string `json:"admin_token"`
55 | }
56 |
57 | type VideoConfig struct {
58 | Enabled bool `json:"enabled"`
59 | Method string `json:"method"`
60 | UseVnc bool `json:"use_vnc"`
61 | VncScale int `json:"vnc_scale"`
62 | VncPassword string `json:"vnc_password"`
63 | FrameRate int `json:"frame_rate"`
64 | }
65 |
66 | type InstallConfig struct {
67 | RootPath string `json:"root_path"`
68 | ConfigPath string `json:"config_path"`
69 | SetWorkingDir bool `json:"set_working_dir"`
70 | }
71 |
72 | type LogConfig struct {
73 | Main string `json:"main"`
74 | MainApp string `json:"main_app"`
75 | ProcLines string `json:"proc_lines"`
76 | WDAWrapperStdout string `json:"wda_wrapper_stdout"`
77 | WDAWrapperStderr string `json:"wda_wrapper_stderr"`
78 | OpenVPN string `json:"openvpn"`
79 | }
80 |
81 | type BinPathConfig struct {
82 | WdaProxy string `json:"wdaproxy"`
83 | DeviceTrigger string `json:"device_trigger"`
84 | IosVideoStream string `json:"ios_video_stream"`
85 | IosVideoPull string `json:"ios_video_pull"`
86 | H264ToJpeg string `json:"h264_to_jpeg"`
87 | Openvpn string `json:"openvpn"`
88 | Iproxy string `json:"iproxy"`
89 | WdaWrapper string `json:"wdawrapper"`
90 | IVF string `json:"ivf"`
91 | VideoEnabler string `json:"video_enabler"`
92 | IosDeploy string `json:"ios-deploy"`
93 | }
94 |
95 | type VPNConfig struct {
96 | VpnType string `json:"type"`
97 | TblickName string `json:"tblick_name"`
98 | OvpnWd string `json:"ovpn_working_dir"`
99 | OvpnConfig string `json:"ovpn_config"`
100 | }
101 |
102 | type FrameServerConfig struct {
103 | Secure bool `json:"secure"`
104 | Cert string `json:"cert"`
105 | Key string `json:"key"`
106 | Width int `json:"width"`
107 | Height int `json:"height"`
108 | }
109 |
110 | type TimingConfig struct {
111 | WdaRestart int `json:"wda_restart"`
112 | }
113 |
114 | type DeviceConfig struct {
115 | Width int
116 | Height int
117 | }
118 |
119 | func get_device_config( config *Config, udid string ) ( *DeviceConfig ) {
120 | dev := DeviceConfig{}
121 |
122 | devs := config.ujson.Get("devices")
123 | if devs == nil {
124 | return nil
125 | }
126 |
127 | /*devs.ForEach( func( conf *uj.JNode ) {
128 | oneid := conf.Get("udid").String()
129 | if oneid == udid {
130 | dev.Width = conf.Get("width").Int()
131 | dev.Height = conf.Get("height").Int()
132 | }
133 | } )*/
134 | dev.Width = 735
135 | dev.Height = 1134
136 |
137 | return &dev
138 | }
139 |
140 | func read_config( configPath string ) *Config {
141 | var config Config
142 |
143 | for {
144 | fh, serr := os.Stat( configPath )
145 | if serr != nil {
146 | log.WithFields( log.Fields{
147 | "type": "err_read_config",
148 | "error": serr,
149 | "config_path": configPath,
150 | } ).Fatal("Could not read specified config path")
151 | }
152 |
153 | var configFile string
154 | switch mode := fh.Mode(); {
155 | case mode.IsDir():
156 | configFile = fmt.Sprintf("%s/config.json", configPath)
157 | case mode.IsRegular():
158 | configFile = configPath
159 | }
160 |
161 | configFh, err := os.Open( configFile )
162 | if err != nil {
163 | log.WithFields( log.Fields{
164 | "type": "err_read_config",
165 | "config_file": configFile,
166 | "error": err,
167 | } ).Fatal("failed reading config file")
168 | }
169 | defer configFh.Close()
170 |
171 | jsonBytes, _ := ioutil.ReadAll( configFh )
172 |
173 | defaultJson := `{
174 | "wda_folder": "./bin/wda",
175 | "device_detector": "api",
176 | "ios_cli": "ios-deploy",
177 | "xcode_dev_team_id": "",
178 | "network": {
179 | "coordinator_port": 8027,
180 | "video_ports": "8000-8005",
181 | "dev_ios_ports": "9240-9250",
182 | "vnc_ports": "5901-5911",
183 | "proxy_ports": "8100-8105",
184 | "decode_ports": "7878-7888",
185 | "usbmuxd_ports": "9920-9930",
186 | "interface": "auto"
187 | },
188 | "stf":{
189 | "ip": "",
190 | "hostname": "",
191 | "location": "",
192 | "admin_token": ""
193 | },
194 | "video":{
195 | "enabled": true,
196 | "method": "avfoundation",
197 | "use_vnc": false,
198 | "vnc_scale": 2,
199 | "vnc_password": "",
200 | "frame_rate": 5,
201 | "app_name": "vidtest2",
202 | "app_bundle_id": "com.dryark.vidtest2"
203 | },
204 | "frameserver":{
205 | "secure": false,
206 | "cert": "",
207 | "key": "",
208 | "width": 0,
209 | "height": 0
210 | },
211 | "install":{
212 | "root_path": "",
213 | "set_working_dir": false,
214 | "config_path": ""
215 | },
216 | "log":{
217 | "main": "logs/coordinator",
218 | "main_app": "logs/app",
219 | "proc_lines": "logs/procs",
220 | "wda_wrapper_stdout": "./logs/wda_wrapper_stdout",
221 | "wda_wrapper_stderr": "./logs/wda_wrapper_stderr",
222 | "openvpn": "logs/openvpn.log"
223 | },
224 | "vpn":{
225 | "type": "none",
226 | "ovpn_working_dir": "/usr/local/etc/openvpn",
227 | "tblick_name": ""
228 | },
229 | "bin_paths":{
230 | "wdaproxy": "bin/wdaproxy",
231 | "device_trigger": "bin/osx_ios_device_trigger",
232 | "openvpn": "/usr/local/opt/openvpn/sbin/openvpn",
233 | "iproxy": "/usr/local/bin/iproxy",
234 | "wdawrapper": "bin/wda_wrapper",
235 | "ios_video_stream":"bin/ios_video_stream",
236 | "ios_video_pull":"bin/ios_video_pull",
237 | "h264_to_jpeg": "bin/decode",
238 | "ivf": "bin/ivf_pull",
239 | "video_enabler": "bin/video_enabler",
240 | "ios-deploy": "bin/ios-deploy"
241 | },
242 | "repos":{
243 | "stf": "https://github.com/nanoscopic/stf-ios-provider.git",
244 | "wda": "https://github.com/nanoscopic/WebDriverAgent.git"
245 | },
246 | "timing":{
247 | "wda_restart": 240
248 | },
249 | "devices":[
250 | ]
251 | }`
252 |
253 | config = Config{
254 | MirrorFeedPort: 8000,
255 | WDAProxyPort: 8100,
256 | DevIosPort: 9240,
257 | VncPort: 5901,
258 | DecodeOutPort: 7878,
259 | DecodeInPort: 7879,
260 | UsbmuxdPort: 9920,
261 | }
262 |
263 | err = json.Unmarshal( []byte( defaultJson ), &config )
264 | if err != nil {
265 | log.Fatal( "1 ", err )
266 | }
267 |
268 | err = json.Unmarshal( jsonBytes, &config )
269 | if err != nil {
270 | log.Fatal( "2 ", err )
271 | }
272 |
273 | config.ujson, _ = uj.Parse( jsonBytes )
274 |
275 | //jsonCombined, _ := json.MarshalIndent(config, "", " ")
276 | //fmt.Printf("Combined config:%s\n", string( jsonCombined ) )
277 | //os.Exit(0)
278 |
279 | if config.ConfigPath != "" {
280 | configPath = config.ConfigPath
281 | continue
282 | }
283 | break
284 | }
285 | return &config
286 | }
--------------------------------------------------------------------------------
/coordinator/firewall.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "os/exec"
7 | "strings"
8 | log "github.com/sirupsen/logrus"
9 | )
10 |
11 | func firewall_ensureperm( findBin string ) {
12 | hasPerm := firewall_hasperm( findBin )
13 | if hasPerm {
14 | log.Warn("App already has firewall permissions: ", findBin )
15 | return
16 | }
17 | firewall_stop()
18 | firewall_addperm( findBin )
19 | firewall_start()
20 | }
21 |
22 | func firewall_hasperm( findBin string ) (bool) {
23 | curBins := firewall_getperms()
24 | for _, bin := range curBins {
25 | if bin == findBin {
26 | return true
27 | }
28 | }
29 | return false
30 | }
31 |
32 | func firewall_addperm( binary string ) {
33 | cmd := exec.Command( "/usr/libexec/ApplicationFirewall/socketfilterfw", "--add", binary )
34 | cmd.Stderr = os.Stderr
35 | cmd.Stdout = os.Stdout
36 | err := cmd.Run()
37 | if err != nil {
38 | log.Fatal( err )
39 | }
40 | }
41 |
42 | func firewall_stop() {
43 | cmd := exec.Command( "/usr/libexec/ApplicationFirewall/socketfilterfw", "--setglobalstate", "off" )
44 | cmd.Stderr = os.Stderr
45 | cmd.Stdout = os.Stdout
46 | err := cmd.Run()
47 | if err != nil {
48 | log.Fatal( err )
49 | }
50 | }
51 |
52 | func firewall_start() {
53 | cmd := exec.Command( "/usr/libexec/ApplicationFirewall/socketfilterfw", "--setglobalstate", "on" )
54 | cmd.Stderr = os.Stderr
55 | cmd.Stdout = os.Stdout
56 | err := cmd.Run()
57 | if err != nil {
58 | log.Fatal( err )
59 | }
60 | }
61 |
62 | func firewall_delperm( binary string ) {
63 | cmd := exec.Command( "/usr/libexec/ApplicationFirewall/socketfilterfw", "--remove", binary )
64 | cmd.Stderr = os.Stderr
65 | cmd.Stdout = os.Stdout
66 | err := cmd.Run()
67 | if err != nil {
68 | log.Fatal( err )
69 | }
70 | }
71 |
72 | func firewall_showperms() {
73 | bytes, _ := exec.Command( "/usr/libexec/ApplicationFirewall/socketfilterfw", "--listapps" ).Output()
74 | fmt.Printf( string( bytes ) )
75 | }
76 |
77 | func firewall_getperms() ( [] string ) {
78 | bytes, _ := exec.Command( "/usr/libexec/ApplicationFirewall/socketfilterfw", "--listapps" ).Output()
79 |
80 | lines := strings.Split( string(bytes), "\n" )
81 |
82 | var apps []string
83 | app := ""
84 | for _, line := range lines {
85 | colonPos := strings.Index( line, ":" )
86 | if colonPos != -1 {
87 | app = line[ colonPos + 3 : len( line ) - 1 ]
88 | } else {
89 | allowPos := strings.Index( line, "( Allow" )
90 | if allowPos != -1 {
91 | apps = append( apps, app )
92 | }
93 | }
94 | }
95 | log.Debug( "Cureent apps with permissions", apps )
96 | return apps
97 | }
--------------------------------------------------------------------------------
/coordinator/go.mod:
--------------------------------------------------------------------------------
1 | module coordinator.go
2 |
3 | go 1.12
4 |
5 | require (
6 | github.com/elastic/go-sysinfo v1.4.0
7 | github.com/fsnotify/fsnotify v1.4.7
8 | github.com/go-cmd/cmd v1.2.0
9 | github.com/jviney/go-proc v0.2.0
10 | github.com/nanoscopic/ujsonin v1.9.0
11 | github.com/sirupsen/logrus v1.4.2
12 | github.com/zeromq/goczmq v4.1.0+incompatible
13 | )
14 |
--------------------------------------------------------------------------------
/coordinator/heartbeat.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "time"
5 | //log "github.com/sirupsen/logrus"
6 | )
7 |
8 | func coro_heartbeat( uuid string, pubEventCh chan<- PubEvent ) ( chan<- bool ) {
9 | count := 1
10 | stopChannel := make(chan bool)
11 |
12 | // Start heartbeat
13 | go func() {
14 | done := false
15 | for {
16 | select {
17 | case <-stopChannel:
18 | done = true
19 | default:
20 | }
21 | if done {
22 | break
23 | }
24 |
25 | if count >= 2 {
26 | count = 0
27 |
28 | beatEvent := PubEvent{}
29 | beatEvent.action = 2
30 | beatEvent.uuid = uuid
31 | beatEvent.name = ""
32 | beatEvent.wdaPort = 0
33 | beatEvent.vidPort = 0
34 | pubEventCh <- beatEvent
35 |
36 | /*log.WithFields( log.Fields{
37 | "type": "heartbeat",
38 | } ).Info("Heartbeat")*/
39 | }
40 | time.Sleep( time.Second * 5 )
41 | count++;
42 | }
43 | }()
44 |
45 | return stopChannel
46 | }
--------------------------------------------------------------------------------
/coordinator/idevice.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "os/exec"
5 | "strings"
6 | "time"
7 | log "github.com/sirupsen/logrus"
8 | uj "github.com/nanoscopic/ujsonin/mod"
9 | )
10 |
11 | func getDeviceName( config *Config, uuid string ) (string) {
12 | i := 0
13 | var nameStr string
14 | for {
15 | i++
16 | if i > 10 { return "" }
17 |
18 | var name []byte
19 | if config.IosCLI == "ios-deploy" {
20 | name, _ = exec.Command( config.BinPaths.IosDeploy, "-i", uuid, "-g", "DeviceName" ).Output()
21 | } else {
22 | name, _ = exec.Command( "/usr/local/bin/idevicename", "-u", uuid ).Output()
23 | }
24 | if name == nil || len(name) == 0 {
25 | log.WithFields( log.Fields{
26 | "type": "ilib_getname_fail",
27 | "uuid": uuid,
28 | "try": i,
29 | } ).Debug("idevicename returned nothing")
30 |
31 | time.Sleep( time.Millisecond * 100 )
32 | continue
33 | }
34 | nameStr = string( name )
35 | break
36 | }
37 | nameStr = nameStr[:len(nameStr)-1]
38 | return nameStr
39 | }
40 |
41 | func getAllDeviceInfo( config *Config, uuid string ) map[string] string {
42 | info := make( map[string] string )
43 |
44 | if config.IosCLI == "ios-deploy" {
45 | mainKeys := "DeviceName,EthernetAddress,ModelNumber,HardwareModel,PhoneNumber,ProductType,ProductVersion,UniqueDeviceID,InternationalCircuitCardIdentity,InternationalMobileEquipmentIdentity,InternationalMobileSubscriberIdentity"
46 | keyArr := strings.Split( mainKeys, "," )
47 | output, _ := exec.Command( config.BinPaths.IosDeploy, "-j", "-i", uuid, "-g", mainKeys ).Output()
48 | root, _ := uj.Parse( output )
49 | for _, key := range keyArr {
50 | node := root.Get( key )
51 | if node != nil {
52 | info[ key ] = node.String()
53 | }
54 | }
55 | return info
56 | }
57 |
58 | rawInfo := getDeviceInfo( config, uuid, "" )
59 | lines := strings.Split( rawInfo, "\n" )
60 |
61 | for _, line := range lines {
62 | char1 := line[0:1]
63 | if char1 == " " { continue }
64 | colonPos := strings.Index( line, ":" )
65 | key := line[0:colonPos]
66 | val := line[(colonPos+2):]
67 | info[ key ] = val
68 | }
69 | return info
70 | }
71 |
72 | func getDeviceInfo( config *Config, uuid string, keyName string ) (string) {
73 | i := 0
74 | var nameStr string
75 | for {
76 | i++
77 | if i > 20 {
78 | log.WithFields( log.Fields{
79 | "type": "ilib_getinfo_fail",
80 | "uuid": uuid,
81 | "key": keyName,
82 | "try": i,
83 | } ).Error("ideviceinfo failed after 20 attempts over 20 seconds")
84 | return ""
85 | }
86 |
87 | ops := []string{}
88 | if uuid != "" {
89 | ops = append( ops, "-u", uuid )
90 | }
91 | if keyName != "" {
92 | ops = append( ops, "-k", keyName )
93 | }
94 |
95 | log.WithFields( log.Fields{
96 | "type": "ilib_getinfo_call",
97 | "ops": ops,
98 | } ).Info("ideviceinfo call")
99 |
100 | var name []byte
101 |
102 | if config.IosCLI == "ios-deploy" {
103 | if( keyName == "" ) {
104 | keyName = "DeviceName,EthernetAddress,ModelNumber,HardwareModel,PhoneNumber,ProductType,ProductVersion,UniqueDeviceID,InternationalCircuitCardIdentity,InternationalMobileEquipmentIdentity,InternationalMobileSubscriberIdentity"
105 | }
106 | name, _ = exec.Command( config.BinPaths.IosDeploy, "-i", uuid, "-g", keyName ).Output()
107 | } else {
108 | name, _ = exec.Command( "/usr/local/bin/ideviceinfo", ops... ).Output()
109 | }
110 |
111 | if name == nil || len(name) == 0 {
112 | log.WithFields( log.Fields{
113 | "type": "ilib_getinfo_fail",
114 | "uuid": uuid,
115 | "key": keyName,
116 | "try": i,
117 | } ).Debug("ideviceinfo returned nothing")
118 |
119 | time.Sleep( time.Millisecond * 1000 )
120 | continue
121 | }
122 | nameStr = string( name )
123 | break
124 | }
125 | nameStr = nameStr[:len(nameStr)-1]
126 | return nameStr
127 | }
128 |
129 | func getFirstDeviceId( config *Config ) ( string ) {
130 | deviceIds := getDeviceIds( config )
131 | return deviceIds[0]
132 | }
133 |
134 | func getDeviceIds( config *Config ) ( []string ) {
135 | if config.IosCLI == "ios-deploy" {
136 | ids := []string{}
137 | jsonText, _ := exec.Command( config.BinPaths.IosDeploy, "-d", "-j", "-t", "1" ).Output()
138 | root, _ := uj.Parse( []byte( "[" + string(jsonText) + "]" ) )
139 |
140 | root.ForEach( func( evNode *uj.JNode ) {
141 | ev := evNode.Get("Event").String()
142 | if ev == "DeviceDetected" {
143 | dev := evNode.Get("Device")
144 | ids = append( ids, dev.Get("DeviceIdentifier").String() )
145 | }
146 | } )
147 | return ids
148 | }
149 | output, _ := exec.Command( "/usr/local/bin/idevice_id", "-l" ).Output()
150 | lines := strings.Split( string(output), "\n" )
151 | return lines
152 | }
--------------------------------------------------------------------------------
/coordinator/launchctl.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "io/ioutil"
7 | "os"
8 | "os/exec"
9 | "os/user"
10 | "strconv"
11 | "strings"
12 | "sync"
13 | "text/template"
14 | log "github.com/sirupsen/logrus"
15 | //ps "github.com/jviney/go-proc"
16 | )
17 |
18 | type Launcher struct {
19 | label string
20 | arguments []string
21 | keepalive bool
22 | stdout string
23 | stderr string
24 | cwd string
25 | file string
26 | asRoot bool
27 | lock sync.Mutex
28 | }
29 |
30 | func NewLauncher( label string, arguments []string, keepalive bool, cwd string, asRoot bool ) (*Launcher) {
31 | file := label // strings.ReplaceAll( label, ".", "_" )
32 | user, _ := user.Current()
33 | if asRoot == true {
34 | file = fmt.Sprintf("/Library/LaunchDaemons/%s.plist", file)
35 | } else {
36 | file = fmt.Sprintf("%s/Library/LaunchAgents/%s.plist", user.HomeDir, file)
37 | }
38 | //strings.Replace
39 | launcher := Launcher{
40 | label: label,
41 | arguments: arguments,
42 | keepalive: keepalive,
43 | stdout: "/dev/null",
44 | stderr: "/dev/null",
45 | cwd: cwd,
46 | file: file,
47 | asRoot: asRoot,
48 | }
49 | return &launcher
50 | }
51 |
52 | func ( self *Launcher ) pid() (pid int) {
53 | //user, _ := user.Current()
54 | pid = 0
55 |
56 | //log.WithFields( log.Fields{ "type": "blah", "asroot": self.asRoot, "user": user.Username } ).Info("fdfsdfds")
57 |
58 | if self.asRoot { //&& user.Username != "root" {
59 | // trying to find information on a root owned plist, but not running as root
60 | // cannot use launchctl as a result :(
61 |
62 | fullCmdLine := strings.Join( self.arguments, " " )
63 |
64 | // This code doesn't work. Why? Who knows. Apparently go-proc can't retrieve processes run by root???
65 | /*procs := ps.GetAllProcessesInfo()
66 | for _, proc := range procs {
67 | testCmd := proc.Command + " " + strings.Join( proc.CommandLine, " " )
68 | //log.WithFields( log.Fields{ "type": "proc", "proc": testCmd } ).Info("proc")
69 | if proc.Pid == 15454 { //strings.Contains( testCmd, "openvpn" ) {
70 | log.WithFields( log.Fields{ "type": "testeq", "find": fullCmdLine, "have": testCmd } ).Info("testeq")
71 | }
72 | if testCmd == fullCmdLine {
73 | return proc.Pid
74 | }
75 | }*/
76 | log.WithFields( log.Fields{ "type": "launch_ps_scan", "cmdLine": fullCmdLine } ).
77 | Debug( "Scanning ps -Af for Cmd" )
78 |
79 | cmd := exec.Command("/bin/ps","-Af")
80 | output, _ := cmd.Output()
81 |
82 | lines := strings.Split( string( output ), "\n" )
83 | for _, line := range lines {
84 | parts := strings.Split( line, " " )
85 | var nodup [] string
86 | for _, part := range parts {
87 | if part != "" {
88 | nodup = append( nodup, part )
89 | }
90 | }
91 | line = strings.Join( nodup, " " )
92 |
93 | if strings.Contains( line, fullCmdLine ) {
94 | sp1 := strings.Index( line, " " )
95 | rest := line[ sp1: ]
96 | sp2 := strings.Index( rest, " " )
97 | rest = rest[ sp2 + 1 : ]
98 | sp3 := strings.Index( rest, " " )
99 | pid := rest[ 0: sp3 - 1 ]
100 | pidNum, _ := strconv.Atoi( pid )
101 | return pidNum
102 | }
103 | }
104 | } else {
105 | output, _ := exec.Command(fmt.Sprintf("/bin/launchctl list %s", self.label)).Output()
106 | lines := strings.Split( string(output), "\n" )
107 |
108 | log.WithFields( log.Fields{
109 | "type": "launch_ctl_list",
110 | "label": self.label,
111 | "output": output,
112 | } ).Debug( "Fetching launchctl info to get PID" )
113 |
114 | for _, line := range lines {
115 | if strings.Contains( line, "\"PID\"" ) {
116 | pos := strings.Index( line, "\"PID\"" )
117 | pos = pos + 7
118 |
119 | val := line[pos:len(line)-2]
120 | pid, _ = strconv.Atoi( val )
121 | }
122 | }
123 | }
124 | return pid
125 | }
126 |
127 | func ( self *Launcher ) load() {
128 | // unload the service if it is already loaded
129 | pid := self.pid()
130 | if pid != 0 {
131 | self.unload()
132 | }
133 |
134 | argx := ""
135 | for _, arg := range self.arguments {
136 | argx += fmt.Sprintf("%s", arg)
137 | }
138 |
139 | keepaliveX := ""
140 | if self.keepalive {
141 | keepaliveX = ""
142 | }
143 |
144 | limitSession := ""
145 | if self.asRoot != true {
146 | limitSession = "LimitLoadToSessionType\n Aqua\n"
147 | }
148 |
149 | var data bytes.Buffer
150 | launchTpl.Execute( &data, map[string] string {
151 | "label": self.label,
152 | "arguments": argx,
153 | "keepalive": keepaliveX,
154 | "stdout": self.stdout,
155 | "stderr": self.stderr,
156 | "cwd": self.cwd,
157 | "limitSession": limitSession,
158 | } )
159 |
160 | // create / recreate the plist file
161 | log.WithFields( log.Fields{
162 | "type": "launch_plist_write",
163 | "file": self.file,
164 | } ).Debug("Writing plist file")
165 | err := ioutil.WriteFile( self.file, data.Bytes(), 0600 )
166 | if err != nil {
167 | log.WithFields( log.Fields{
168 | "type": "launch_err",
169 | "file": self.file,
170 | "error": err,
171 | } ).Error("Error writing plist file")
172 | }
173 |
174 | // load it
175 | log.WithFields( log.Fields{
176 | "type": "launch_plist_load",
177 | "file": self.file,
178 | } ).Debug("Loading LaunchAgent")
179 | self.lock.Lock()
180 | c := exec.Command("/bin/launchctl","load",self.file)
181 | c.Stdout = os.Stdout
182 | c.Stderr = os.Stderr
183 | c.Run()
184 | self.lock.Unlock()
185 | }
186 |
187 | func ( self *Launcher ) unload() {
188 | // unload
189 | self.lock.Lock()
190 | exec.Command("/bin/launchctl","unload",self.file).Run()
191 |
192 | // delete the file
193 | os.Remove(self.file)
194 | self.lock.Unlock()
195 | }
196 |
197 | var launchTpl = template.Must(template.New("launchfile").Parse(`
198 |
199 |
200 |
201 |
202 | Label
203 | {{.label}}
204 |
205 | ProgramArguments
206 |
207 | {{.arguments}}
208 |
209 |
210 | KeepAlive
211 | {{.keepalive}}
212 |
213 | StandardOutPath
214 | {{.stdout}}
215 |
216 | StandardErrorPath
217 | {{.stderr}}
218 |
219 | WorkingDirectory
220 | {{.cwd}}
221 |
222 | {{.limitSession}}
223 |
224 |
225 | `))
--------------------------------------------------------------------------------
/coordinator/log.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "os"
7 | "os/signal"
8 | "strings"
9 | "syscall"
10 | "sync"
11 | "container/list"
12 | log "github.com/sirupsen/logrus"
13 | )
14 |
15 | type ProcTracker struct {
16 | que *list.List
17 | length int
18 | }
19 |
20 | type InMemTracker struct {
21 | procTrackers map[string] *ProcTracker
22 | }
23 |
24 | type JSONLog struct {
25 | file *os.File
26 | fileName string
27 | formatter *log.JSONFormatter
28 | failed bool
29 | hupData *HupData
30 | id int
31 | inMemTracker *InMemTracker
32 | mutex sync.Mutex
33 | }
34 |
35 | type HupData struct {
36 | hupA bool
37 | hupB bool
38 | mutex sync.Mutex
39 | }
40 |
41 | func ( hook *JSONLog ) Fire( entry *log.Entry ) error {
42 | // If we have failed to write to the file; don't bother trying
43 | if hook.failed { return nil }
44 |
45 | jsonformat, _ := hook.formatter.Format( entry )
46 |
47 | fh := hook.file
48 |
49 | doHup := false
50 | hupData := hook.hupData
51 | hupData.mutex.Lock()
52 | if hook.id == 1 {
53 | doHup = hupData.hupA
54 | if doHup { hupData.hupA = false }
55 | } else if hook.id == 2 {
56 | doHup = hupData.hupB
57 | if doHup { hupData.hupB = false }
58 | }
59 | hupData.mutex.Unlock()
60 |
61 | if doHup {
62 | fh.Close()
63 | fhnew, err := os.OpenFile( hook.fileName, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666 )
64 | if err != nil {
65 | fmt.Fprintf( os.Stderr, "Unable to open file for writing: %v", err )
66 | fh = nil
67 | }
68 | fh = fhnew
69 | hook.file = fh
70 |
71 | log.WithFields( log.Fields{
72 | "type": "sighup",
73 | "state": "reopen",
74 | "file": hook.fileName,
75 | } ).Info("HUP requested")
76 | //fmt.Fprintf( os.Stdout, "Hup %s\n", hook.fileName )
77 | }
78 |
79 | var err error
80 | if entry.Context != nil {
81 | // There is context; this is meant for the lines logfile
82 | str := string( jsonformat )
83 | str = strings.Replace( str, "\"level\":\"info\",", "", 1 )
84 | str = strings.Replace( str, "\"msg\":\"\",", "", 1 )
85 | _, err = fh.WriteString( str )
86 |
87 | // Possibly better to us a coroutine to accept new log messages
88 | // rather than lock on every one.
89 | hook.mutex.Lock()
90 | hook.inMemTracker.addEntry( entry, str )
91 | hook.mutex.Unlock()
92 | } else {
93 | _, err = fh.WriteString( string( jsonformat ) )
94 | }
95 |
96 | if err != nil {
97 | hook.failed = true
98 | fmt.Fprintf( os.Stderr, "Cannot write to logfile: %v", err )
99 | return err
100 | }
101 |
102 | return nil
103 | }
104 | func (tracker *InMemTracker) addEntry( entry *log.Entry, json string ) {
105 | if proc, ok := entry.Data["proc"]; ok {
106 | procS := proc.(string)
107 | var ok2 bool
108 | var pt *ProcTracker
109 | if pt, ok2 = tracker.procTrackers[ procS ]; !ok2 {
110 | pt = &ProcTracker{
111 | que: list.New(),
112 | length: 0,
113 | }
114 | tracker.procTrackers[ procS ] = pt
115 | }
116 |
117 | pt.length = pt.length + 1
118 | pt.que.PushBack( json )
119 |
120 | // Max out at 20 elements per queue
121 | if pt.length > 20 {
122 | e := pt.que.Front()
123 | pt.que.Remove(e)
124 | }
125 | }
126 | }
127 | func (hook *JSONLog) Levels() []log.Level {
128 | return []log.Level{ log.PanicLevel, log.FatalLevel, log.ErrorLevel, log.WarnLevel, log.InfoLevel, log.DebugLevel }
129 | }
130 | func AddJSONLog( logger *log.Logger, fileName string, id int, hupData *HupData ) ( *JSONLog ) {
131 | logFile, err := os.OpenFile( fileName, os.O_CREATE|os.O_APPEND|os.O_RDWR, 0666 )
132 | if err != nil {
133 | fmt.Fprintf( os.Stderr, "Unable to open file for writing: %v", err )
134 | }
135 |
136 | fileHook := JSONLog{
137 | file: logFile,
138 | fileName: fileName,
139 | formatter: &log.JSONFormatter{},
140 | failed: false,
141 | hupData: hupData,
142 | id: id,
143 | inMemTracker: NewInMemTracker(),
144 | }
145 |
146 | if logger == nil {
147 | log.AddHook( &fileHook )
148 | } else {
149 | logger.AddHook( &fileHook )
150 | }
151 | return &fileHook
152 | }
153 | func NewInMemTracker() ( *InMemTracker ) {
154 | newt := InMemTracker{
155 | procTrackers: make( map [string] *ProcTracker ),
156 | }
157 | return &newt
158 | }
159 | type DummyWriter struct {
160 | }
161 | func (self *DummyWriter) Write( p[]byte) (n int, err error) {
162 | return len(p), nil
163 | }
164 |
165 | func setup_log( config *Config, debug bool, jsonLog bool ) (*log.Entry, *InMemTracker) {
166 | if jsonLog {
167 | log.SetFormatter( &log.JSONFormatter{} )
168 | }
169 |
170 | lineLogger1 := log.New()
171 | dummyWriter := DummyWriter{}
172 | lineLogger1.SetOutput( &dummyWriter )
173 | lineLogger := lineLogger1.WithContext( context.Background() )
174 |
175 | if debug {
176 | log.WithFields( log.Fields{ "type": "debug_status" } ).Warn("Debugging enabled")
177 | log.SetLevel( log.DebugLevel )
178 | lineLogger1.SetLevel( log.DebugLevel )
179 | } else {
180 | log.SetLevel( log.InfoLevel )
181 | lineLogger1.SetLevel( log.InfoLevel )
182 | }
183 |
184 | hupData := coro_sighup()
185 |
186 | AddJSONLog( nil, config.Log.Main, 1, hupData )
187 | lineJsonLog := AddJSONLog( lineLogger1, config.Log.ProcLines, 2, hupData )
188 | lineTracker := lineJsonLog.inMemTracker
189 |
190 | return lineLogger, lineTracker
191 | }
192 |
193 | func coro_sighup() ( *HupData ) {
194 | hupData := HupData{
195 | hupA: false,
196 | hupB: false,
197 | }
198 | c := make(chan os.Signal, 2)
199 | signal.Notify(c, syscall.SIGHUP)
200 | go func() {
201 | for {
202 | <- c
203 | log.WithFields( log.Fields{
204 | "type": "sighup",
205 | "state": "begun",
206 | } ).Info("HUP requested")
207 | hupData.mutex.Lock()
208 | hupData.hupA = true
209 | hupData.hupB = true
210 | hupData.mutex.Unlock()
211 | }
212 | }()
213 | return &hupData
214 | }
--------------------------------------------------------------------------------
/coordinator/network.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "net"
6 | "os"
7 | "os/exec"
8 | "strings"
9 | "regexp"
10 | log "github.com/sirupsen/logrus"
11 | )
12 |
13 | func getDefaultIf() ( string ) {
14 | out, err := exec.Command( "/usr/sbin/netstat", "-nr", "-f", "inet" ).Output()
15 | if err != nil {
16 | fmt.Printf("Error from netstat: %s\n", err )
17 | return ""
18 | }
19 | lines := strings.Split( string(out), "\n" )
20 | iFace := ""
21 | space := regexp.MustCompile(`\s+`)
22 |
23 | for _, line := range lines {
24 | if strings.Contains( line, "default " ) {
25 | line = space.ReplaceAllString( line, " " )
26 |
27 | parts := strings.Split( line, " " )
28 | if parts[0] == "default" {
29 | iFace = parts[3]
30 | }
31 | }
32 | }
33 | return iFace
34 | }
35 |
36 | func ifAddr( ifName string ) ( addrOut string, okay bool ) {
37 | ifaces, err := net.Interfaces()
38 | if err != nil {
39 | fmt.Printf( err.Error() )
40 | os.Exit( 1 )
41 | }
42 |
43 | addrOut = ""
44 | for _, iface := range ifaces {
45 | addrs, err := iface.Addrs()
46 | if err != nil {
47 | fmt.Printf( err.Error() )
48 | os.Exit( 1 )
49 | }
50 | for _, addr := range addrs {
51 | var ip net.IP
52 | switch v := addr.(type) {
53 | case *net.IPNet:
54 | ip = v.IP
55 | case *net.IPAddr:
56 | ip = v.IP
57 | default:
58 | fmt.Printf("Unknown type\n")
59 | }
60 | if iface.Name == ifName {
61 | str := ip.String()
62 | if !strings.Contains(str,":") {
63 | addrOut = str
64 | }
65 | }
66 | }
67 | }
68 | if addrOut != "" {
69 | return addrOut, true
70 | }
71 | fmt.Printf("Network interface %s not found, exiting\n", ifName )
72 | return "", false
73 | }
74 |
75 | func get_net_info( config *Config ) ( string, string, bool ) {
76 | var vpnMissing bool = false
77 |
78 | // This information comes from Tunnelblick log
79 | // It may no longer be active
80 | tunName, curIP, err := vpn_info( config )
81 |
82 | if err != "" {
83 | log.WithFields( log.Fields{
84 | "type": "vpn_err",
85 | "err": err,
86 | } ).Info( err )
87 | return "", "", true
88 | }
89 |
90 | log.WithFields( log.Fields{
91 | "type": "info_vpn",
92 | "interface_name": tunName,
93 | } ).Info("VPN Info - interface")
94 |
95 | ipConfirm := getTunIP( tunName )
96 | if ipConfirm != curIP {
97 | // The tunnel is no longer active
98 | vpnMissing = true
99 | } else {
100 | log.WithFields( log.Fields{
101 | "type": "info_vpn",
102 | "interface_name": tunName,
103 | "ip": curIP,
104 | } ).Info("VPN Info - ip")
105 | }
106 |
107 | return tunName, curIP, vpnMissing
108 | }
109 |
110 | func ifaceCurIP( tunName string ) string {
111 | ipStr, _ := ifAddr( tunName )
112 | if ipStr != "" {
113 | log.WithFields( log.Fields{
114 | "type": "net_interface_info",
115 | "interface_name": tunName,
116 | "ip": ipStr,
117 | } ).Debug("Interface Details")
118 | } else {
119 | log.WithFields( log.Fields{
120 | "type": "err_net_interface",
121 | "interface_name": tunName,
122 | } ).Fatal("Could not find interface")
123 | }
124 |
125 | return ipStr
126 | }
--------------------------------------------------------------------------------
/coordinator/periodic.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "time"
5 | )
6 |
7 | func do_restart( config *Config, devd *RunningDev ) {
8 | if config.Stf.AdminToken != "" {
9 | stf_reserve( config, devd.uuid )
10 | }
11 | restart_wdaproxy( devd, false )
12 | wait_wdaup( devd )
13 | if config.Stf.AdminToken != "" {
14 | stf_release( config, devd.uuid )
15 | }
16 | }
17 |
18 | func test_restart_on_release( devd *RunningDev ) {
19 | restart_closure := func() { do_restart( devd.confDup, devd ) }
20 | stf_on_release( restart_closure )
21 | }
22 |
23 | func periodic_start( config *Config, devd *RunningDev ) {
24 | endChan := devd.periodic
25 | wdaRestartMinutes := config.Timing.WdaRestart
26 | go func() {
27 | minute := 0
28 | stop := false
29 | for {
30 | time.Sleep( time.Minute * 1 )
31 | minute++
32 | if wdaRestartMinutes != 0 {
33 | if ( minute % wdaRestartMinutes ) == 0 { // every 4 hours by default
34 | if devd.owner == "" {
35 | do_restart( config, devd )
36 | } else {
37 | restart_closure := func() { do_restart( config, devd ) }
38 | stf_on_release( restart_closure )
39 | }
40 | }
41 | }
42 | select {
43 | case <- endChan:
44 | stop = true
45 | break
46 | default:
47 | }
48 | if stop { break }
49 | }
50 | } ()
51 | }
52 |
53 | func periodic_stop( devd *RunningDev ) {
54 | endChan := devd.periodic
55 | endChan <- true
56 | }
--------------------------------------------------------------------------------
/coordinator/ports.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "sort"
5 | "strconv"
6 | "strings"
7 | log "github.com/sirupsen/logrus"
8 | )
9 |
10 | type PortItem struct {
11 | available bool
12 | }
13 |
14 | type PortMap struct {
15 | wdaPorts map[int] *PortItem
16 | vidPorts map[int] *PortItem
17 | devIosPorts map[int] *PortItem
18 | vncPorts map[int] *PortItem
19 | usbmuxdPorts map[int] *PortItem
20 | decodePorts map[int] *PortItem
21 | }
22 |
23 | func NewPortMap( config *Config ) ( *PortMap ) {
24 | wdaPorts := construct_ports( "WDA", config, config.Network.Wda )
25 | vidPorts := construct_ports( "Video", config, config.Network.Video )
26 | devIosPorts := construct_ports( "Dev IOS", config, config.Network.DevIos )
27 | vncPorts := construct_ports( "VNC", config, config.Network.Vnc )
28 | decodePorts := construct_ports( "Decode", config, config.Network.Decode )
29 | usbmuxdPorts := construct_ports( "usbmuxd", config, config.Network.Usbmuxd )
30 | portMap := PortMap {
31 | wdaPorts: wdaPorts,
32 | vidPorts: vidPorts,
33 | devIosPorts: devIosPorts,
34 | vncPorts: vncPorts,
35 | decodePorts: decodePorts,
36 | usbmuxdPorts: usbmuxdPorts,
37 | }
38 | return &portMap
39 | }
40 |
41 | func construct_ports( name string, config *Config, spec string ) ( map [int] *PortItem ) {
42 | ports := make( map [int] *PortItem )
43 | if strings.Contains( spec, "-" ) {
44 | parts := strings.Split( spec, "-" )
45 | from, _ := strconv.Atoi( parts[0] )
46 | to, _ := strconv.Atoi( parts[1] )
47 | for i := from; i <= to; i++ {
48 | portItem := PortItem{
49 | available: true,
50 | }
51 | ports[ i ] = &portItem
52 | }
53 | } else {
54 | log.WithFields( log.Fields{
55 | "type": "portmap",
56 | "related": name,
57 | "spec": spec,
58 | } ).Fatal("Invalid ports spec")
59 | }
60 | return ports
61 | }
62 |
63 | func map_keys( amap map[int] *PortItem ) ( []int ) {
64 | arr := make( []int, len(amap) )
65 | i := 0
66 | for k := range amap {
67 | arr[i] = k
68 | i++
69 | }
70 | sort.Ints( arr )
71 | return arr
72 | }
73 |
74 | func assign_port( amap map[int] *PortItem ) (int) {
75 | arr := map_keys( amap )
76 | for _,port := range arr {
77 | portItem := amap[port]
78 | if portItem.available {
79 | portItem.available = false
80 | return port
81 | }
82 | }
83 | return 0
84 | }
85 |
86 | func assign_ports( gConfig *Config, portMap *PortMap ) ( int,int,int,int,int,int,int,*Config ) {
87 | dupConfig := *gConfig
88 |
89 | wdaPort := assign_port( portMap.wdaPorts )
90 | dupConfig.WDAProxyPort = wdaPort
91 |
92 | vidPort := assign_port( portMap.vidPorts )
93 | dupConfig.MirrorFeedPort = vidPort
94 |
95 | devIosPort := assign_port( portMap.devIosPorts )
96 | dupConfig.DevIosPort = devIosPort
97 |
98 | vncPort := assign_port( portMap.vncPorts )
99 | dupConfig.VncPort = vncPort
100 |
101 | usbmuxdPort := assign_port( portMap.usbmuxdPorts )
102 | dupConfig.UsbmuxdPort = usbmuxdPort
103 |
104 | nanoOutPort := assign_port( portMap.decodePorts )
105 | nanoInPort := assign_port( portMap.decodePorts )
106 | dupConfig.DecodeOutPort = nanoOutPort
107 | dupConfig.DecodeInPort = nanoInPort
108 |
109 | return wdaPort, vidPort, devIosPort, vncPort, usbmuxdPort, nanoOutPort, nanoInPort, &dupConfig
110 | }
111 |
112 | func free_ports(
113 | wdaPort int,
114 | vidPort int,
115 | devIosPort int,
116 | vncPort int,
117 | usbmuxdPort int,
118 | portMap *PortMap ) {
119 | wdaItem := portMap.wdaPorts[ wdaPort ]
120 | wdaItem.available = true
121 |
122 | vidItem := portMap.vidPorts[ vidPort ]
123 | vidItem.available = true
124 |
125 | dItem := portMap.devIosPorts[ devIosPort ]
126 | dItem.available = true
127 |
128 | vncItem := portMap.vncPorts[ vncPort ]
129 | vncItem.available = true
130 |
131 | usbmuxdItem := portMap.usbmuxdPorts[ usbmuxdPort ]
132 | usbmuxdItem.available = true
133 | }
--------------------------------------------------------------------------------
/coordinator/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 | }
--------------------------------------------------------------------------------
/coordinator/proc_device_trigger.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | )
5 |
6 | func proc_device_trigger( o ProcOptions ) {
7 | o.procName = "device_trigger"
8 |
9 | conf := o.config
10 | if conf.DeviceDetector == "api" {
11 | o.binary = o.config.BinPaths.IosDeploy
12 | o.args = []string{
13 | "-d",
14 | "-n", "test",
15 | "-t", "0",
16 | }
17 | } else {
18 | o.binary = o.config.BinPaths.DeviceTrigger
19 | }
20 |
21 | proc_generic( o )
22 | }
--------------------------------------------------------------------------------
/coordinator/proc_device_unit.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "strconv"
7 | "strings"
8 | log "github.com/sirupsen/logrus"
9 | )
10 |
11 | func restart_device_unit( devd *RunningDev ) {
12 | restart_proc_generic( devd, "stf_device_ios" )
13 | }
14 |
15 | var onRelease func()
16 | func stf_on_release( newOnRelease func() ) {
17 | onRelease = newOnRelease
18 | }
19 |
20 | func proc_device_ios_unit( o ProcOptions, uuid string, curIP string) {
21 | vncPort := 0
22 | if o.config.Video.UseVnc && o.config.Video.Enabled {
23 | vncPort = o.devd.vncPort
24 | }
25 |
26 | secure := o.config.FrameServer.Secure
27 | var frameServer string
28 | if secure {
29 | frameServer = fmt.Sprintf("wss://%s:%d/echo", curIP, o.devd.vidPort)
30 | } else {
31 | frameServer = fmt.Sprintf("ws://%s:%d/echo", curIP, o.devd.vidPort)
32 | }
33 |
34 | curDir, _ := os.Getwd()
35 |
36 | o.args = []string{
37 | fmt.Sprintf("--inspect=0.0.0.0:%d", o.devd.devIosPort),
38 | "runmod.js" , "device-ios",
39 | "--serial" , uuid,
40 | "--name" , o.devd.name,
41 | "--connect-push" , fmt.Sprintf("tcp://%s:7270", o.config.Stf.Ip),
42 | "--connect-sub" , fmt.Sprintf("tcp://%s:7250", o.config.Stf.Ip),
43 | "--connect-port" , strconv.Itoa( o.devd.usbmuxdPort ),
44 | "--public-ip" , curIP,
45 | "--wda-port" , strconv.Itoa( o.devd.wdaPort ),
46 | "--storage-url" , fmt.Sprintf("https://%s", o.config.Stf.HostName),
47 | "--screen-ws-url-pattern", fmt.Sprintf("wss://%s/frames/%s/%d/x", o.config.Stf.HostName, curIP, o.devd.vidPort),
48 | //"--screen-ws-url-pattern", frameServer,
49 | "--vnc-password" , o.config.Video.VncPassword,
50 | "--vnc-port" , strconv.Itoa( vncPort ),
51 | "--vnc-scale" , strconv.Itoa( o.config.Video.VncScale ),
52 | "--stream-width" , strconv.Itoa( o.devd.streamWidth ),
53 | "--stream-height" , strconv.Itoa( o.devd.streamHeight ),
54 | "--click-width" , strconv.Itoa( o.devd.clickWidth ),
55 | "--click-height" , strconv.Itoa( o.devd.clickHeight ),
56 | "--click-scale" , strconv.Itoa( o.devd.clickScale ),
57 | "--ios-deploy-path" , ( curDir + "/" + o.config.BinPaths.IosDeploy ),
58 | }
59 | o.startFields = log.Fields {
60 | "server_ip": o.config.Stf.Ip,
61 | "client_ip": curIP,
62 | "server_host": o.config.Stf.HostName,
63 | "video_port": o.devd.vidPort,
64 | "node_port": o.devd.devIosPort,
65 | "device_name": o.devd.name,
66 | "vnc_scale": o.config.Video.VncScale,
67 | "stream_width": o.devd.streamWidth,
68 | "stream_height": o.devd.streamHeight,
69 | "clickScale": o.devd.clickScale,
70 | "clickWidth": o.devd.clickWidth,
71 | "clickHeight": o.devd.clickHeight,
72 | "frame_server": frameServer,
73 | }
74 |
75 | devd := o.devd
76 | o.stderrHandler = func( line string, plog *log.Entry ) (bool) {
77 | if strings.Contains( line, "Now owned by" ) {
78 | pos := strings.Index( line, "Now owned by" )
79 | pos += len( "Now owned by" ) + 2
80 | ownedStr := line[ pos: ]
81 | endpos := strings.Index( ownedStr, "\"" )
82 | owner := ownedStr[ :endpos ]
83 | plog.WithFields( log.Fields{
84 | "type": "wda_owner_start",
85 | "owner": owner,
86 | } ).Info("Device Owner Start")
87 | devd.owner = owner
88 | }
89 | if strings.Contains( line, "No longer owned by" ) {
90 | pos := strings.Index( line, "No longer owned by" )
91 | pos += len( "No longer owned by" ) + 2
92 | ownedStr := line[ pos: ]
93 | endpos := strings.Index( ownedStr, "\"" )
94 | owner := ownedStr[ :endpos ]
95 | plog.WithFields( log.Fields{
96 | "type": "wda_owner_stop",
97 | "owner": owner,
98 | } ).Info("Device Owner Stop")
99 | devd.owner = ""
100 | if onRelease != nil {
101 | onRelease()
102 | onRelease = nil
103 | }
104 | }
105 | if strings.Contains( line, "responding with identity" ) {
106 | plog.WithFields( log.Fields{
107 | "type": "device_ios_ident",
108 | } ).Debug("Device IOS Unit Registered Identity")
109 | }
110 | if strings.Contains( line, "Sent ready message" ) {
111 | plog.WithFields( log.Fields{
112 | "type": "device_ios_ready",
113 | } ).Debug("Device IOS Unit Ready")
114 | }
115 | return true
116 | }
117 | o.procName = "stf_device_ios"
118 | o.binary = "/usr/local/opt/node@12/bin/node"
119 | o.startDir = "./repos/stf-ios-provider"
120 | proc_generic( o )
121 | }
122 |
123 |
--------------------------------------------------------------------------------
/coordinator/proc_generic.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | log "github.com/sirupsen/logrus"
5 | gocmd "github.com/go-cmd/cmd"
6 | "time"
7 | )
8 |
9 | type OutputHandler func( string, *log.Entry ) (bool)
10 |
11 | type ProcOptions struct {
12 | config *Config
13 | baseProgs *BaseProgs
14 | devd *RunningDev
15 | lineLog *log.Entry
16 | procName string
17 | binary string
18 | args []string
19 | stderrHandler OutputHandler
20 | stdoutHandler OutputHandler
21 | startFields log.Fields
22 | startDir string
23 | env map[string]string
24 | curIP string
25 | noRestart bool
26 | noWait bool
27 | onStop func( *RunningDev )
28 | }
29 |
30 | type GPMsg struct {
31 | msgType int
32 | }
33 |
34 | type GenericProc struct {
35 | controlCh chan GPMsg
36 | backoff *Backoff
37 | pid int
38 | cmd *gocmd.Cmd
39 | }
40 |
41 | func (self *GenericProc) Kill() {
42 | if self.cmd == nil { return }
43 | self.controlCh <- GPMsg{ msgType: 1 }
44 | }
45 |
46 | func (self *GenericProc) Restart() {
47 | if self.cmd == nil { return }
48 | self.controlCh <- GPMsg{ msgType: 2 }
49 | }
50 |
51 | func restart_proc_generic( devd *RunningDev, name string ) {
52 | genProc := devd.process[ name ]
53 | genProc.Restart()
54 | }
55 |
56 | func proc_generic( opt ProcOptions ) ( *GenericProc ) {
57 | controlCh := make( chan GPMsg )
58 | proc := GenericProc {
59 | controlCh: controlCh,
60 | }
61 |
62 | devd := opt.devd
63 |
64 | var plog *log.Entry
65 | var lineLog *log.Entry
66 | if devd != nil {
67 | plog = log.WithFields( log.Fields{
68 | "proc": opt.procName,
69 | "uuid": censor_uuid( devd.uuid ),
70 | } )
71 | lineLog = opt.lineLog.WithFields( log.Fields{
72 | "proc": opt.procName,
73 | "uuid": censor_uuid( devd.uuid ),
74 | } )
75 | devd.lock.Lock()
76 | devd.process[ opt.procName ] = &proc
77 | devd.lock.Unlock()
78 | } else {
79 | plog = log.WithFields( log.Fields{ "proc": opt.procName } )
80 | lineLog = opt.lineLog.WithFields( log.Fields{ "proc": opt.procName } )
81 | opt.baseProgs.lock.Lock()
82 | opt.baseProgs.process[ opt.procName ] = &proc
83 | opt.baseProgs.lock.Unlock()
84 | }
85 |
86 | backoff := Backoff{}
87 | proc.backoff = &backoff
88 |
89 | stop := false
90 |
91 | go func() { for {
92 | startFields := log.Fields{
93 | "type": "proc_start",
94 | "binary": opt.binary,
95 | }
96 | if opt.startFields != nil {
97 | for k, v := range opt.startFields {
98 | startFields[k] = v
99 | }
100 | }
101 |
102 | plog.WithFields( startFields ).Info("Process start - " + opt.procName)
103 |
104 | cmd := gocmd.NewCmdOptions( gocmd.Options{ Streaming: true }, opt.binary, opt.args... )
105 | proc.cmd = cmd
106 |
107 | if opt.startDir != "" {
108 | cmd.Dir = opt.startDir
109 | }
110 |
111 | if opt.env != nil {
112 | var envArr []string
113 | for k,v := range( opt.env ) {
114 | envArr = append( envArr, k, v )
115 | }
116 | cmd.Env = envArr
117 | }
118 |
119 | backoff.markStart()
120 |
121 | statCh := cmd.Start()
122 |
123 | i := 0
124 | for {
125 | status := cmd.Status()
126 |
127 | if status.Error != nil {
128 | plog.WithFields( log.Fields{
129 | "type": "proc_err",
130 | "error": status.Error,
131 | } ).Error("Error starting - " + opt.procName)
132 |
133 | return
134 | }
135 |
136 | if status.Exit != -1 {
137 | plog.WithFields( log.Fields{
138 | "type": "proc_exit",
139 | "exit": status.Exit,
140 | } ).Error("Error starting - " + opt.procName)
141 |
142 | return
143 | }
144 |
145 | proc.pid = status.PID
146 | if proc.pid != 0 {
147 | break
148 | }
149 | time.Sleep(50 * time.Millisecond)
150 | if i > 4 {
151 | break
152 | }
153 | }
154 |
155 | plog.WithFields( log.Fields{
156 | "type": "proc_pid",
157 | "pid": proc.pid,
158 | } ).Debug("Process pid")
159 |
160 | outStream := cmd.Stdout
161 | errStream := cmd.Stderr
162 |
163 | runDone := false
164 | for {
165 | select {
166 | case <- statCh:
167 | runDone = true
168 | case msg := <- controlCh:
169 | plog.Debug("Got stop request on control channel")
170 | if msg.msgType == 1 { // stop
171 | stop = true
172 | proc.cmd.Stop()
173 | } else if msg.msgType == 2 { // restart
174 | proc.cmd.Stop()
175 | }
176 | case line := <- outStream:
177 | doLog := true
178 | if opt.stdoutHandler != nil { doLog = opt.stdoutHandler( line, plog ) }
179 | if doLog { lineLog.WithFields( log.Fields{ "line": line } ).Info(""); }
180 | case line := <- errStream:
181 | doLog := true
182 | if opt.stderrHandler != nil { doLog = opt.stderrHandler( line, plog ) }
183 | if doLog { lineLog.WithFields( log.Fields{ "line": line, "iserr": true } ).Info("") }
184 | }
185 | if runDone { break }
186 | }
187 |
188 | proc.cmd = nil
189 |
190 | backoff.markEnd()
191 |
192 | plog.WithFields( log.Fields{ "type": "proc_end" } ).Warn("Process end - "+ opt.procName)
193 |
194 | if opt.onStop != nil {
195 | opt.onStop( devd )
196 | }
197 |
198 | if opt.noRestart {
199 | plog.Debug( "No restart requested" )
200 | break
201 | }
202 |
203 | if stop { break }
204 |
205 | if !opt.noWait {
206 | backoff.wait()
207 | } else {
208 | plog.Debug("No wait requested")
209 | }
210 | } }()
211 |
212 | return &proc
213 | }
--------------------------------------------------------------------------------
/coordinator/proc_h264_to_jpeg.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | "strings"
7 | log "github.com/sirupsen/logrus"
8 | )
9 |
10 | func proc_h264_to_jpeg( o ProcOptions ) {
11 | devd := o.devd.dup()
12 | udid := devd.uuid
13 |
14 | nanoIn := o.config.DecodeInPort
15 | nanoOut := o.config.DecodeOutPort
16 |
17 | inSpec := fmt.Sprintf("tcp://127.0.0.1:%d", nanoIn)
18 | outSpec := fmt.Sprintf("tcp://127.0.0.1:%d", nanoOut)
19 |
20 | o.binary = o.config.BinPaths.H264ToJpeg
21 | o.startFields = log.Fields {
22 | "h264SrcSpec": outSpec,
23 | "jpegDestSpec": inSpec,
24 | }
25 | o.procName = "h264_to_jpeg"
26 | o.args = []string {
27 | "nano",
28 | "--in", outSpec,
29 | "--out", inSpec,
30 | "--frameSkip", "2",
31 | "--cacheid", udid,
32 | }
33 |
34 | width := o.config.FrameServer.Width
35 | height := o.config.FrameServer.Height
36 |
37 | if width != 0 {
38 | o.args = append( o.args, "--dw", strconv.Itoa( width ) )
39 | }
40 | if height != 0 {
41 | o.args = append( o.args, "--dh", strconv.Itoa( height ) )
42 | }
43 |
44 | o.stdoutHandler = func( line string, plog *log.Entry ) (bool) {
45 | if strings.Contains( line, "Iframe - size:" ) {
46 | return false
47 | }
48 | return true
49 | }
50 |
51 | proc_generic( o )
52 | }
--------------------------------------------------------------------------------
/coordinator/proc_ios_video_pull.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | //"strconv"
6 | "strings"
7 | log "github.com/sirupsen/logrus"
8 | )
9 |
10 | func proc_ios_video_pull( o ProcOptions ) {
11 | devd := o.devd.dup()
12 | udid := devd.uuid
13 |
14 | nanoOut := o.config.DecodeOutPort
15 |
16 | outSpec := fmt.Sprintf("tcp://127.0.0.1:%d", nanoOut)
17 |
18 | o.binary = o.config.BinPaths.IosVideoPull
19 | o.startFields = log.Fields {
20 | "pushSpec": outSpec,
21 | }
22 | o.procName = "ios_video_pull"
23 | o.args = []string {
24 | "-pull",
25 | "-udid", udid,
26 | "-pushSpec", outSpec,
27 | }
28 | o.stdoutHandler = func( line string, plog *log.Entry ) (bool) {
29 | if strings.Contains( line, "error: libusb: interrupted" ) {
30 | return false
31 | }
32 | return true
33 | }
34 | proc_generic( o )
35 | }
--------------------------------------------------------------------------------
/coordinator/proc_ios_video_stream.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "strconv"
6 | log "github.com/sirupsen/logrus"
7 | )
8 |
9 | func restart_ios_video_stream( devd *RunningDev ) {
10 | restart_proc_generic( devd, "ios_video_stream" )
11 | }
12 |
13 | func proc_ios_video_stream( o ProcOptions, tunName string, frameInIp string ) {
14 | devd := o.devd.dup()
15 | udid := devd.uuid
16 | port := o.config.MirrorFeedPort
17 |
18 | nanoIn := o.config.DecodeInPort
19 |
20 | inSpec := fmt.Sprintf("tcp://%s:%d", frameInIp, nanoIn)
21 |
22 | coordinator := fmt.Sprintf( "127.0.0.1:%d", o.config.Network.Coordinator )
23 |
24 | o.binary = o.config.BinPaths.IosVideoStream
25 | o.startFields = log.Fields {
26 | "tunName": tunName,
27 | "pullSpec": inSpec,
28 | "port": port,
29 | }
30 | o.procName = "ios_video_stream"
31 | o.args = []string {
32 | "-stream",
33 | "-port", strconv.Itoa( port ),
34 | "-udid", udid,
35 | "-interface", tunName,
36 | "-pullSpec", inSpec,
37 | "-coordinator", coordinator,
38 | }
39 | secure := o.config.FrameServer.Secure
40 | if secure {
41 | cert := o.config.FrameServer.Cert
42 | key := o.config.FrameServer.Key
43 | o.args = append( o.args,
44 | "--secure",
45 | "--cert", cert,
46 | "--key", key,
47 | )
48 | }
49 | proc_generic( o )
50 | }
--------------------------------------------------------------------------------
/coordinator/proc_ivf.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | //"strconv"
6 | //"strings"
7 | log "github.com/sirupsen/logrus"
8 | )
9 |
10 | func restart_ivf( devd *RunningDev ) {
11 | restart_proc_generic( devd, "ivf" )
12 | }
13 |
14 | func proc_ivf( o ProcOptions ) {
15 | devd := o.devd.dup()
16 | udid := devd.uuid
17 |
18 | nanoIn := o.config.DecodeInPort
19 | toStreamSpec := fmt.Sprintf("tcp://127.0.0.1:%d", nanoIn)
20 |
21 | o.binary = o.config.BinPaths.IVF
22 | o.startFields = log.Fields {
23 | "outSpec": toStreamSpec,
24 | }
25 | o.procName = "ivf"
26 | o.args = []string {
27 | "nano",
28 | "--udid", udid,
29 | "--out", toStreamSpec,
30 | "--frameSkip", "2",
31 | }
32 | proc_generic( o )
33 | }
--------------------------------------------------------------------------------
/coordinator/proc_stf_provider.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "strings"
7 | log "github.com/sirupsen/logrus"
8 | )
9 |
10 | func proc_stf_provider( o ProcOptions, curIP string ) {
11 | o.binary = o.config.BinPaths.IosVideoStream
12 |
13 | serverHostname := o.config.Stf.HostName
14 | clientHostname, _ := os.Hostname()
15 | serverIP := o.config.Stf.Ip
16 |
17 | location := fmt.Sprintf("macmini/%s", clientHostname)
18 | if o.config.Stf.Location != "" {
19 | location = o.config.Stf.Location
20 | }
21 |
22 | o.startFields = log.Fields {
23 | "client_ip": curIP,
24 | "server_ip": serverIP,
25 | "client_hostname": clientHostname,
26 | "server_hostname": serverHostname,
27 | "location": location,
28 | }
29 | o.binary = "/usr/local/opt/node@12/bin/node"
30 | o.args = []string {
31 | "--inspect=127.0.0.1:9230",
32 | "runmod.js" , "provider",
33 | "--name" , location,
34 | "--connect-sub" , fmt.Sprintf("tcp://%s:7250", serverIP),
35 | "--connect-push" , fmt.Sprintf("tcp://%s:7270", serverIP),
36 | "--storage-url" , fmt.Sprintf("https://%s", serverHostname),
37 | "--public-ip" , curIP,
38 | "--min-port=7400",
39 | "--max-port=7700",
40 | "--heartbeat-interval=10000",
41 | "--server-ip" , serverIP,
42 | "--no-cleanup",
43 | }
44 | o.procName = "stf_ios_provider"
45 | o.startDir = "./repos/stf-ios-provider"
46 | o.stdoutHandler = func( line string, plog *log.Entry ) (bool) {
47 | if strings.Contains( line, " IOS Heartbeat:" ) {
48 | return false
49 | }
50 | return true
51 | }
52 | proc_generic( o )
53 | }
--------------------------------------------------------------------------------
/coordinator/proc_video_enabler.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | )
5 |
6 | func proc_video_enabler( o ProcOptions ) {
7 | o.procName = "video_enabler"
8 | o.binary = o.config.BinPaths.VideoEnabler
9 | proc_generic( o )
10 | }
--------------------------------------------------------------------------------
/coordinator/proc_vnc_proxy.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "strconv"
5 | log "github.com/sirupsen/logrus"
6 | )
7 |
8 | func proc_vnc_proxy( o ProcOptions ) {
9 | o.procName = "vnc_proxy"
10 |
11 | vncPort := o.config.VncPort
12 | o.binary = o.config.BinPaths.Iproxy
13 | o.startFields = log.Fields {
14 | "vncPort": vncPort,
15 | }
16 | o.args = []string {
17 | strconv.Itoa( vncPort ), "5900",
18 | }
19 | proc_generic( o )
20 | }
--------------------------------------------------------------------------------
/coordinator/proc_wdaproxy.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | log "github.com/sirupsen/logrus"
6 | "strconv"
7 | "strings"
8 | "time"
9 | )
10 |
11 | func restart_wdaproxy( devd *RunningDev, onRelease bool ) {
12 | if onRelease {
13 | test_restart_on_release( devd )
14 | return
15 | }
16 | restart_proc_generic( devd, "wdaproxy" )
17 | }
18 | func wait_wdaup( devd *RunningDev ) {
19 | for {
20 | if devd.wda == true { break }
21 | time.Sleep( time.Second * 10 )
22 | }
23 | }
24 |
25 | func proc_wdaproxy( o ProcOptions, devEventCh chan<- DevEvent, temp bool ) {
26 | uuid := o.devd.uuid
27 | config := o.config
28 |
29 | if temp {
30 | o.procName = "wdaproxytemp"
31 | o.noRestart = true
32 | o.noWait = false
33 | } else {
34 | o.procName = "wdaproxy"
35 | o.noWait = true
36 | o.noRestart = false
37 | }
38 |
39 | o.binary = "../wdaproxy" //o.config.BinPaths.WdaProxy
40 | o.startFields = log.Fields {
41 | "wdaPort": o.config.WDAProxyPort,
42 | "iosVersion": o.devd.iosVersion,
43 | "--iosDeploy": config.BinPaths.IosDeploy,
44 | }
45 | o.args = []string {
46 | "-p", strconv.Itoa(o.config.WDAProxyPort),
47 | "-q", strconv.Itoa(8100),//o.config.WDAProxyPort),
48 | "-d",
49 | fmt.Sprintf("--iosDeploy=%s", config.BinPaths.IosDeploy),
50 | fmt.Sprintf("--mobileDevice=%s", "/usr/local/bin/mobiledevice"),
51 | "-W", ".",
52 | "-u", uuid,
53 | fmt.Sprintf("--iosversion=%s", o.devd.iosVersion),
54 | }
55 | o.startDir = o.config.WdaFolder
56 |
57 | o.stdoutHandler = func( line string, plog *log.Entry ) (bool) {
58 | if strings.Contains( line, "TEST EXECUTE FAILED" ) {
59 | plog.WithFields( log.Fields{
60 | "type": "wda_failed",
61 | } ).Error("WDA Failed")
62 |
63 | devEventCh <- DevEvent{
64 | action: 5,
65 | uuid: uuid,
66 | }
67 | }
68 | return true
69 | }
70 |
71 | devd := o.devd
72 | o.stderrHandler = func( line string, plog *log.Entry ) (bool) {
73 | if strings.Contains( line, "[WDA] successfully started" ) {
74 | plog.WithFields( log.Fields{
75 | "type": "wda_started",
76 | } ).Info("WDA Running")
77 | devd.lock.Lock()
78 | devd.wda = true
79 | devd.lock.Unlock()
80 |
81 | devEventCh <- DevEvent{
82 | action: 4,
83 | uuid: uuid,
84 | }
85 | }
86 | return true
87 | }
88 | o.onStop = func( devd *RunningDev ) {
89 | devd.lock.Lock()
90 | devd.wda = false
91 | devd.lock.Unlock()
92 | }
93 |
94 | proc_generic( o )
95 | }
--------------------------------------------------------------------------------
/coordinator/shutdown.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "os/signal"
7 | //"strings"
8 | "syscall"
9 | "time"
10 | log "github.com/sirupsen/logrus"
11 | //ps "github.com/jviney/go-proc"
12 | si "github.com/elastic/go-sysinfo"
13 | )
14 |
15 | func cleanup_procs(config *Config) {
16 | plog := log.WithFields( log.Fields{
17 | "type": "proc_cleanup",
18 | } )
19 |
20 | procMap := map[string]string {
21 | "ios_video_stream": config.BinPaths.IosVideoStream,
22 | "device_trigger": config.BinPaths.DeviceTrigger,
23 | "h264_to_jpeg": config.BinPaths.H264ToJpeg,
24 | "wdaproxy": "../wdaproxy",
25 | "ivf": config.BinPaths.IVF,
26 | "ios-deploy": config.BinPaths.IosDeploy,
27 | }
28 |
29 | // Cleanup hanging processes if any
30 | procs, listErr := si.Processes()
31 | if listErr != nil {
32 | fmt.Printf( "listErr:%s\n", listErr )
33 | os.Exit(1)
34 | }
35 |
36 | var hangingPids []int
37 |
38 | for _, proc := range procs {
39 | info, infoErr := proc.Info()
40 | if infoErr != nil {
41 | //fmt.Printf( "infoErr:%s\n", infoErr )
42 | continue
43 | }
44 |
45 | cmd := info.Args
46 | //cmdFlat := strings.Join( cmd, " " )
47 |
48 | for k,v := range procMap {
49 | if cmd[0] == v {
50 | pid := proc.PID()
51 | plog.WithFields( log.Fields{
52 | "proc": k,
53 | "pid": pid,
54 | } ).Warn("Leftover " + k + " - Sending SIGTERM")
55 |
56 | syscall.Kill( pid, syscall.SIGTERM )
57 | hangingPids = append( hangingPids, pid )
58 | }
59 | }
60 |
61 | /*if strings.Contains( cmdFlat, "node" ) {
62 | log.WithFields( log.Fields{
63 | "cmdLine": cmdFlat,
64 | } ).Info("Leftover Node proc")
65 | }*/
66 |
67 | // node --inspect=[ip]:[port] runmod.js device-ios
68 | if cmd[0] == "/usr/local/opt/node@12/bin/node" && cmd[3] == "device-ios" {
69 | pid := proc.PID()
70 |
71 | plog.WithFields( log.Fields{
72 | "proc": "device-ios",
73 | "pid": pid,
74 | } ).Warn("Leftover Proc - Sending SIGTERM")
75 |
76 | syscall.Kill( pid, syscall.SIGTERM )
77 | hangingPids = append( hangingPids, pid )
78 | }
79 |
80 | // node --inspect=[ip]:[port] runmod.js provider
81 | if cmd[0] == "/usr/local/opt/node@12/bin/node" && cmd[3] == "provider" {
82 | pid := proc.PID()
83 |
84 | plog.WithFields( log.Fields{
85 | "proc": "stf_provider",
86 | "pid": pid,
87 | } ).Warn("Leftover Proc - Sending SIGTERM")
88 |
89 | syscall.Kill( pid, syscall.SIGTERM )
90 | hangingPids = append( hangingPids, pid )
91 | }
92 | }
93 |
94 | if len( hangingPids ) > 0 {
95 | // Give the processes half a second to shudown cleanly
96 | time.Sleep( time.Millisecond * 500 )
97 |
98 | // Send kill to processes still around
99 | for _, pid := range( hangingPids ) {
100 | proc, _ := si.Process( pid )
101 | if proc != nil {
102 | info, infoErr := proc.Info()
103 | arg0 := "unknown"
104 | if infoErr == nil {
105 | args := info.Args
106 | arg0 = args[0]
107 | } else {
108 | // If the process vanished before here; it errors out fetching info
109 | continue
110 | }
111 |
112 | plog.WithFields( log.Fields{
113 | "arg0": arg0,
114 | } ).Warn("Leftover Proc - Sending SIGKILL")
115 | syscall.Kill( pid, syscall.SIGKILL )
116 | }
117 | }
118 |
119 | // Spend up to 500 ms waiting for killed processes to vanish
120 | i := 0
121 | for {
122 | i = i + 1
123 | time.Sleep( time.Millisecond * 100 )
124 | allGone := 1
125 | for _, pid := range( hangingPids ) {
126 | proc, _ := si.Process( pid )
127 | if proc != nil {
128 | _, infoErr := proc.Info()
129 | if infoErr != nil {
130 | continue
131 | }
132 | allGone = 0
133 | }
134 | }
135 | if allGone == 1 && i > 5 {
136 | break
137 | }
138 | }
139 |
140 | // Write out error messages for processes that could not be killed
141 | for _, pid := range( hangingPids ) {
142 | proc, _ := si.Process( pid )
143 | if proc != nil {
144 | info, infoErr := proc.Info()
145 | arg0 := "unknown"
146 | if infoErr != nil {
147 | continue
148 | }
149 | args := info.Args
150 | arg0 = args[0]
151 |
152 | plog.WithFields( log.Fields{
153 | "arg0": arg0,
154 | } ).Error("Kill attempted and failed")
155 | }
156 | }
157 | }
158 | }
159 |
160 | func closeAllRunningDevs( runningDevs map [string] *RunningDev ) {
161 | for _, devd := range runningDevs {
162 | closeRunningDev( devd, nil )
163 | }
164 | }
165 |
166 | func closeRunningDev( devd *RunningDev, portMap *PortMap ) {
167 | devd.lock.Lock()
168 | devd.shuttingDown = true
169 | devd.lock.Unlock()
170 |
171 | if portMap != nil {
172 | free_ports( devd.wdaPort, devd.vidPort, devd.devIosPort, devd.vncPort, devd.usbmuxdPort, portMap )
173 | }
174 |
175 | plog := log.WithFields( log.Fields{
176 | "type": "proc_cleanup_kill",
177 | "uuid": censor_uuid( devd.uuid ),
178 | } )
179 |
180 | plog.Info("Closing running dev")
181 |
182 | for k,v := range( devd.process ) {
183 | plog.WithFields( log.Fields{ "proc": k } ).Debug("Killing "+k)
184 | if v != nil { v.Kill() }
185 | }
186 | }
187 |
188 | func closeBaseProgs( baseProgs *BaseProgs ) {
189 | baseProgs.shuttingDown = true
190 | vpn_shutdown( baseProgs )
191 |
192 | plog := log.WithFields( log.Fields{ "type": "proc_cleanup_kill" } )
193 |
194 | for k,v := range( baseProgs.process ) {
195 | plog.WithFields( log.Fields{ "proc": k } ).Debug("Killing "+k)
196 | v.Kill()
197 | }
198 | }
199 |
200 | func coro_sigterm( runningDevs map [string] *RunningDev, baseProgs *BaseProgs, config *Config ) {
201 | c := make(chan os.Signal, 2)
202 | signal.Notify(c, os.Interrupt, syscall.SIGTERM)
203 | go func() {
204 | <- c
205 | log.WithFields( log.Fields{
206 | "type": "sigterm",
207 | "state": "begun",
208 | } ).Info("Shutdown started")
209 |
210 | // This triggers zmq to stop receiving
211 | // We don't actually wait after this to ensure it has finished cleanly... oh well :)
212 | gStop = true
213 |
214 | closeAllRunningDevs( runningDevs )
215 | closeBaseProgs( baseProgs )
216 |
217 | time.Sleep( time.Millisecond * 1000 )
218 | cleanup_procs( config )
219 |
220 | log.WithFields( log.Fields{
221 | "type": "sigterm",
222 | "state": "done",
223 | } ).Info("Shutdown finished")
224 |
225 | os.Exit(0)
226 | }()
227 | }
--------------------------------------------------------------------------------
/coordinator/stf_control.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "crypto/tls"
6 | "fmt"
7 | "io/ioutil"
8 | "net/http"
9 | "strings"
10 | )
11 |
12 | func NewStfClient() (http.Client) {
13 | tr := &http.Transport{
14 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
15 | }
16 | return http.Client{ Transport: tr }
17 | }
18 |
19 | func stf_set_auth( config *Config, req *http.Request ) {
20 | token := config.Stf.AdminToken
21 | req.Header.Set( "Authorization", "Bearer " + token )
22 | }
23 |
24 | func stf_do_request( config *Config, req *http.Request ) (bool, *http.Response) {
25 | client := NewStfClient()
26 | stf_set_auth( config, req )
27 | resp, err := client.Do( req )
28 | if err != nil || !strings.HasPrefix( resp.Status, "200" ) {
29 | fmt.Println("Error:", err )
30 | fmt.Println("Response Status:", resp.Status)
31 | body, _ := ioutil.ReadAll(resp.Body)
32 | fmt.Println("Response Body:", string(body))
33 |
34 | return false,nil
35 | }
36 | return true, resp
37 | }
38 |
39 | func stf_reserve( config *Config, udid string ) (bool) {
40 | json := fmt.Sprintf(`{"serial":"%s"}`,udid)
41 | fmt.Println("Sending:",json)
42 | url := fmt.Sprintf("https://%s/api/v1/user/devices", config.Stf.HostName )
43 | req, _ := http.NewRequest("POST", url, bytes.NewReader( []byte( json ) ) )
44 | req.Header.Set( "Content-Type", "application/json" )
45 |
46 | success, _ := stf_do_request( config, req )
47 | return success
48 | }
49 |
50 | func stf_release( config *Config, udid string ) (bool) {
51 | url := fmt.Sprintf("https://%s/api/v1/user/devices/%s", config.Stf.HostName, udid)
52 | req, _ := http.NewRequest("DELETE", url, nil )
53 |
54 | success, _ := stf_do_request( config, req )
55 | return success
56 | }
--------------------------------------------------------------------------------
/coordinator/video_app.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "io/ioutil"
6 | "os"
7 | "os/exec"
8 | )
9 |
10 | func va_write_config( config *Config, uuid string, vidport string, ip string ) {
11 | // create a temp file containing config
12 | // use ios-deploy to write the file to Documents dir of app
13 | fmt.Printf("Writing video app config port=%s ip=%s\n", vidport, ip )
14 |
15 | conf := fmt.Sprintf(`{
16 | "port": "%s",
17 | "ip": "%s"
18 | }`, vidport, ip )
19 | fh, err := ioutil.TempFile("", "config")
20 | if err != nil {
21 | os.Exit(1)
22 | }
23 | fh.WriteString( conf )
24 | //defer os.Remove( fh.Name() )
25 |
26 | fmt.Printf( "%s -i %s -o %s -1 %s -2 %s\n", config.BinPaths.IosDeploy, uuid, fh.Name(), "com.dryark.vidtest2", "Documents/config.json" );
27 |
28 | exec.Command(
29 | config.BinPaths.IosDeploy,
30 | "-i", uuid,
31 | "-o", fh.Name(),
32 | "-1", "com.dryark.vidtest2",
33 | "-2", "Documents/config.json",
34 | ).Output()
35 |
36 | }
37 |
38 | func va_start_stream() {
39 | }
40 |
41 | func va_stop_stream() {
42 | }
43 |
44 | func va_check_status() {
45 | }
--------------------------------------------------------------------------------
/coordinator/wda.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "net/http"
7 | "strconv"
8 | "strings"
9 | "sync"
10 | "time"
11 | uj "github.com/nanoscopic/ujsonin/mod"
12 | log "github.com/sirupsen/logrus"
13 | )
14 |
15 | type WDAType struct {
16 | base string
17 | channel chan DevEvent
18 | devd *RunningDev
19 | }
20 |
21 | func NewWDACaller( base string ) ( *WDAType ) {
22 | self := WDAType { base: base }
23 | return &self
24 | }
25 |
26 | func NewTempWDA( o ProcOptions ) ( *WDAType ) {
27 | tempCh := make( chan DevEvent )
28 | wda := WDAType {
29 | channel: tempCh,
30 | base: ( "http://" + o.curIP + ":" + strconv.Itoa( o.devd.wdaPort ) ),
31 | devd: o.devd,
32 | }
33 |
34 | proc_wdaproxy( o, tempCh, true )
35 |
36 | // Wait for WDA to actually start up
37 | for {
38 | done := 0
39 | select {
40 | case devEvent := <- tempCh:
41 | if devEvent.action == 4 {
42 | log.Info("TempWDA Started")
43 | done = 1
44 | break
45 | }
46 | }
47 | if done == 1 { break }
48 | }
49 |
50 | return &wda
51 | }
52 |
53 | func aio_reset_media_services( o ProcOptions ) {
54 | baseCopy := *(o.baseProgs)
55 | o.baseProgs = &baseCopy
56 | devCopy := *(o.devd)
57 | o.devd = &devCopy
58 | devCopy.lock = &sync.Mutex{}
59 |
60 | wda := NewTempWDA( o )
61 | time.Sleep( time.Second * 2 )
62 | wda.reset_media_services()
63 | o.baseProgs.shuttingDown = true
64 | wda.end()
65 | time.Sleep( time.Second * 2 )
66 | }
67 |
68 | func ( self *WDAType ) end() {
69 | devd := self.devd
70 | wdaProc := devd.process["wdaproxytemp"]
71 | log.WithFields( log.Fields{
72 | "type": "proc_kill",
73 | "pid": wdaProc.pid,
74 | } ).Debug("Attempting to kill")
75 | wdaProc.Kill()
76 | }
77 |
78 | func ( self *WDAType ) reset_media_services() {
79 | sid := self.create_session( "com.apple.Preferences" )
80 | devEl := self.el_by_name( sid, "Developer" )
81 | log.Debug("Got ID " + devEl + " for Developer item" )
82 | self.scroll_to( sid, devEl )
83 | self.click( sid, devEl )
84 | resetEl := self.el_by_name( sid, "Reset Media Services" )
85 | log.Debug("Got ID " + resetEl + " for Reset Media Services item" )
86 | self.scroll_to( sid, resetEl )
87 | self.click( sid, resetEl )
88 | self.home( sid )
89 | }
90 |
91 | func ( self *WDAType ) el_by_name( sid string, name string ) ( string ) {
92 | json := fmt.Sprintf(`{
93 | "using": "name",
94 | "value": "%s"
95 | }`, name )
96 | url := self.base + "/session/" + sid + "/element"
97 | log.Info( "visiting " + url )
98 | resp, _ := http.Post( url, "application/json", strings.NewReader( json ) )
99 | res := resp_to_val( resp )
100 | //log.Info( "response " + resp_to_str( resp ) )
101 | el := res.Get("ELEMENT")
102 | if el != nil {
103 | return el.String()
104 | }
105 | log.Error( "could not find element with name %s", name )
106 | return ""
107 | }
108 |
109 | func ( self *WDAType ) click( sid string, eid string ) {
110 | url := self.base + "/session/" + sid + "/element/" + eid + "/click"
111 | log.Info( "visiting " + url )
112 | resp, _ := http.Post( url, "application/json", strings.NewReader( "{}" ) )
113 | if resp.StatusCode != 200 {
114 | log.Error( "got resp" + strconv.Itoa( resp.StatusCode ) + "from " + url )
115 | }
116 | //res := resp_to_val( resp )
117 | }
118 |
119 | func ( self *WDAType ) force_touch( sid string, eid string ) {
120 | url := self.base + "/session/" + sid + "/wda/element/" + eid + "/forceTouch"
121 | log.Info( "visiting " + url )
122 |
123 | json := `{
124 | "duration": 1,
125 | "pressure": 1000
126 | }`
127 |
128 | resp, _ := http.Post( url, "application/json", strings.NewReader( json ) )
129 | if resp.StatusCode != 200 {
130 | log.Error( "got resp" + strconv.Itoa( resp.StatusCode ) + "from " + url )
131 | }
132 | }
133 |
134 | func ( self *WDAType ) scroll_to( sid string, eid string ) {
135 | url := self.base + "/session/" + sid + "/wda/element/" + eid + "/scroll"
136 | log.Info( "visiting " + url )
137 | resp, _ := http.Post( url, "application/json", strings.NewReader( "{\"toVisible\":1}" ) )
138 | if resp.StatusCode != 200 {
139 | log.Error( "got resp" + strconv.Itoa( resp.StatusCode ) + "from " + url )
140 | }
141 | }
142 |
143 | func ( self *WDAType ) home( sid string ) {
144 | url := self.base + "/wda/homescreen"
145 | log.Info( "visiting " + url )
146 | resp, _ := http.Post( url, "application/json", strings.NewReader( "{}" ) )
147 | if resp.StatusCode != 200 {
148 | log.Error( "got resp" + strconv.Itoa( resp.StatusCode ) + "from " + url )
149 | }
150 | }
151 |
152 | func ( self *WDAType ) create_session( bundle string ) ( string ) {
153 | ops := fmt.Sprintf( `{
154 | "capabilities": {
155 | "alwaysMatch": {},
156 | "firstMatch": [
157 | {
158 | "arguments": [],
159 | "bundleId": "%s",
160 | "environment": {},
161 | "shouldUseSingletonTestManager": true,
162 | "shouldUseTestManagerForVisibilityDetection": false,
163 | "shouldWaitForQuiescence": true
164 | }
165 | ]
166 | }
167 | }`, bundle );
168 | resp, _ := http.Post( self.base + "/session", "application/json", strings.NewReader( ops ) )
169 | res := resp_to_val( resp )
170 | return res.Get("sessionId").String()
171 | }
172 |
173 | func ( self *WDAType ) swipe( sid string, x1 int, y1 int, x2 int, y2 int ) ( string ) {
174 | log.Info( "Swiping:", x1, y1, x2, y2 )
175 | json := fmt.Sprintf( `{
176 | "actions": [
177 | {
178 | "action": "press",
179 | "options": {
180 | "x":%d,
181 | "y":%d
182 | }
183 | },
184 | {
185 | "action":"wait",
186 | "options": {
187 | "ms": 500
188 | }
189 | },
190 | {
191 | "action": "moveTo",
192 | "options": {
193 | "x":%d,
194 | "y":%d
195 | }
196 | },
197 | {
198 | "action":"release",
199 | "options":{}
200 | }
201 | ]
202 | }`, x1, y1, x2, y2 )
203 | resp, _ := http.Post( self.base + "/session/" + sid + "/wda/touch/perform", "application/json", strings.NewReader( json ) )
204 | res := resp_to_str( resp )
205 | log.Info( "response " + res )
206 | return res
207 | }
208 |
209 | func ( self *WDAType ) launch_app( sid string, app string ) ( string ) {
210 | log.Info( "Launching:", app )
211 | json := fmt.Sprintf( `{
212 | "bundleId": "%s",
213 | "shouldWaitForQuiescence": false,
214 | "arguments": [],
215 | "environment": []
216 | }`, app )
217 | resp, _ := http.Post( self.base + "/session/" + sid + "/wda/apps/launch", "application/json", strings.NewReader( json ) )
218 | res := resp_to_str( resp )
219 | log.Info( "response " + res )
220 | return res
221 | }
222 |
223 | func wda_session( base string ) ( string ) {
224 | resp, _ := http.Get( base + "/status" )
225 | content, _ := uj.Parse( []byte( resp_to_str( resp ) ) )
226 | sid := content.Get("sessionId").String()
227 | return sid
228 | }
229 |
230 | func ( self *WDAType ) is_locked() ( bool ) {
231 | resp, _ := http.Get( self.base + "/wda/locked" )
232 | respStr := resp_to_str( resp )
233 | fmt.Printf("response str:%s\n", respStr)
234 | content, _ := uj.Parse( []byte( respStr ) )
235 | //fmt.Printf("output:%s\n", content )
236 | return content.Get("value").Bool()
237 | }
238 |
239 | func ( self *WDAType ) start_broadcast( devd *RunningDev, sid string, app_name string ) {
240 | self.control_center( devd, sid )
241 |
242 | devEl := self.el_by_name( sid, "Screen Recording" )
243 | self.force_touch( sid, devEl )
244 |
245 | devEl = self.el_by_name( sid, app_name )
246 | self.click( sid, devEl )
247 |
248 | devEl = self.el_by_name( sid, "Start Broadcast" )
249 | self.click( sid, devEl )
250 | }
251 |
252 | func ( self *WDAType ) control_center( devd *RunningDev, sid string ) {
253 | prod := devd.productNum
254 |
255 | // ProductTypes that use the new method of bringing up the control center
256 | // See https://gist.github.com/adamawolf/3048717
257 | var newProds = map[string]bool{
258 | "iPhone11": true,
259 | "iPhone12": true,
260 | "iPhone13": true,
261 | "iPad11": true,
262 | }
263 | var newProdFull = map[string]bool{
264 | "iPhone10,3": true,
265 | "iPhone10,6": true,
266 | }
267 |
268 | width, height := self.window_size( sid )
269 | if newProds[ prod ] || newProdFull[ devd.productType ] {
270 | maxx := width -1
271 | self.swipe( sid, maxx, 0, maxx, 100 )
272 | } else {
273 | midx := width / 2
274 | maxy := height - 1
275 | self.swipe( sid, midx, maxy, midx, maxy - 100 )
276 | }
277 | }
278 |
279 | func ( self *WDAType ) window_size( sid string ) ( int, int ) {
280 | resp, _ := http.Get( self.base + "/session/" + sid + "/window/size" )
281 | val := resp_to_val( resp )
282 | width := val.Get("width").Int()
283 | height := val.Get("height").Int()
284 | return width, height
285 | }
286 |
287 | func ( self *WDAType ) unlock() {
288 | http.Post( self.base + "/wda/unlock", "application/json", strings.NewReader( "{}" ) )
289 | }
290 |
291 | func source( base string ) ( string ) {
292 | resp, _ := http.Get( base + "/source" )
293 | res := resp_to_str( resp )
294 | //print Dumper( res )
295 | return res
296 | }
297 |
298 | func wda_apps_list( base string ) ( string ) {
299 | sid := wda_session( base )
300 | resp, _ := http.Get( base + "/session/" + sid + "/wda/apps/list" )
301 | res := resp_to_str( resp )
302 | //print Dumper( res )
303 | return res
304 | }
305 |
306 | func wda_battery_info( base string ) ( string ) {
307 | sid := wda_session( base )
308 | resp, _ := http.Get( base + "/session/" + sid + "/wda/batteryInfo" )
309 | res := resp_to_str( resp )
310 | //print Dumper( $res )
311 | return res
312 | }
313 |
314 | func resp_to_str( resp *http.Response ) ( string ) {
315 | body := resp.Body
316 | buf := new( bytes.Buffer )
317 | buf.ReadFrom( body )
318 | return buf.String()
319 | }
320 |
321 | func resp_to_json( resp *http.Response ) ( *uj.JNode ) {
322 | rawContent := resp_to_str( resp )
323 | if !strings.HasPrefix( rawContent, "{" ) {
324 | return nil // &JNode{ nodeType: 1, hash: NewNodeHash() }
325 | }
326 | content, _ := uj.Parse( []byte( rawContent ) )
327 | return content
328 | }
329 |
330 | func resp_to_val( resp *http.Response ) ( *uj.JNode ) {
331 | rawContent := resp_to_str( resp )
332 | if !strings.HasPrefix( rawContent, "{" ) {
333 | return nil // &JNode{ nodeType: 1, hash: NewNodeHash() }
334 | }
335 | content, _ := uj.Parse( []byte( rawContent ) )
336 | val := content.Get("value")
337 | if val == nil { return content }
338 | return val
339 | }
--------------------------------------------------------------------------------
/get-version-info.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/python
2 |
3 | import json
4 | import os
5 | import subprocess
6 | import sys
7 | import argparse
8 |
9 | parser = argparse.ArgumentParser( description = "Collect git version info" )
10 | parser.add_argument('--repo',default="")
11 | parser.add_argument('--unix',action="count")
12 | parser.add_argument('--wdasource',action="count")
13 | args = parser.parse_args()
14 |
15 | def git_info( dir ):
16 | if args.unix==1:
17 | cmd = ["/usr/bin/git","-C","./"+dir,"log","-1","--date=unix", "--no-merges"]
18 | else:
19 | cmd = ["/usr/bin/git","-C","./"+dir,"log","-1", "--no-merges"]
20 |
21 | try:
22 | res = subprocess.check_output( cmd, stderr=subprocess.STDOUT )
23 | except subprocess.CalledProcessError as e:
24 | #sys.stderr.write( e.output )
25 | return {
26 | "error": "missing"#e.output
27 | }
28 |
29 | remote = subprocess.check_output( ["/usr/bin/git", "-C", "./" + dir, "remote","-v"] )
30 |
31 | res = res[:-1] # remove trailing "\n"
32 | remote = remote[:-1]
33 | remote = remote.split("\n")[0].split("\t") # just first line
34 |
35 | parts = res.split("\n")
36 |
37 | return {
38 | "commit": parts[0][7:], # remove 'commit '
39 | "author": parts[1][7:].lstrip(), # remove 'Author:' and spaces
40 | "date": parts[2][5:].lstrip(), # remove 'Date:' and spaces
41 | "remote": remote[1].replace(" (fetch)",""), # just url to fetch
42 | }
43 |
44 | def xcode_version():
45 | res = subprocess.check_output( ["/usr/bin/xcodebuild", "-version"] )
46 | res = res[:-1]
47 | return res.split("\n")
48 |
49 | if args.repo != "":
50 | if args.repo == 'wda':
51 | data = {
52 | "wda": git_info( 'repos/WebDriverAgent' ),
53 | }
54 | data['wda']['xcode'] = xcode_version()
55 | if args.repo == 'ios_support':
56 | data = {
57 | "ios_support": git_info( '.' ),
58 | }
59 | else:
60 | data = {
61 | "wda": git_info( 'repos/WebDriverAgent' ),
62 | "h264_to_jpeg": git_info( 'repos/h264_to_jpeg' ),
63 | "device_trigger": git_info( 'repos/osx_ios_device_trigger' ),
64 | "stf": git_info( 'repos/stf-ios-provider' ),
65 | "ios_video_stream": git_info( 'repos/ios_video_stream' ),
66 | "wdaproxy": git_info( 'repos/wdaproxy' ),
67 | "ios_support": git_info( '.' ),
68 | "ios_avf_pull": git_info( 'repos/ios_avf_pull' )
69 | }
70 | if os.path.exists( 'bin/wda/build_info.json' ):
71 | fh = open( 'bin/wda/build_info.json', 'r' )
72 | wda_root = json.load( fh )
73 | if args.wdasource != 1:
74 | data["wda"] = wda_root["wda"]
75 |
76 | print json.dumps( data, indent = 2 )
77 |
78 |
--------------------------------------------------------------------------------
/get-wda-build-path.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # From https://stackoverflow.com/questions/3915040/bash-fish-command-to-print-absolute-path-to-a-file
3 | abspath() {
4 | if [ -d "$1" ]; then (cd "$1"; pwd)
5 | elif [ -f "$1" ]; then
6 | if [[ $1 = /* ]]; then echo "$1"
7 | elif [[ $1 == */* ]]; then echo "$(cd "${1%/*}"; pwd)/${1##*/}"
8 | else echo "$(pwd)/$1"; fi
9 | fi
10 | }
11 |
12 | BPATH=$(xcodebuild -project repos/WebDriverAgent/WebDriverAgent.xcodeproj -showBuildSettings -configuration Debug | grep TARGET_BUILD | awk '{print $3}' | tr -d "\n")
13 | #echo BUILD_PATH="$BPATH"
14 | echo "$(abspath $BPATH/..)"
15 |
--------------------------------------------------------------------------------
/init.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | mkdir -p repos
3 |
4 | GR="\033[32m"
5 | RED="\033[91m"
6 | RST="\033[0m"
7 |
8 | function install_brew_if_needed() {
9 | if ! command -v brew > /dev/null; then
10 | echo "Brew not installed; installing"
11 | /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"
12 | fi
13 | }
14 |
15 | function assert_has_xcodebuild() {
16 | XCODE_VERSION="none"
17 | XCODE_MAJOR_VERSION="0"
18 | XCODE_MINOR_VERSION="0"
19 | if command -v xcodebuild > /dev/null; then
20 | XCODE_VERSION=`xcodebuild -version | grep Xcode | tr -d "\n" | perl -pe 's/Xcode //'`
21 | XCODE_MAJOR_VERSION=`echo $XCODE_VERSION | perl -pe 's/([0-9]+)\.[0-9]+/$1/'`
22 | XCODE_MINOR_VERSION=`echo $XCODE_VERSION | perl -pe 's/[0-9]+\.([0-9]+)/$1/'`
23 | fi
24 |
25 | #echo "XCODE Version: $XCODE_VERSION"
26 | #echo "XCODE Version: Major = $XCODE_MAJOR_VERSION, Minor = $XCODE_MINOR_VERSION"
27 |
28 | if [ $XCODE_MAJOR_VERSION > 10 ]; then
29 | echo -e "${GR}Xcode $XCODE_VERSION installed$RST"
30 | elif [ "$XCODE_VERSION" == "10.3" ]; then
31 | echo -e "${GR}Xcode 10.3 installed$RST"
32 | else
33 | echo -e "${RED}Xcode 10.3+ not installed$RST"
34 | echo -e "${RED}You need to install it and then rerun init.sh$RST"
35 | exit 1
36 | fi
37 | }
38 |
39 | install_brew_if_needed
40 | assert_has_xcodebuild
41 | ./util/brewser.pl installdeps stf_ios_support.rb
42 | ./util/brewser.pl ensurehead libplist 2.2.1
43 | ./util/brewser.pl fixpc libplist 2.0
44 | ./util/brewser.pl ensurehead libusbmuxd 2.0.3
45 | ./util/brewser.pl fixpc libusbmuxd 2.0
46 | #make libimd
--------------------------------------------------------------------------------
/logs/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dryark/stf_ios_support/2f32cbbf1506a740eb21a496c1cdc11d875558f7/logs/.gitkeep
--------------------------------------------------------------------------------
/makefile_preflight.pl:
--------------------------------------------------------------------------------
1 | #!/usr/bin/perl -w
2 | use strict;
3 | my $brew_check = `./util/brewser.pl checkdeps stf_ios_support.rb`;
4 | if( $brew_check =~ m/Missing/ ) {
5 | print STDERR $brew_check, "\nRun init.sh to correct\n";
6 | print "x";
7 | exit(1);
8 | }
9 | if( $brew_check =~ m/Brew must be installed/ ) {
10 | print STDERR "Brew must be installed", "\nRun init.sh to correct\n";
11 | print "x";
12 | exit(1);
13 | }
14 | `./check-versions.pl`;
15 |
--------------------------------------------------------------------------------
/offline/Makefile:
--------------------------------------------------------------------------------
1 | all: /usr/local/bin/ideviceinfo
2 |
3 | # --- Basic Directories ---
4 |
5 | repos:
6 | mkdir repos
7 |
8 | # --- LibIMobileDevice ---
9 |
10 | /usr/local/bin/ideviceinfo: repos/libimobiledevice repos/libimobiledevice/tools/ideviceinfo | repos/libimobiledevice
11 | $(MAKE) -C repos/libimobiledevice install
12 |
13 | repos/libimobiledevice/tools/ideviceinfo: repos/libimobiledevice repos/libimobiledevice/Makefile | repos/libimobiledevice
14 | $(MAKE) -C repos/libimobiledevice
15 |
16 | repos/libimobiledevice/Makefile: | repos/libimobiledevice
17 | cd repos/libimobiledevice && NOCONFIGURE=1 ./autogen.sh
18 | cd repos/libimobiledevice && ./configure --disable-openssl
19 |
20 | # --- Clones ---
21 |
22 | repos/libimobiledevice: | repos
23 | git clone https://github.com/libimobiledevice/libimobiledevice.git repos/libimobiledevice
--------------------------------------------------------------------------------
/run:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | bin/coordinator $*
3 |
--------------------------------------------------------------------------------
/runner/Makefile:
--------------------------------------------------------------------------------
1 | TARGET = runner
2 |
3 | all: $(TARGET)
4 |
5 | runner_sources := $(wildcard *.go)
6 |
7 | $(TARGET): $(runner_sources) go.sum
8 | go build -o $(TARGET) -ldflags "-X main.GitCommit=$(GIT_COMMIT) -X main.GitDate=$(GIT_DATE) -X main.GitRemote=$(GIT_REMOTE) -X main.EasyVersion=$(EASY_VERSION)" .
9 |
10 | go.sum:
11 | go get
12 | go get .
13 |
14 | clean:
15 | $(RM) $(TARGET) go.sum
--------------------------------------------------------------------------------
/runner/gencert.pl:
--------------------------------------------------------------------------------
1 | #!/usr/bin/perl -w
2 | use strict;
3 |
4 | my $hostname = "localhost";
5 | my $mainip = "127.0.0.1";
6 |
7 | my $template = "runnercert.tmpl";
8 | my $template_data = slurp( $template );
9 |
10 | $template_data =~ s/HOSTNAME/$hostname/g;
11 | $template_data =~ s/IPADDR/$mainip/g;
12 |
13 | open( my $outfh, ">runnercert.conf" );
14 | print $outfh $template_data;
15 | my $ips = get_ips();
16 | my $index = 2;
17 | for my $ip ( @$ips ) {
18 | print $outfh "DNS.$index = $ip\n";
19 | $index++;
20 | }
21 | close( $outfh );
22 |
23 | `openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout server.key -out server.crt -config runnercert.conf -subj "/C=US/ST=Washington/L=Seattle/O=Dis/CN=$hostname"`;
24 |
25 | sub slurp {
26 | my $file = shift;
27 | open( my $fh, "<$file" );
28 | my $data;
29 | {
30 | local $/ = undef;
31 | $data = <$fh>;
32 | }
33 | close( $fh );
34 | return $data;
35 | }
36 |
37 | sub get_ips {
38 | my @lines = `ifconfig`;
39 | my @ips;
40 | for my $line ( @lines ) {
41 | next if( $line !~ m/inet / );
42 | if( $line =~ m/inet ([0-9.]+) / ) {
43 | push( @ips, $1 );
44 | }
45 | }
46 | return \@ips;
47 | }
--------------------------------------------------------------------------------
/runner/go.mod:
--------------------------------------------------------------------------------
1 | module runner.go
2 |
3 | go 1.12
4 |
5 | require (
6 | github.com/go-cmd/cmd v1.2.1
7 | github.com/gorilla/template v0.0.0-20130106121210-ad2f1c41567b
8 | github.com/jviney/go-proc v0.2.0
9 | github.com/nanoscopic/ujsonin v1.9.0
10 | github.com/sirupsen/logrus v1.6.0
11 | )
12 |
--------------------------------------------------------------------------------
/runner/go.sum:
--------------------------------------------------------------------------------
1 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
2 | github.com/go-cmd/cmd v1.2.1 h1:fV4o2i9JX8+TeI1xB1x7Ji/3ndEfJNdaa+7uYGVhkuM=
3 | github.com/go-cmd/cmd v1.2.1/go.mod h1:F2yJeMVdy5ymftSgCR0zMN7XLhKFJpG5/1brXju8EXU=
4 | github.com/go-test/deep v1.0.6/go.mod h1:QV8Hv/iy04NyLBxAdO9njL0iVPN1S4d/A3NVv1V36o8=
5 | github.com/gorilla/template v0.0.0-20130106121210-ad2f1c41567b h1:jMvt85GaJEKEZdIQYsS72I/aVFvIIA4c+0/+P+wDVag=
6 | github.com/gorilla/template v0.0.0-20130106121210-ad2f1c41567b/go.mod h1:xfqvveesQKRN2vVwJ1nSfDdBGGkj+C+uxQpUqxIU76s=
7 | github.com/jviney/go-proc v0.2.0 h1:HzS0OfhmrhHrDIW7R1ajsCYu0HN/gB6ayl6BW76Zw5U=
8 | github.com/jviney/go-proc v0.2.0/go.mod h1:lb9gbAFTP34lAyqc4DEvOo21RDaAT6xO3cEuxrzI+mU=
9 | github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
10 | github.com/nanoscopic/ujsonin v1.9.0 h1:lblaLCr1H6g5Iy5IObSZJcvtzBIHK04GVYtuxATPtpk=
11 | github.com/nanoscopic/ujsonin v1.9.0/go.mod h1:FyHvuWes/DhijYGBTtQB74enYOLiHodd3M3waNV3gWU=
12 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
13 | github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
14 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
15 | golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
16 |
--------------------------------------------------------------------------------
/runner/http_server.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bytes"
5 | "fmt"
6 | "net/http"
7 | "text/template"
8 | "strconv"
9 | "strings"
10 | "time"
11 | uj "github.com/nanoscopic/ujsonin/mod"
12 | )
13 |
14 | type Info struct {
15 | proc *GenericProc
16 | passmap map[string] string
17 | }
18 |
19 | func coro_http_server( port int, proc *GenericProc, passmap map[string] string, secure bool, crt string, key string, runnerVersion VersionInfo, config *uj.JNode ) {
20 | var listen_addr = fmt.Sprintf( "0.0.0.0:%d", port )
21 | startServer( listen_addr, proc, passmap, secure, crt, key, runnerVersion, config )
22 | }
23 |
24 | func BasicAuth(handler http.HandlerFunc, passmap map[string] string ) http.HandlerFunc {
25 | realm := "Enter auth for coordinator runner admin"
26 |
27 | return func(w http.ResponseWriter, r *http.Request) {
28 |
29 | user, pass, ok := r.BasicAuth()
30 |
31 | if !ok || !check_pass( user, pass, passmap ) {
32 | w.Header().Set("WWW-Authenticate", `Basic realm="`+realm+`"`)
33 | w.WriteHeader(401)
34 | w.Write([]byte("Unauthorised.\n"))
35 | return
36 | }
37 |
38 | handler(w, r)
39 | }
40 | }
41 |
42 | func startServer( listen_addr string, proc *GenericProc, passmap map[string] string, secure bool, crt string, key string, runnerVersion VersionInfo, config *uj.JNode ) {
43 | info := Info{
44 | proc: proc,
45 | passmap: passmap,
46 | }
47 |
48 | fmt.Printf("HTTP server started")
49 |
50 | rootClosure := BasicAuth( func( w http.ResponseWriter, r *http.Request ) {
51 | handleRoot( w, r, info, runnerVersion )
52 | }, passmap );
53 | startClosure := BasicAuth( func( w http.ResponseWriter, r *http.Request ) {
54 | handleStart( w, r, info )
55 | }, passmap );
56 | stopClosure := BasicAuth( func( w http.ResponseWriter, r *http.Request ) {
57 | handleStop( w, r, info )
58 | }, passmap );
59 | restartClosure := BasicAuth( func( w http.ResponseWriter, r *http.Request ) {
60 | handleRestart( w, r, info )
61 | }, passmap );
62 | updateClosure := BasicAuth( func( w http.ResponseWriter, r *http.Request ) {
63 | handleUpdate( w, r, info, config )
64 | }, passmap );
65 |
66 | http.HandleFunc( "/", rootClosure )
67 | http.HandleFunc( "/start", startClosure )
68 | http.HandleFunc( "/stop", stopClosure )
69 | http.HandleFunc( "/restart", restartClosure )
70 | http.HandleFunc( "/update", updateClosure )
71 |
72 | var err error
73 | if secure {
74 | err = http.ListenAndServeTLS( listen_addr, crt, key, nil )
75 | } else {
76 | err = http.ListenAndServe( listen_addr, nil )
77 | }
78 | fmt.Printf("HTTP ListenAndServe Error %s\n", err)
79 | }
80 |
81 | func handleRoot( w http.ResponseWriter, r *http.Request, info Info, rv VersionInfo ) {
82 | var allVersions map[string] VersionInfo = make( map[string] VersionInfo )
83 | allVersions["Runner"] = rv
84 | loadVersionInfo( allVersions )
85 |
86 | versionText := ""
87 | for itemName,item := range allVersions {
88 | var str bytes.Buffer
89 |
90 | remote := item.GitRemote
91 | remote = strings.Replace( remote, "git@github.com:", "", 1 )
92 | remote = strings.Replace( remote, ".git", "", 1 )
93 | rawRemote := remote
94 | remote = "" + remote + ""
95 |
96 | commit := item.GitCommit
97 | commit = "" + commit + ""
98 |
99 | time := unixToTimeObject( item.GitDate )
100 | timeStr := time.Format( "Mon, Jan 2 2006 3:04 PM MST" )
101 |
102 | versionTpl.Execute( &str, map[string] string {
103 | "GitCommit": commit,
104 | "GitDate": timeStr,
105 | "GitRemote": remote,
106 | "EasyVersion": item.EasyVersion,
107 | "Name": itemName,
108 | } )
109 | versionText += str.String() + "
"
110 | }
111 |
112 | rootTpl.Execute( w, map[string] string{
113 | "pid": strconv.Itoa( info.proc.pid ),
114 | "timeUp": info.proc.backoff.timeUpText(),
115 | "Versions": versionText,
116 | } )
117 | }
118 |
119 | func unixToTimeObject( unix string ) ( time.Time ) {
120 | i, _ := strconv.ParseInt( unix, 10, 64)
121 | return time.Unix( i, 0 )
122 | }
123 |
124 | func handleStart( w http.ResponseWriter, r *http.Request, info Info ) {
125 | w.Header().Set("Content-Type", "text/html; charset=utf-8")
126 | info.proc.Start()
127 | fmt.Fprintf( w, "ok" )
128 | }
129 |
130 | func handleStop( w http.ResponseWriter, r *http.Request, info Info ) {
131 | w.Header().Set("Content-Type", "text/html; charset=utf-8")
132 | info.proc.Stop()
133 | fmt.Fprintf( w, "ok" )
134 | }
135 |
136 | func handleRestart( w http.ResponseWriter, r *http.Request, info Info ) {
137 | w.Header().Set("Content-Type", "text/html; charset=utf-8")
138 | info.proc.Restart()
139 | fmt.Fprintf( w, "ok" )
140 | }
141 |
142 | func handleUpdate( w http.ResponseWriter, r *http.Request, info Info, config *uj.JNode ) {
143 | w.Header().Set("Content-Type", "text/html; charset=utf-8")
144 | runUpdate( info, w, config )
145 | }
146 |
147 | var versionTpl = template.Must(template.New("version").Parse(`
148 | {{.Name}} Version info:
149 |
150 |
151 | Git Commit |
152 | {{.GitCommit}} |
153 |
154 |
155 | Git Date |
156 | {{.GitDate}} |
157 |
158 |
159 | Git Remote |
160 | {{.GitRemote}} |
161 |
162 |
163 | Easy Version |
164 | {{.EasyVersion}} |
165 |
166 |
167 | `))
168 |
169 | var rootTpl = template.Must(template.New("root").Parse(`
170 |
171 |
172 |
173 |
235 |
236 |
237 | DeviceFarmer IOS Coordinator Runner
238 |
239 |
241 |
242 |
243 |
244 |
245 |
246 | PID: {{.pid}}
247 | Time up: {{.timeUp}}
248 |
249 |
250 |
251 |
252 |
253 | {{.Versions}}
254 |
255 |
256 | `))
257 |
--------------------------------------------------------------------------------
/runner/pass_check.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "crypto/sha256"
6 | "crypto/subtle"
7 | uj "github.com/nanoscopic/ujsonin/mod"
8 | )
9 |
10 | func check_pass( user string, pass string, hashes map[string] string ) bool {
11 | hash := hash_pass( pass )
12 | check, ok := hashes[ user ]
13 | if !ok { return false }
14 | res := subtle.ConstantTimeCompare([]byte(check),[]byte(hash))
15 | if res == 1 { return true }
16 | return false
17 | }
18 |
19 | func hash_pass( pass string ) string {
20 | h := sha256.New()
21 | h.Write([]byte(pass))
22 | hash := fmt.Sprintf("%x", h.Sum(nil))
23 | return hash
24 | }
25 |
26 | func json_users_to_passmap( users *uj.JNode ) ( map[string] string ) {
27 | passmap := map[string] string{}
28 |
29 | users.ForEach( func( cur *uj.JNode ) {
30 | //cur.Dump()
31 | user := cur.Get("user").String()
32 | pass := cur.Get("pass").String()
33 | passmap[ user ] = pass
34 | } )
35 | return passmap
36 | }
--------------------------------------------------------------------------------
/runner/proc_backoff.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "time"
5 | "strconv"
6 | )
7 |
8 | type Backoff struct {
9 | fails int
10 | start time.Time
11 | elapsedSeconds float64
12 | }
13 |
14 | func ( self *Backoff ) markStart() {
15 | self.start = time.Now()
16 | }
17 |
18 | func ( self *Backoff ) timeUp() ( float64 ) {
19 | elapsed := time.Since( self.start )
20 | seconds := elapsed.Seconds()
21 | return seconds
22 | }
23 |
24 | func ( self *Backoff ) timeUpText() ( string ) {
25 | seconds := uint16( self.timeUp() )
26 | minutes := uint16(0)
27 | hours := uint16(0)
28 | days := uint16(0)
29 | if seconds > 60 {
30 | mod := seconds % 60
31 | minutes = seconds / 60
32 | seconds = mod
33 | }
34 | if minutes > 60 {
35 | mod := minutes % 60
36 | hours = minutes / 60
37 | minutes = mod
38 | }
39 | if hours > 24 {
40 | mod := hours % 24
41 | days = hours / 24
42 | hours = mod
43 | }
44 | text := strconv.Itoa(int(seconds)) + " sec"
45 | if minutes > 0 {
46 | text = strconv.Itoa(int(minutes)) + " mins " + text
47 | }
48 | if hours > 0 {
49 | text = strconv.Itoa(int(hours)) + " hrs " + text
50 | }
51 | if days > 0 {
52 | text = strconv.Itoa(int(days)) + " days " + text
53 | }
54 | return text
55 | }
56 |
57 | func ( self *Backoff ) markEnd() ( float64 ) {
58 | elapsed := time.Since( self.start )
59 | seconds := elapsed.Seconds()
60 | self.elapsedSeconds = seconds
61 | return seconds
62 | }
63 |
64 | func ( self *Backoff ) wait() {
65 | sleeps := []int{ 0, 0, 2, 5, 10 }
66 | numSleeps := len( sleeps )
67 | if self.elapsedSeconds < 20 {
68 | self.fails = self.fails + 1
69 | index := self.fails
70 | if index >= numSleeps {
71 | index = numSleeps - 1
72 | }
73 | sleepLen := sleeps[ index ]
74 | if sleepLen != 0 {
75 | time.Sleep( time.Second * time.Duration( sleepLen ) )
76 | }
77 | } else {
78 | self.fails = 0
79 | }
80 | }
--------------------------------------------------------------------------------
/runner/runner.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "time"
6 | "flag"
7 | gocmd "github.com/go-cmd/cmd"
8 | uj "github.com/nanoscopic/ujsonin/mod"
9 | "io/ioutil"
10 | "os"
11 | "os/exec"
12 | )
13 |
14 | type GPMsg struct {
15 | msgType int
16 | }
17 |
18 | type GenericProc struct {
19 | controlCh chan GPMsg
20 | backoff *Backoff
21 | pid int
22 | cmd *gocmd.Cmd
23 | hold bool
24 | }
25 |
26 | func (self *GenericProc) Kill() {
27 | if self.cmd == nil { return }
28 | self.controlCh <- GPMsg{ msgType: 1 }
29 | }
30 |
31 | func (self *GenericProc) Restart() {
32 | if self.cmd == nil { return }
33 | self.controlCh <- GPMsg{ msgType: 2 }
34 | }
35 |
36 | func (self *GenericProc) Start() {
37 | self.controlCh <- GPMsg{ msgType: 3 }
38 | }
39 |
40 | func (self *GenericProc) Stop() {
41 | self.controlCh <- GPMsg{ msgType: 4 }
42 | }
43 |
44 | func proc_generic( binary string, args []string, startDir string ) ( *GenericProc ) {
45 | controlCh := make( chan GPMsg )
46 | backoff := Backoff{}
47 |
48 | proc := GenericProc {
49 | controlCh: controlCh,
50 | backoff: &backoff,
51 | hold: false,
52 | }
53 |
54 | stop := false
55 | hold := false
56 |
57 | go func() { for {
58 | if hold == true {
59 | fmt.Println("Waiting for signal to start again")
60 | }
61 |
62 | for {
63 | if hold == false {
64 | break
65 | }
66 | select {
67 | case msg := <- controlCh:
68 | fmt.Printf("Got message on control channel")
69 | if msg.msgType == 3 { // start
70 | hold = false
71 | break
72 | }
73 | }
74 | }
75 |
76 | fmt.Printf("Coordinator start\n")
77 |
78 | if !fileExists( binary ) {
79 | fmt.Printf("Coordinator binary does not exist. Waiting for creation\n")
80 | hold = true
81 | continue
82 | }
83 | cmd := gocmd.NewCmdOptions( gocmd.Options{ Streaming: true }, binary, args... )
84 | proc.cmd = cmd
85 |
86 | if startDir != "" {
87 | cmd.Dir = startDir
88 | }
89 |
90 | backoff.markStart()
91 |
92 | statCh := cmd.Start()
93 |
94 | i := 0
95 | for {
96 | proc.pid = cmd.Status().PID
97 | if proc.pid != 0 {
98 | break
99 | }
100 | time.Sleep(50 * time.Millisecond)
101 | if i > 4 {
102 | break
103 | }
104 | }
105 |
106 | fmt.Printf("PID %d\n", proc.pid)
107 |
108 | outStream := cmd.Stdout
109 | errStream := cmd.Stderr
110 |
111 | runDone := false
112 | for {
113 | select {
114 | case <- statCh:
115 | runDone = true
116 | case msg := <- controlCh:
117 | fmt.Printf("Got stop request on control channel\n")
118 | typ := msg.msgType
119 |
120 | if typ == 1 { // stop
121 | stop = true
122 | } else if typ == 4 { // stop
123 | hold = true
124 | }
125 |
126 | if typ == 1 || typ == 2 || typ == 4 {
127 | proc.cmd.Stop()
128 | cleanup_subprocs( binary )
129 | }
130 |
131 | case line := <- outStream:
132 | fmt.Println( line )
133 | case line := <- errStream:
134 | fmt.Println( line )
135 | }
136 | if runDone { break }
137 | }
138 |
139 | proc.cmd = nil
140 | proc.pid = 0
141 |
142 | backoff.markEnd()
143 |
144 | fmt.Printf("Coordinator end\n")
145 |
146 | if stop { break }
147 | backoff.wait()
148 | } }()
149 |
150 | return &proc
151 | }
152 |
153 | func cleanup_subprocs( binary string ) {
154 | // Make absolutely sure all coordinator subprocesses have been stopped
155 | out, _ := exec.Command( binary, "-killProcs" ).Output()
156 | fmt.Println( out )
157 | }
158 |
159 | func gen_cert() {
160 | out, err := exec.Command( "/usr/bin/perl", "gencert.pl" ).Output()
161 | if err != nil {
162 | fmt.Printf("Error from cert gen: %s\n", err )
163 | return
164 | }
165 | fmt.Println( out )
166 | }
167 |
168 | var GitCommit string
169 | var GitDate string
170 | var GitRemote string
171 | var EasyVersion string
172 |
173 | type VersionInfo struct {
174 | GitCommit string
175 | GitDate string
176 | GitRemote string
177 | EasyVersion string
178 | }
179 |
180 | func main() {
181 | runnerVersion := VersionInfo{
182 | GitCommit: GitCommit,
183 | GitDate: GitDate,
184 | GitRemote: GitRemote,
185 | EasyVersion: EasyVersion,
186 | }
187 |
188 | var passToHash = flag.String( "pass", "", "Password to show hash of" )
189 | var doVersion = flag.Bool( "version" , false , "Show coordinator version info" )
190 |
191 | flag.Parse()
192 |
193 | if *passToHash != "" {
194 | hash := hash_pass( *passToHash )
195 | fmt.Printf("hash:%s\n", hash )
196 | return
197 | }
198 | if *doVersion {
199 | fmt.Printf("Commit:%s\nDate:%s\nRemote:%s\nVersion:%s\n", GitCommit, GitDate, GitRemote, EasyVersion )
200 | os.Exit(0)
201 | }
202 |
203 | if !fileExists("server.crt") {
204 | gen_cert()
205 | }
206 |
207 | content, _ := ioutil.ReadFile("runner.json")
208 | root, _ := uj.Parse( content )
209 | users := root.Get("users")
210 | passmap := json_users_to_passmap( users )
211 | secure := root.Get("https").Bool()
212 | installDir := root.Get("install_dir").String()
213 |
214 | coordPath := installDir + "/bin/coordinator"
215 | cleanup_subprocs( coordPath )
216 | cleanup_procs()
217 |
218 | proc := proc_generic( coordPath, []string{}, installDir )
219 |
220 | coro_sigterm( proc, coordPath )
221 |
222 | if !fileExists( "runner.json" ) {
223 | fmt.Println("runner.json config file not present. exiting\n")
224 | os.Exit( 1 )
225 | return
226 | }
227 |
228 | crt := ""
229 | key := ""
230 | if secure {
231 | crt = root.Get("crt").String()
232 | key = root.Get("key").String()
233 | }
234 |
235 | coro_http_server( 8021, proc, passmap, secure, crt, key, runnerVersion, root )
236 | }
--------------------------------------------------------------------------------
/runner/runner.json:
--------------------------------------------------------------------------------
1 | {
2 | "users": [
3 | {
4 | "user": "replaceme",
5 | "pass": "e9cfcc4980e32ec6f3e8fca563f4de3c5f53766a8f0cc37accffc8ce1d99ff98"
6 | }
7 | ],
8 | "https": true,
9 | "crt": "server.crt",
10 | "key": "server.key",
11 | "updates": "/Users/user/stf_updates",
12 | "install_dir": "/Users/user/stf",
13 | "config": "/Users/user/stf/config.json",
14 | "update_host": "localhost",
15 | "update_port": 8022
16 | }
17 |
--------------------------------------------------------------------------------
/runner/runnercert.tmpl:
--------------------------------------------------------------------------------
1 | [req]
2 | default_bits = 2048
3 | default_keyfile = server.key
4 | distinguished_name = req_distinguished_name
5 | req_extensions = req_ext
6 | x509_extensions = v3_ca
7 |
8 | [req_distinguished_name]
9 | countryName = Country Name (2 letter code)
10 | countryName_default = US
11 | stateOrProvinceName = State or Province Name (full name)
12 | stateOrProvinceName_default = Washington
13 | localityName = Locality Name (eg, city)
14 | localityName_default = Seattle
15 | organizationName = Organization Name (eg, company)
16 | organizationName_default = HOSTNAME
17 | organizationalUnitName = organizationalunit
18 | organizationalUnitName_default = Development
19 | commonName = Common Name (e.g. server FQDN or YOUR name)
20 | commonName_default = HOSTNAME
21 | commonName_max = 64
22 |
23 | [req_ext]
24 | subjectAltName = @alt_names
25 |
26 | [v3_ca]
27 | subjectAltName = @alt_names
28 |
29 | [alt_names]
30 | DNS.1 = HOSTNAME
31 |
--------------------------------------------------------------------------------
/runner/shutdown.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "fmt"
5 | "os"
6 | "os/signal"
7 | "syscall"
8 | "strings"
9 | ps "github.com/jviney/go-proc"
10 | )
11 |
12 | func coro_sigterm( proc *GenericProc, binary string ) {
13 | c := make(chan os.Signal, 2)
14 | signal.Notify(c, os.Interrupt, syscall.SIGTERM)
15 | go func() {
16 | <- c
17 | fmt.Println("\nShutdown started")
18 |
19 | // shutdown proc
20 | proc.Kill()
21 | cleanup_subprocs( binary )
22 |
23 | fmt.Println("Shutdown finished")
24 |
25 | os.Exit(0)
26 | }()
27 | }
28 |
29 | func cleanup_procs() {
30 | // Cleanup hanging processes if any
31 | procs := ps.GetAllProcessesInfo()
32 | for _, proc := range procs {
33 | cmd := proc.CommandLine
34 | if strings.HasSuffix( cmd[0], "/bin/coordinator" ) {
35 | fmt.Printf("Leftover coordinator with PID %d. Killing\n", proc.Pid )
36 | syscall.Kill( proc.Pid, syscall.SIGTERM )
37 | }
38 | }
39 | }
--------------------------------------------------------------------------------
/runner/update.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net/http"
5 | //"time"
6 | "fmt"
7 | "io"
8 | "os"
9 | uj "github.com/nanoscopic/ujsonin/mod"
10 | "io/ioutil"
11 | "strings"
12 | gocmd "github.com/go-cmd/cmd"
13 | escape "github.com/gorilla/template/v0/escape"
14 | )
15 |
16 | func writeLine( w http.ResponseWriter, f http.Flusher, str string, args ...interface{} ) {
17 | fmt.Fprintf( w, "" )
21 | f.Flush()
22 | }
23 |
24 | func writeText( w http.ResponseWriter, f http.Flusher, str string, args ...interface{} ) {
25 | fmt.Fprintf( w, "" )
29 | f.Flush()
30 | }
31 |
32 | func runUpdate( info Info, w http.ResponseWriter, config *uj.JNode ) {
33 | fw, ok := w.(http.Flusher)
34 | if !ok {
35 | fmt.Fprintf( w, "sadness. broken. :(" )
36 | return
37 | }
38 |
39 | //"updates": "/Users/user/stf_updates",
40 | //"install_dir": "/Users/user/stf",
41 | //"config": "/Users/user/stf/config.json"
42 |
43 | fmt.Fprintf( w, "" )
44 |
45 | writeLine( w, fw, "Stopping coordinator" )
46 | info.proc.Stop()
47 |
48 | installDir := config.Get("install_dir").String()
49 | configFile := config.Get("config").String()
50 | updateHost := config.Get("update_host").String()
51 | updatePort := config.Get("update_port").String()
52 | updateUrl := "http://" + updateHost + ":" + updatePort + "/"
53 |
54 | configWithin := false
55 | if strings.HasPrefix( configFile, installDir ) {
56 | configWithin = true
57 | }
58 |
59 | configSource := configFile
60 |
61 | lineSpace := "
"
62 |
63 | if dirExists( installDir ) {
64 | writeLine( w, fw, "Install directory exists; erasing %s", installDir )
65 | if configWithin {
66 | writeLine( w, fw, lineSpace + "Config file within install dir; backing up %s", configFile )
67 | tempFile, err := ioutil.TempFile( "/tmp", "config_json" )
68 | if err != nil {
69 | writeLine( w, fw, err.Error() )
70 | }
71 | err = copyFileContents( configFile, tempFile.Name() )
72 | if err != nil {
73 | writeLine( w, fw, err.Error() )
74 | }
75 | configSource = tempFile.Name()
76 | }
77 | os.RemoveAll( installDir )
78 | }
79 |
80 | writeLine( w, fw, "Creating install directory %s", installDir )
81 | os.MkdirAll( installDir, 0755 )
82 |
83 | //writeLine( w, fw, "Downloading update information" )
84 |
85 | updateFolder := config.Get("updates").String()
86 | if !dirExists( updateFolder ) {
87 | writeLine( w, fw, "Update folder %s did not exist. Created", updateFolder )
88 | os.MkdirAll( updateFolder, 0755 )
89 | }
90 |
91 | updatesDest := updateFolder + "/updates.json"
92 | updatesSrc := updateUrl + "updates.json"
93 | err := download( updatesDest, updatesSrc )
94 | if err != nil {
95 | writeLine( w, fw, "Error downloading update metadata from %s: %s\n", updatesSrc, err )
96 | return
97 | }
98 | updateContent, _ := ioutil.ReadFile( updatesDest )
99 | uRoot, _ := uj.Parse( updateContent )
100 | latest := uRoot.Get("latest").String()
101 | writeLine( w, fw, "Latest update:%s", latest )
102 |
103 | latestDest := updateFolder + "/" + latest
104 | download( latestDest, updateUrl + latest )
105 | latestContent, _ := ioutil.ReadFile( latestDest )
106 | lRoot, _ := uj.Parse( latestContent )
107 | files := lRoot.Get("files")
108 | //writeLine( w, fw, "Files in update:" )
109 | fileArr := []string{}
110 | files.ForEach( func( fileNode *uj.JNode ) {
111 | file := fileNode.String()
112 | //writeLine( w, fw, lineSpace + file )
113 | fileArr = append( fileArr, file )
114 | } )
115 |
116 | writeLine( w, fw, "Downloading files:" )
117 | for _,file := range fileArr {
118 | dest := updateFolder + "/" + file
119 | writeText( w, fw, lineSpace + file + "..." )
120 | src := updateUrl + file
121 | download( dest, src )
122 | writeLine( w, fw, " Done" )
123 | }
124 |
125 | action := lRoot.Get("action").String()
126 | writeLine( w, fw, "Running install ( %s )", action )
127 | parts := strings.Split( action, " " )
128 | parts[0] = updateFolder + "/" + parts[0]
129 |
130 | os.Chmod( parts[0], 0770 )
131 | cmd := gocmd.NewCmdOptions( gocmd.Options{ Streaming: true }, parts[0], parts[1:]... )
132 |
133 | env := map[string] string {
134 | "CONFIG_SRC": configSource,
135 | "INSTALL_DIR": installDir,
136 | "UPDATE_DIR": updateFolder,
137 | }
138 |
139 | var envArr []string
140 | for k,v := range( env ) {
141 | envArr = append( envArr, k + "=" + v )
142 | }
143 | cmd.Env = envArr
144 |
145 | statCh := cmd.Start()
146 |
147 | outStream := cmd.Stdout
148 | errStream := cmd.Stderr
149 | runDone := false
150 | for {
151 | select {
152 | case <- statCh:
153 | runDone = true
154 | case line := <- outStream:
155 | line = strings.Replace( line, "[32m", "", 1 )
156 | line = strings.Replace( line, "[91m", "", 1 )
157 | line = strings.Replace( line, "[0m", "", 1 )
158 | writeLine( w, fw, line )
159 | case line := <- errStream:
160 | writeLine( w, fw, "err:%s", line )
161 | }
162 | if runDone { break }
163 | }
164 |
165 | writeLine( w, fw, "Install complete" )
166 |
167 | if configWithin {
168 | os.Remove( configSource )
169 | }
170 |
171 | writeLine( w, fw, "Starting coordinator" )
172 | info.proc.Start()
173 | }
174 |
175 | // copyFileContents copies the contents of the file named src to the file named
176 | // by dst. The file will be created if it does not already exist. If the
177 | // destination file exists, all it's contents will be replaced by the contents
178 | // of the source file.
179 | func copyFileContents(src, dst string) (err error) {
180 | in, err := os.Open(src)
181 | if err != nil {
182 | return
183 | }
184 | defer in.Close()
185 | out, err := os.Create(dst)
186 | if err != nil {
187 | return
188 | }
189 | defer func() {
190 | cerr := out.Close()
191 | if err == nil {
192 | err = cerr
193 | }
194 | }()
195 | if _, err = io.Copy(out, in); err != nil {
196 | return
197 | }
198 | err = out.Sync()
199 | return err
200 | }
201 |
202 | func download( dest string, url string) error {
203 | resp, err := http.Get( url )
204 | if err != nil { return err }
205 | defer resp.Body.Close()
206 |
207 | out, err := os.Create( dest )
208 | if err != nil { return err }
209 | defer out.Close()
210 |
211 | _, err = io.Copy(out, resp.Body)
212 | return err
213 | }
--------------------------------------------------------------------------------
/runner/versions.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | uj "github.com/nanoscopic/ujsonin/mod"
5 | "io/ioutil"
6 | "os/exec"
7 | "strings"
8 | "os"
9 | "fmt"
10 | )
11 |
12 | func loadVersionInfo( vmap map[string] VersionInfo ) {
13 | if !fileExists( "bin/bins.json" ) {
14 | fmt.Printf("bin/bins.json file does not exist; cannot do extended version check")
15 | return
16 | }
17 |
18 | content, _ := ioutil.ReadFile("bin/bins.json")
19 | root, _ := uj.Parse( content )
20 |
21 | bins := root.Get("bins")
22 |
23 | bins.ForEach( func( item *uj.JNode ) {
24 | //short := item.Get("short").String()
25 | name := item.Get("name").String()
26 | cmd := item.Get("cmd").String()
27 |
28 | arr := strings.Split( "./bin/" + cmd, " " )
29 | out, err := exec.Command(arr[0], arr[1:]...).Output()
30 |
31 | if err != nil {
32 | fmt.Printf("err running %s : %s\n", "./bin/" + cmd, err )
33 | return
34 | }
35 |
36 | vmap[ name ] = processVersionText( string(out) )
37 | } )
38 | }
39 |
40 | func processVersionText( text string ) ( VersionInfo ) {
41 | lines := strings.Split( text, "\n" )
42 | var res VersionInfo = VersionInfo{}
43 | for _,line := range lines {
44 | if strings.HasPrefix( line, "Commit:" ) { res.GitCommit = line[7:] }
45 | if strings.HasPrefix( line, "Date:" ) { res.GitDate = line[5:] }
46 | if strings.HasPrefix( line, "Remote:" ) { res.GitRemote = line[7:] }
47 | if strings.HasPrefix( line, "Version:" ) { res.EasyVersion = line[8:] }
48 | }
49 | return res
50 | }
51 |
52 | func fileExists(filename string) bool {
53 | info, err := os.Stat(filename)
54 | if os.IsNotExist(err) {
55 | return false
56 | }
57 | return !info.IsDir()
58 | }
59 |
60 | func dirExists(filename string) bool {
61 | info, err := os.Stat(filename)
62 | if os.IsNotExist(err) {
63 | return false
64 | }
65 | return info.IsDir()
66 | }
--------------------------------------------------------------------------------
/server/.env:
--------------------------------------------------------------------------------
1 | PUBLIC_IP=192.168.56.108
2 | SECRET=secret
3 | RETHINKDB_PORT_28015_TCP=tcp://rethinkdb:28015
4 | HOSTNAME=stf.test
5 | STF_IMAGE=livxtrm/devicefarmer:latest
6 |
--------------------------------------------------------------------------------
/server/README.md:
--------------------------------------------------------------------------------
1 | ## STF Server Setup
2 | ### Set environment variables
3 | 1. `docker pull openstf/stf:v3.4.1`
4 | 1. Look through docker-compose.yml
5 | 1. Update `.env` with your environment settings
6 |
7 | 1. STF_IMAGE ( custom image if desired otherwise openstf/stf )
8 | 1. HOSTNAME ( hostname of your server )
9 | 1. PUBLIC_IP ( IP address of your server )
10 | 1. Setup certificates for Nginx on your local system
11 |
12 | 1. For testing you can generate a self signed certificate using `cert/gencert.sh`
13 | 1. Pass the paths for those cert in by tweaking the mounted files in docker-compose.yml
14 |
15 | 1. eg: Change the `cert/...` parts
16 | 1. `docker-compose up`
17 | 1. If testing using self signed cert; trust the cert in your browser ( or in keychain on mac )
18 |
--------------------------------------------------------------------------------
/server/cert/gencert.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout server.key -out server.crt -config server.conf -subj "/C=US/ST=Washington/L=Seattle/O=Dis/CN=stf.test"
3 |
--------------------------------------------------------------------------------
/server/cert/server.conf:
--------------------------------------------------------------------------------
1 | [req]
2 | default_bits = 2048
3 | default_keyfile = server.key
4 | distinguished_name = req_distinguished_name
5 | req_extensions = req_ext
6 | x509_extensions = v3_ca
7 |
8 | [req_distinguished_name]
9 | countryName = Country Name (2 letter code)
10 | countryName_default = US
11 | stateOrProvinceName = State or Province Name (full name)
12 | stateOrProvinceName_default = Washington
13 | localityName = Locality Name (eg, city)
14 | localityName_default = Seattle
15 | organizationName = Organization Name (eg, company)
16 | organizationName_default = stf.test
17 | organizationalUnitName = organizationalunit
18 | organizationalUnitName_default = Development
19 | commonName = Common Name (e.g. server FQDN or YOUR name)
20 | commonName_default = stf.test
21 | commonName_max = 64
22 |
23 | [req_ext]
24 | subjectAltName = @alt_names
25 |
26 | [v3_ca]
27 | subjectAltName = @alt_names
28 |
29 | [alt_names]
30 | DNS.1 = stf.test
31 | DNS.2 = 192.168.56.108
32 |
--------------------------------------------------------------------------------
/server/cert/server.crt:
--------------------------------------------------------------------------------
1 | -----BEGIN CERTIFICATE-----
2 | MIIDVDCCAjygAwIBAgIJAIGe4VrJjSC/MA0GCSqGSIb3DQEBCwUAMFUxCzAJBgNV
3 | BAYTAlVTMRMwEQYDVQQIDApXYXNoaW5ndG9uMRAwDgYDVQQHDAdTZWF0dGxlMQww
4 | CgYDVQQKDANEaXMxETAPBgNVBAMMCHN0Zi50ZXN0MB4XDTIwMDgxNDE2MjYyMVoX
5 | DTIxMDgxNDE2MjYyMVowVTELMAkGA1UEBhMCVVMxEzARBgNVBAgMCldhc2hpbmd0
6 | b24xEDAOBgNVBAcMB1NlYXR0bGUxDDAKBgNVBAoMA0RpczERMA8GA1UEAwwIc3Rm
7 | LnRlc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDLaPlh0AiVEpWM
8 | Y5oErTs5Qw3UXsfJWsuEK18b8lDs+nWrHClCAb0Rp9y/oVbNVwuobZGl36oNwUmT
9 | vV4bIBjDpNUDhaJu5rN/rsFV2phIBe/qgJPqz3ty1kFkJ5aFEnTClcuPwYkjU4E+
10 | Ed3jcOAuaCtdkSfWab8/gc+Gn9qL1nAFz3qG5Bbej4SExKI7yp3WRNM4hUjppH5j
11 | EiO8Ti5Jp2d+1Ahzxjc7DQjIkYcCA3KNNXQrzB3GrEq9j5D4sYW6x8NVeqLr7olB
12 | a4ylfEpxDafzakIdtxezFKyElmYenai/DHOAciH7X7XMBHk9IHUa0eCzpjYr5GHa
13 | nujFUpVvAgMBAAGjJzAlMCMGA1UdEQQcMBqCCHN0Zi50ZXN0gg4xOTIuMTY4LjU2
14 | LjEwODANBgkqhkiG9w0BAQsFAAOCAQEAJTAclMsxxD3H/AEo2NregQSQ1p5ET+w6
15 | Mc34GlGa/jycrBL23iXPJQZj0TE/XEiJmjm1BbwTDXJFNTSBAp7+ufnmtnaYPL+o
16 | BEY8VvjCoTek7xmipVO7z2NoeDb+UHVn8PSCXWiDdCR2yY+c2qdTHzZtLUr2w/80
17 | imP1+bl9QS6vnXvc5g0BadtBcxZshjK0OtsnNfsQXmxeY0Y3X0YHpt1D8ZvyD4gf
18 | IjulHgrEWPmDG/dYA/k8ZJfgbRchkzkilmG36lHRpKQ8aIAafJaQp/Z2KaOvyFbv
19 | CGXSfkeMmEQU+hRq1ccbglARkWxRNGkg1aOQmdNEnK49QBREq5FLcA==
20 | -----END CERTIFICATE-----
21 |
--------------------------------------------------------------------------------
/server/cert/server.key:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDLaPlh0AiVEpWM
3 | Y5oErTs5Qw3UXsfJWsuEK18b8lDs+nWrHClCAb0Rp9y/oVbNVwuobZGl36oNwUmT
4 | vV4bIBjDpNUDhaJu5rN/rsFV2phIBe/qgJPqz3ty1kFkJ5aFEnTClcuPwYkjU4E+
5 | Ed3jcOAuaCtdkSfWab8/gc+Gn9qL1nAFz3qG5Bbej4SExKI7yp3WRNM4hUjppH5j
6 | EiO8Ti5Jp2d+1Ahzxjc7DQjIkYcCA3KNNXQrzB3GrEq9j5D4sYW6x8NVeqLr7olB
7 | a4ylfEpxDafzakIdtxezFKyElmYenai/DHOAciH7X7XMBHk9IHUa0eCzpjYr5GHa
8 | nujFUpVvAgMBAAECggEAPu9CwY2xKhZu6NnkTHAgs83YWI3euKD7+O/GZIormbbA
9 | c2mqJj8NdYn/VdcgWTYGaF1GRBEYt1rHXguoMzJSFy5HrehJ4pBEl0vFi7+vgBE+
10 | MssHeQ4q/tPltYw+GPwl3hKkwdy6hpCOm1rB0V4aLqGSUUfZEJD1WDvcrqWE4+Cu
11 | 2Kl8SqsDmUyvWgpQJjVQ+X9mQEsrSebNK/s7vBl1VX5K3RHWW5u2oSg1lgdxYTmM
12 | wUBO/maV+68bZWl3VUfJmwdlenoVahzyUOtxNy36kapqVe1pmq7QMtAkBhemtrw6
13 | rrdCWyjl8mH96rtO8O+mXvkCnuiBurfcmpIpyLwzeQKBgQDpJid/tqZvmzlVNmQi
14 | F4WMmsnj7QwfhkCI2AdnUAy5LtWumcVeQVutq3jHidLkJxTEFDK85xcF/++oftMp
15 | NFdEErt71fA9GQTLg7a7OeyY9eR/gelZNMqEnjjUGKzfj4DyM4WsqTgOZsfb4yPz
16 | GTtj2B7Xf50LjFdPTDXHgptI2wKBgQDfWKa+OTPQpPAj4kWoOEzN4Sr4Y4gQioO/
17 | ksnlmhUgBkhoHVGV1u742mXSpVYRzyQv6H1kLGJRTkEFHl3ozJUneWXmFY7eUNTn
18 | fnuPss1yLuE+wqLJ+iB9mSQGgBdetY5ynB3cYvVup5hDmSV8lHP+Jp3BTdpEx5pp
19 | xyancSBP/QKBgD68oJZSLNkNWNEgMLOnxqz+HeNyLvfwpT7tepiHRtUx0BgKkrx5
20 | M9U4tehjotb32TOmB70jJePcab3aWrHUvsK3k7GP8PRP3iVxTON2g77pM9JHv+Xc
21 | Ob6T4NDZzvLdZ6JE0OyUIFxntdHqfgr1ODD2v93XHgg0fG3/IN2NvIFPAoGBAJBz
22 | 0uyHLLcOZm6fAzRorWwe7N7X6QHhxJJcCw7gGDetOJl2FPVXnRoAjwitfLxp/9qo
23 | gKkQd8pkVXNND6no37M3NiuY19175CeRS7NGDtCB95bS5dzCVM9HA+DcacEMpgQE
24 | at/GdTzLUpSt8WvgzCCdszx58Oi5PGqbrqlvZlm1AoGAWvUHqRIVN5sy6IJPCBoh
25 | SSlSZxPK9hnVnBvWUHxGN3IX9YiAfAx2fpAMC8glR2aKATtKVOWxMgQ2YJ/ZBk7p
26 | SM3D3v5D2RlmLXy7ulvslF+KxGHJWmDxQ4Zuaqiq7+MAEaAwq1A80MYjqqwWnlQH
27 | ZBWwH6vFWgJn7gps7/gW1aE=
28 | -----END PRIVATE KEY-----
29 |
--------------------------------------------------------------------------------
/server/docker-compose.yml:
--------------------------------------------------------------------------------
1 | version: '2'
2 |
3 | volumes:
4 | rethinkdb:
5 | storage-temp:
6 |
7 | services:
8 | nginx:
9 | build: nginx/
10 | volumes:
11 | - ./nginx/nginx.conf:/etc/nginx/nginx.conf
12 | # You'll need to set the paths below to where your certs actually are
13 | - ./cert/server.crt:/etc/nginx/ssl/cert.crt
14 | - ./cert/server.key:/etc/nginx/ssl/cert.key
15 | restart: unless-stopped
16 | ports:
17 | - 80:80
18 | - 443:443
19 | depends_on:
20 | - app
21 | - auth
22 | - storage-plugin-apk
23 | - storage-plugin-image
24 | - storage-temp
25 | - websocket
26 | - api
27 | rethinkdb:
28 | image: rethinkdb:2.3
29 | restart: unless-stopped
30 | ports:
31 | - 8080:8080
32 | volumes:
33 | - rethinkdb:/data
34 | app:
35 | image: ${STF_IMAGE}
36 | restart: unless-stopped
37 | environment:
38 | - RETHINKDB_PORT_28015_TCP
39 | - SECRET
40 | command: >
41 | node runcli.js app
42 | --auth-url https://${HOSTNAME}/auth/mock/
43 | --websocket-url wss://${HOSTNAME}/ --port 3000
44 | volumes:
45 | - ./runcli.js:/app/runcli.js
46 | ports:
47 | - 10006:9229
48 | depends_on:
49 | - rethinkdb
50 | - auth
51 | - websocket
52 | auth:
53 | image: ${STF_IMAGE}
54 | restart: unless-stopped
55 | volumes:
56 | - ./runcli.js:/app/runcli.js
57 | environment:
58 | - SECRET
59 | - RETHINKDB_PORT_28015_TCP
60 | command: node runcli.js auth-mock --app-url http://${HOSTNAME}/ --port 3000
61 | processor:
62 | image: ${STF_IMAGE}
63 | restart: unless-stopped
64 | environment:
65 | - RETHINKDB_PORT_28015_TCP
66 | command: >
67 | node runcli.js processor
68 | --connect-app-dealer tcp://triproxy:7160
69 | --connect-dev-dealer tcp://dev-triproxy:7260
70 | volumes:
71 | - ./runcli.js:/app/runcli.js
72 | ports:
73 | - 10002:9229
74 | depends_on:
75 | - rethinkdb
76 | - triproxy
77 | - dev-triproxy
78 | triproxy:
79 | image: ${STF_IMAGE}
80 | restart: unless-stopped
81 | command: >
82 | node runcli.js triproxy app
83 | --bind-pub "tcp://*:7150"
84 | --bind-dealer "tcp://*:7160"
85 | --bind-pull "tcp://*:7170"
86 | volumes:
87 | - ./runcli.js:/app/runcli.js
88 | ports:
89 | - 10005:9229
90 | dev-triproxy:
91 | image: ${STF_IMAGE}
92 | restart: unless-stopped
93 | command: >
94 | node runcli.js triproxy dev
95 | --bind-pub "tcp://*:7250"
96 | --bind-dealer "tcp://*:7260"
97 | --bind-pull "tcp://*:7270"
98 | volumes:
99 | - ./runcli.js:/app/runcli.js
100 | ports:
101 | - 7250:7250
102 | - 7270:7270
103 | - 10003:9229
104 | migrate:
105 | image: ${STF_IMAGE}
106 | environment:
107 | - RETHINKDB_PORT_28015_TCP
108 | volumes:
109 | - ./runcli.js:/app/runcli.js
110 | command: node runcli.js migrate
111 | depends_on:
112 | - rethinkdb
113 | reaper:
114 | image: ${STF_IMAGE}
115 | restart: unless-stopped
116 | environment:
117 | - RETHINKDB_PORT_28015_TCP
118 | depends_on:
119 | - migrate
120 | - rethinkdb
121 | - dev-triproxy
122 | - triproxy
123 | volumes:
124 | - ./runcli.js:/app/runcli.js
125 | command: >
126 | node runcli.js reaper dev
127 | --connect-push tcp://dev-triproxy:7270
128 | --connect-sub tcp://triproxy:7150
129 | --heartbeat-timeout 30000
130 | storage-plugin-apk:
131 | image: ${STF_IMAGE}
132 | restart: unless-stopped
133 | volumes:
134 | - ./runcli.js:/app/runcli.js
135 | command: node runcli.js storage-plugin-apk --port 3000 --storage-url http://${PUBLIC_IP}/
136 | depends_on:
137 | - storage-temp
138 | storage-plugin-image:
139 | image: ${STF_IMAGE}
140 | restart: unless-stopped
141 | volumes:
142 | - ./runcli.js:/app/runcli.js
143 | command: node runcli.js storage-plugin-image --port 3000 --storage-url http://${PUBLIC_IP}/
144 | depends_on:
145 | - storage-temp
146 | storage-temp:
147 | build: storage-temp/
148 | restart: unless-stopped
149 | volumes:
150 | - storage-temp:/app/data
151 | - ./runcli.js:/app/runcli.js
152 | command: node runcli.js storage-temp --port 3000 --save-dir /app/data
153 | websocket:
154 | image: ${STF_IMAGE}
155 | restart: unless-stopped
156 | environment:
157 | - SECRET
158 | - RETHINKDB_PORT_28015_TCP
159 | command: >
160 | node runcli.js
161 | websocket
162 | --port 3000
163 | --storage-url "http://${PUBLIC_IP}/"
164 | --connect-sub "tcp://triproxy:7150"
165 | --connect-push "tcp://triproxy:7170"
166 | volumes:
167 | - ./runcli.js:/app/runcli.js
168 | ports:
169 | - 10004:9229
170 | depends_on:
171 | - migrate
172 | - rethinkdb
173 | - storage-temp
174 | - triproxy
175 | - dev-triproxy
176 | api:
177 | image: ${STF_IMAGE}
178 | restart: unless-stopped
179 | environment:
180 | - SECRET
181 | - RETHINKDB_PORT_28015_TCP
182 | command: >
183 | node runcli.js
184 | api
185 | --port 3000
186 | --connect-sub tcp://triproxy:7150
187 | --connect-push tcp://triproxy:7170
188 | --connect-sub-dev tcp://dev-triproxy:7250
189 | --connect-push-dev tcp://dev-triproxy:7270
190 | ports:
191 | - 10001:9229
192 | volumes:
193 | - ./runcli.js:/app/runcli.js
194 | ports:
195 | - 9229:9229
196 | depends_on:
197 | - migrate
198 | - rethinkdb
199 | - triproxy
200 | - dev-triproxy
201 |
202 |
--------------------------------------------------------------------------------
/server/nginx/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM nginx:mainline
2 |
3 | COPY ./entrypoint.sh /
4 | RUN chmod +x /entrypoint.sh
5 |
6 | CMD ["/entrypoint.sh"]
7 |
--------------------------------------------------------------------------------
/server/nginx/entrypoint.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | NAMESERVER=$(awk '/nameserver/{print $2}' /etc/resolv.conf | tr '\\n' ' ')
4 | RESOLVER_CONFIG="/etc/nginx/conf.d/resolver.conf"
5 | echo Got nameserver $NAMESERVER from resolv.conf
6 | echo Writing include file at $RESOLVER_CONFIG
7 | echo "resolver $NAMESERVER;" > $RESOLVER_CONFIG
8 | nginx -g 'daemon off;'
9 |
--------------------------------------------------------------------------------
/server/nginx/nginx.conf:
--------------------------------------------------------------------------------
1 | worker_processes auto;
2 |
3 | events {
4 | worker_connections 1024;
5 | }
6 |
7 | http {
8 | include /etc/nginx/conf.d/resolver.conf;
9 | keepalive_timeout 65;
10 | types_hash_max_size 2048;
11 |
12 | default_type application/octet-stream;
13 |
14 | upstream stf_app {
15 | server app:3000 max_fails=0;
16 | }
17 |
18 | upstream stf_auth {
19 | server auth:3000 max_fails=0;
20 | }
21 |
22 | upstream stf_storage_apk {
23 | server storage-plugin-apk:3000 max_fails=0;
24 | }
25 |
26 | upstream stf_storage_image {
27 | server storage-plugin-image:3000 max_fails=0;
28 | }
29 |
30 | upstream stf_storage {
31 | server storage-temp:3000 max_fails=0;
32 | }
33 |
34 | upstream stf_websocket {
35 | server websocket:3000 max_fails=0;
36 | }
37 |
38 | upstream stf_api {
39 | server api:3000 max_fails=0;
40 | }
41 |
42 | types {
43 | application/javascript js;
44 | image/gif gif;
45 | image/jpeg jpg;
46 | text/css css;
47 | text/html html;
48 | }
49 |
50 | map $http_upgrade $connection_upgrade {
51 | default upgrade;
52 | '' close;
53 | }
54 |
55 | server {
56 | listen 80;
57 | server_name _;
58 | return 301 https://$host$request_uri;
59 | }
60 |
61 |
62 | server {
63 | listen 443 ssl;
64 | server_name _;
65 |
66 | ssl_certificate /etc/nginx/ssl/cert.crt;
67 | ssl_certificate_key /etc/nginx/ssl/cert.key;
68 | ssl_session_timeout 5m;
69 | ssl_session_cache shared:SSL:10m;
70 | ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
71 |
72 | server_tokens off;
73 | root /dev/null;
74 |
75 | # Client IP should be changed from [^/] to some more specific range such as:
76 | # (?192.168.255.[0-9]+) to restrict it to a reasonable IP range
77 | # If left alone this example config will let clients arbitrarily tunnel to any IP on ports 8000-8009
78 | location ~ "^/frames/(?[^/]+)/(?800[0-9])/x$" {
79 | proxy_pass http://$client_ip:$client_port/echo/;
80 | proxy_http_version 1.1;
81 | proxy_set_header Upgrade $http_upgrade;
82 | proxy_set_header Connection $connection_upgrade;
83 | proxy_set_header X-Forwarded-For $remote_addr;
84 | proxy_set_header X-Real-IP $remote_addr;
85 | }
86 |
87 | location /auth/ {
88 | proxy_pass http://stf_auth/auth/;
89 | }
90 |
91 | location /api/ {
92 | proxy_pass http://stf_api/api/;
93 | }
94 |
95 | location /s/image/ {
96 | proxy_pass http://stf_storage_image;
97 | }
98 |
99 | location /s/apk/ {
100 | proxy_pass http://stf_storage_apk;
101 | }
102 |
103 | location /s/ {
104 | client_max_body_size 1024m;
105 | client_body_buffer_size 128k;
106 | proxy_pass http://stf_storage;
107 | }
108 |
109 | location /socket.io/ {
110 | proxy_pass http://stf_websocket;
111 | proxy_http_version 1.1;
112 | proxy_set_header Upgrade $http_upgrade;
113 | proxy_set_header Connection $connection_upgrade;
114 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
115 | proxy_set_header X-Real-IP $http_x_real_ip;
116 | }
117 |
118 | # This .well-known path is mapped to make it easier to use letsencrypt for certs
119 | location /.well-known/ {
120 | }
121 |
122 | location / {
123 | proxy_pass http://stf_app;
124 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
125 | proxy_set_header X-Real-IP $http_x_real_ip;
126 | }
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/server/runcli.js:
--------------------------------------------------------------------------------
1 | require('./lib/cli')
2 |
--------------------------------------------------------------------------------
/server/storage-temp/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM livxtrm/devicefarmer:latest
2 |
3 | USER root
4 | RUN mkdir data && chown stf:stf data
5 | USER stf
6 | VOLUME ["data"]
7 |
--------------------------------------------------------------------------------
/stf_ios_support.rb:
--------------------------------------------------------------------------------
1 | class StfIosSupport < Formula
2 | desc "OpenSTF IOS Device Provider"
3 | homepage ""
4 | url "https://github.com/nanoscopic/empty/archive/empty.tar.gz"
5 | version "1.0.0"
6 | sha256 "324c7d7662fd392fa2b7e0c9ce2bb9fd2ff677403c31311b13ac64bd1a15cbf7"
7 |
8 | def install
9 | system "touch #{prefix}/intentionally_empty_install"
10 | end
11 |
12 | # depends_on "cmake" => :build
13 | depends_on "jq"
14 | # depends_on "rethinkdb"
15 | depends_on "graphicsmagick"
16 | depends_on "zeromq"
17 | depends_on "protobuf"
18 | depends_on "yasm"
19 | depends_on "pkg-config"
20 | depends_on "carthage"
21 | depends_on "automake"
22 | depends_on "autoconf"
23 | depends_on "libtool"
24 | depends_on "wget"
25 | # depends_on "libimobiledevice" # need to install with --HEAD
26 | depends_on "go" => :build
27 | depends_on :xcode => "10.3"
28 | depends_on "node@12"
29 | depends_on "libsodium"
30 | depends_on "czmq"
31 | depends_on "jpeg-turbo"
32 | depends_on "nanomsg"
33 | depends_on "libgcrypt"
34 | depends_on "gnutls"
35 | depends_on "mobiledevice"
36 | # depends_on "libplist" # need to install with --HEAD
37 | # depends_on "libusbmuxd" # need to install with --HEAD
38 | end
39 |
--------------------------------------------------------------------------------
/tblick-info.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | CONFNAME="$1"
4 | VPERR=""
5 |
6 | # Junk below mostly copied from Tunnelblick client.2.up.tunnelblick.sh
7 | TBCONFIG="/Users/$USER/Library/Application Support/Tunnelblick/Configurations/$1.tblk/Contents/Resources/config.ovpn"
8 | if [ ! -f "$TBCONFIG" ]; then
9 | TBCONFIG="/Library/Application Support/Tunnelblick/Shared/$1.tblk/Contents/Resources/config.ovpn"
10 | fi
11 |
12 | if [ -f "$TBCONFIG" ]; then
13 | TBALTPREFIX="/Library/Application Support/Tunnelblick/Users/"
14 | TBALTPREFIXLEN="${#TBALTPREFIX}"
15 | TBCONFIGSTART="${TBCONFIG:0:$TBALTPREFIXLEN}"
16 | if [ "$TBCONFIGSTART" = "$TBALTPREFIX" ] ; then
17 | TBBASE="${TBCONFIG:$TBALTPREFIXLEN}"
18 | TBSUFFIX="${TBBASE#*/}"
19 | TBUSERNAME="${TBBASE%%/*}"
20 | TBCONFIG="/Users/$TBUSERNAME/Library/Application Support/Tunnelblick/Configurations/$TBSUFFIX"
21 | fi
22 |
23 | CONFIG_PATH_DASHES_SLASHES="$(echo "${TBCONFIG}" | sed -e 's/-/--/g' | sed -e 's/\//-S/g')"
24 |
25 | # Determine IP adddress and Tunnel name of most recent connection
26 | LF=$(find "/Library/Application Support/Tunnelblick/Logs/" -type f -iname "${CONFIG_PATH_DASHES_SLASHES}*.openvpn.log")
27 | if [ -f "$LF" ]; then
28 | LINE=$(cat "$LF" | grep -E "ifconfig utun[0-9]+ [0-9]+" | tail -1)
29 | if [ "$LINE" == "" ]; then
30 | VPERR="Config '$CONFNAME' does not appear to have ever connected"
31 | else
32 | IPADDR=$(echo $LINE| cut -d \ -f 5)
33 | TUN=$(echo $LINE| cut -d \ -f 4)
34 | fi
35 | else
36 | VPERR="Config '$CONFNAME' does not have any log"
37 | fi
38 | else
39 | VPERR="Config '$CONFNAME' does not appear to exist in Tunnelblick"
40 | fi
41 |
42 | echo "{"
43 | echo " \"err\": \"$VPERR\","
44 | echo " \"ipAddr\": \"$IPADDR\","
45 | echo " \"tunName\": \"$TUN\""
46 | echo "}"
47 |
--------------------------------------------------------------------------------
/temp/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dryark/stf_ios_support/2f32cbbf1506a740eb21a496c1cdc11d875558f7/temp/.gitkeep
--------------------------------------------------------------------------------
/update_server/Makefile:
--------------------------------------------------------------------------------
1 | TARGET = server
2 |
3 | all: $(TARGET)
4 |
5 | runner_sources := $(wildcard *.go)
6 |
7 | $(TARGET): $(runner_sources) go.sum
8 | go build -o $(TARGET) -ldflags "-X main.GitCommit=$(GIT_COMMIT) -X main.GitDate=$(GIT_DATE) -X main.GitRemote=$(GIT_REMOTE) -X main.EasyVersion=$(EASY_VERSION)" .
9 |
10 | go.sum:
11 | go get
12 | go get .
13 |
14 | clean:
15 | $(RM) $(TARGET) go.sum
--------------------------------------------------------------------------------
/update_server/go.mod:
--------------------------------------------------------------------------------
1 | module main.go
2 |
3 | go 1.12
4 |
5 | require (
6 | )
7 |
--------------------------------------------------------------------------------
/update_server/main.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "net/http"
5 | "fmt"
6 | )
7 |
8 | func main() {
9 | fs := http.FileServer( http.Dir( "./updates" ) )
10 | fmt.Println( http.ListenAndServe( ":8022", fs ) )
11 | }
--------------------------------------------------------------------------------
/update_server/updates/index.html:
--------------------------------------------------------------------------------
1 | updates
2 |
--------------------------------------------------------------------------------
/update_server/updates/remote.pl:
--------------------------------------------------------------------------------
1 | #!/usr/bin/perl -w
2 | use strict;
3 | use File::Copy;
4 |
5 | my $configSrc = $ENV{CONFIG_SRC} or die "ENV CONFIG_SRC not set";
6 | my $installDir = $ENV{INSTALL_DIR} or die "ENV INSTALL_DIR not set";
7 | my $updateDir = $ENV{UPDATE_DIR} or die "ENV UPDATE_DIR not set";
8 |
9 | my $dist = $ARGV[0];
10 |
11 | print "Extracting $dist to $installDir\n";
12 |
13 | if( ! -e "/usr/local/bin/pv" ) {
14 | print "pv not installed. installing...\n";
15 | system("/usr/local/bin/brew install pv");
16 | }
17 |
18 | print STDERR "PROGSTART\n";
19 | system("/usr/local/bin/pv -n $updateDir/$dist | /usr/bin/tar -xf - -C $installDir");
20 | print STDERR "PROGEND\n";
21 |
22 | my $configDest = "$installDir/config.json";
23 | print "Copying $configSrc to $configDest\n";
24 | copy( $configSrc, $configDest );
25 |
26 | chdir $installDir;
27 | $ENV{'PATH'} = "/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin";
28 | system('./init.sh');
--------------------------------------------------------------------------------
/update_server/updates/updates.json:
--------------------------------------------------------------------------------
1 | {
2 | "latest": "v1.json"
3 | }
4 |
--------------------------------------------------------------------------------
/update_server/updates/v1.json:
--------------------------------------------------------------------------------
1 | {
2 | "files": [
3 | "remote.pl",
4 | "v1.tgz"
5 | ],
6 | "action": "remote.pl v1.tgz",
7 | "provides": [
8 | [ "coordinator", "1" ]
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/util/alf2.pl:
--------------------------------------------------------------------------------
1 | #!/usr/bin/perl -w
2 | use strict;
3 | use Data::Dumper;
4 | use MIME::Base64;
5 | use JSON::PP;
6 |
7 | my $checkapp = '';
8 | my $action = $ARGV[0] || '';
9 | if( $action eq 'permok' || $action eq 'ensureperm' ) {
10 | $checkapp = $ARGV[1] || "/Applications/STF Coordinator.app";
11 | }
12 | elsif( $action eq 'dump' ) {}
13 | else {
14 | print "ALF Firewall / Network Permissions Tool\n
15 | Usage:
16 | ./alf2.pl [action] [args]
17 | Actions:
18 | dump - dump contents of permissions
19 | permok [path to app or binary] - check if a app or binary has permission
20 | ensureperm [path to app or binary] - ensure an app or binary has permission\n";
21 | }
22 |
23 | my $i = 1;
24 | my $app;
25 | my %apps;
26 | for my $line ( `/usr/libexec/ApplicationFirewall/socketfilterfw --listapps` ) {
27 | if( $line =~ m/^$i\s+:\s+(.+?)\s*$/ ) {
28 | $app = $1;
29 | $i++;
30 | }
31 |
32 | if( $line =~ m/Allow incoming connections/ ) {
33 | if( !$checkapp || $checkapp eq $app ) {
34 | my $csreq = get_cs_req( $i - 2 );
35 | chomp $csreq;
36 | my $info = {
37 | csreq => $csreq,
38 | valid => 1
39 | };
40 | if( $csreq =~ m/cdhash H"(.+?)"/ ) {
41 | my $sha1 = uc($1);
42 | my $cdhash = get_cdhash( $app );
43 | $info->{ valid } = ( $sha1 eq $cdhash ) ? 1 : 0;
44 | }
45 | $apps{ $app } = $info;
46 | }
47 | else {
48 | $apps{ $app } = {};
49 | }
50 | }
51 | }
52 |
53 | if( $action eq 'permok' ) {
54 | my $info = $apps{ $checkapp };
55 | print $info->{valid} ? 'yes' : 'no';
56 | }
57 | elsif( $action eq 'ensureperm' ) {
58 | # TODO
59 | }
60 | elsif( $action eq 'dump' ) {
61 | print JSON::PP->new->ascii->pretty->encode( \%apps );
62 | }
63 |
64 | sub get_cs_req {
65 | my $i = shift;
66 | my $grab = 0;
67 | my $b64;
68 | for my $line ( `plutil -extract applications.$i.reqdata xml1 -o - /Library/Preferences/com.apple.alf.plist` ) {
69 | if( $line =~ m// ) {
70 | $grab = 1;
71 | next;
72 | }
73 | if( $grab ) {
74 | $b64 = $line;
75 | last;
76 | }
77 | }
78 | my $raw = decode_base64( $b64 );
79 | open( my $fh, ">csreq.bin" );
80 | binmode( $fh );
81 | print $fh $raw;
82 | my $csreq = `csreq -r csreq.bin -t`;
83 | close( $fh );
84 | unlink( "csreq.bin" );
85 | return $csreq;
86 | }
87 |
88 | sub get_cdhash {
89 | my $path = shift;
90 | for my $line ( `codesign -dvvv "$path" 2>&1` ) {
91 | if( $line =~ m/^CDHash=(.+?)\s*$/ ) {
92 | return uc($1);
93 | }
94 | }
95 | return '';
96 | }
--------------------------------------------------------------------------------
/util/brewser.pl:
--------------------------------------------------------------------------------
1 | #!/usr/bin/perl -w
2 | use strict;
3 | use JSON::PP qw/decode_json/;
4 | use Data::Dumper;
5 | use Carp qw/confess/;
6 |
7 | my $GR="\033[32m";
8 | my $RED="\033[91m";
9 | my $RST="\033[0m";
10 | my $action = $ARGV[0] || 'help';
11 |
12 | if( !`which brew` ) {
13 | print "Brew must be installed\n";
14 | help();
15 | exit(1);
16 | }
17 |
18 | if( $action eq 'list' ) {
19 | my $pkgs = get_pkg_versions();
20 | for my $pkg ( keys %$pkgs ) {
21 | my $ver = $pkgs->{$pkg};
22 | print "$pkg,$ver\n";
23 | }
24 | }
25 | elsif( $action eq 'installdeps' ) {
26 | my $rbspec = $ARGV[1] or die "Ruby spec file must be given";
27 | my $spec = read_file( $rbspec );
28 | my $pkgs = get_pkg_versions();
29 | my @need;
30 | for my $line ( split( "\n", $spec ) ) {
31 | if( $line =~ m/^\s*depends_on "(.+?)"/ ) {
32 | my $dep = $1;
33 | if( my $ver = $pkgs->{ $dep } ) {
34 | print "$GR$dep\t\t=> version $ver$RST\n";
35 | }
36 | else {
37 | push( @need, $dep );
38 | }
39 | }
40 | }
41 | if( @need ) {
42 | my $allneed = join(' ',@need);
43 | print "Installing missing packages:\n";
44 | print " ".join("\n ",@need);
45 | `brew install $allneed 1>&2`;
46 | }
47 | }
48 | elsif( $action eq 'checkdeps' ) {
49 | my $rbspec = $ARGV[1] or die "Ruby spec file must be given";
50 | my $spec = read_file( $rbspec );
51 | my $pkgs = get_pkg_versions();
52 | my @need;
53 | for my $line ( split( "\n", $spec ) ) {
54 | if( $line =~ m/^\s*depends_on "(.+?)"/ ) {
55 | my $dep = $1;
56 | if( my $ver = $pkgs->{ $dep } ) {
57 | print "$GR$dep\t\t=> version $ver$RST\n";
58 | }
59 | else {
60 | push( @need, $dep );
61 | }
62 | }
63 | }
64 | if( @need ) {
65 | my $allneed = join(' ',@need);
66 | print "Missing brew package(s):\n";
67 | print " ".join("\n ",@need);
68 | }
69 | }
70 | elsif( $action eq 'info' ) {
71 | my $pkg = $ARGV[1];
72 | my ( $info, $ver ) = install_info( $pkg );
73 | if( !$info ) {
74 | print "$pkg is not installed\n";
75 | exit 1;
76 | }
77 | print JSON::PP->new->ascii->pretty->encode( $info );
78 | if( $ver =~ m/HEAD/ ) {
79 | my $headVersion = head_version( $pkg );
80 | print "HEAD version = $headVersion\n";
81 | }
82 | }
83 | elsif( $action eq 'ensurehead' ) {
84 | ensure_head( $ARGV[1], $ARGV[2] || '' );
85 | }
86 | elsif( $action eq 'fixpc' ) {
87 | fix_pc( $ARGV[1], $ARGV[2] );
88 | }
89 | else {
90 | help();
91 | }
92 |
93 | sub help {
94 | print "Brewser
95 | Usage:
96 | ./brewser.pl [action] [args]
97 | Actions:
98 | list - list packages and versions installed
99 | info [package name] - pretty print json install receipt of named package
100 | ensurehead [package name] - ensure HEAD version of a package is installed
101 | If a non-HEAD version is installed, it will be removed and the current HEAD installed.
102 | If a HEAD version is installed, even if old, nothing will happen.
103 | installdeps [ruby spec file] - install dependencies for a specified brew package spec file
104 | fixpc [package name] [version] - Ensure both [pkg].pc and [pkg]-[ver].pc exist\n";
105 | }
106 | sub get_pkg_versions {
107 | my %pkgs;
108 | my @dirs = sort `find /usr/local/Cellar -name .brew -maxdepth 3 -type d`;
109 | for my $dir ( @dirs ) {
110 | $pkgs{ $1 } = $2 if( $dir =~ m|^/usr/local/Cellar/([^/]+)/([^/]+)/\.brew$| );
111 | }
112 | return \%pkgs;
113 | }
114 |
115 | sub read_file {
116 | my $file = shift;
117 | open( my $fh, "<$file" ) or confess("Could not open $file");
118 | my $data;
119 | { local $/ = undef; $data = <$fh>; }
120 | close( $fh );
121 | return $data;
122 | }
123 |
124 | sub install_info {
125 | my ( $pkg, $ver ) = @_;
126 | if( !$ver ) {
127 | my $path = `find /usr/local/Cellar/$pkg -maxdepth 1 2>/dev/null | tail -1`;
128 | chomp $path;
129 | return 0 if( !$path );
130 | my @parts = split( "/", $path );
131 | $ver = pop @parts;
132 | }
133 | my $receiptFile = "/usr/local/Cellar/$pkg/$ver/INSTALL_RECEIPT.json";
134 | #print "Checking $receiptFile\n";
135 | return 0 if( ! -e $receiptFile );
136 | return decode_json( read_file( $receiptFile ) ), $ver;
137 | }
138 |
139 | sub files {
140 | my $path = shift;
141 | opendir( my $DIR, $path );
142 | my @files = readdir( $DIR );
143 | my @outfiles;
144 | for my $file ( @files ) {
145 | next if( $file =~ m/^\.+$/ );
146 | push( @outfiles, $file );
147 | }
148 | closedir( $DIR );
149 | return @outfiles;
150 | }
151 |
152 | sub pkg_pc_file {
153 | my ( $pkg ) = @_;
154 | #my $pc = "/usr/local/lib/pkgconfig/$pkg.pc";
155 | my $path = `find /usr/local/Cellar/$pkg -maxdepth 1 2>/dev/null | tail -1`;
156 | chomp $path;
157 | return 0 if( !$path );
158 | my $pcPath = "$path/lib/pkgconfig/";
159 | my @pcFiles = files( $pcPath );
160 | my $pc = "";
161 | for my $pcFile ( @pcFiles ) {
162 | if( $pcFile =~ m/$pkg(\-|\.)/ ) {
163 | $pc = "$pcPath/$pcFile";
164 | last;
165 | }
166 | }
167 | return 0 if( !$pc );
168 | return $pc;
169 | }
170 |
171 | sub head_version {
172 | my $pkg = shift;
173 | my $pc = pkg_pc_file( $pkg );
174 | return 0 if( !$pc );
175 | my $version = `cat $pc | grep Version | cut -d\\ -f2`;
176 | chomp $version;
177 | return $version;
178 | }
179 |
180 | sub ensure_head {
181 | my ( $pkg, $ver ) = @_;
182 | my ( $info, $iv ) = install_info( $pkg );
183 | my $spec = $info ? $info->{source}{spec} : '';
184 | if( !$spec || $spec ne 'head' ) {
185 | print "$pkg - Installing HEAD\n";
186 | `brew uninstall $pkg --ignore-dependencies` if( $spec );
187 | `brew install --HEAD $pkg`;
188 | }
189 | else {
190 | print "$GR$pkg - HEAD already installed$RST\n";
191 | if( $ver ) {
192 | my $installedVer = head_version( $pkg );
193 | my $greater = version_gte( $ver, $installedVer );
194 | if( !$greater ) {
195 | print "Installed HEAD version is $installedVer; need $ver\n";
196 | `brew uninstall $pkg --ignore-dependencies`;
197 | `brew install --HEAD $pkg`;
198 | }
199 | else {
200 | if( $greater == 1 ) { print "$GR$pkg - installed HEAD is version ${installedVer} ( ==$ver )$RST\n"; }
201 | elsif( $greater == 2 ) { print "$GR$pkg - installed HEAD is version ${installedVer} ( >$ver )$RST\n"; }
202 | }
203 | }
204 | }
205 | }
206 |
207 | sub fix_pc {
208 | my ( $pkg, $ver ) = @_;
209 | my $f1 = "/usr/local/lib/pkgconfig/$pkg.pc";
210 | my $f2 = "/usr/local/lib/pkgconfig/$pkg-$ver.pc";
211 |
212 | my $pc = pkg_pc_file( $pkg );
213 | if( !$pc ) {
214 | print "Could not fix pkgconfig for $pkg; could not locate installed pc file in Cellar\n";
215 | return
216 | }
217 | if( ! -e $f2 ) {
218 | print "$f2 was missing; creating symlink to $pc\n";
219 | `ln -s $pc $f2`;
220 | }
221 | if( ! -e $f1 ) {
222 | print "$f1 was missing; creating symlink to $pc\n";
223 | `ln -s $pc $f1`;
224 | }
225 | }
226 |
227 | sub version_gte {
228 | my ( $v1, $v2 ) = @_;
229 | my @p1 = split(/\./,$v1);
230 | my @p2 = split(/\./,$v2);
231 | for( my $i=0; $i<3; $i++ ) {
232 | my $n1 = $p1[ $i ];
233 | my $n2 = $p2[ $i ];
234 | #print "Comparing $n1 $n2\n";
235 | return 2 if( $n2 > $n1 );
236 | return 0 if( $n2 < $n1 );
237 | }
238 | return 1;
239 | }
--------------------------------------------------------------------------------
/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 = "Mac Developer";
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 | }
--------------------------------------------------------------------------------
/util/tcc.pl:
--------------------------------------------------------------------------------
1 | #!/usr/bin/perl -w
2 | use strict;
3 | use Data::Dumper;
4 | use IPC::Open2;
5 | use File::Temp qw/tempfile/;
6 |
7 | my $user = getlogin();
8 | my $db = "/Users/$user/Library/Application Support/com.apple.TCC/TCC.db";
9 | my @cols = qw/
10 | service
11 | client
12 | client_type
13 | allowed
14 | prompt_count
15 | csreq
16 | policy_id
17 | indirect_object_identifier
18 | indirect_object_code_identity
19 | flags
20 | last_modified
21 | /;
22 |
23 | my %usehex = (
24 | csreq => 1,
25 | indirect_object_code_identity => 1
26 | );
27 |
28 | my $action = $ARGV[0] || 'usage';
29 | if ( $action eq 'getcamera' ) { print_all_camera_access(); }
30 | elsif( $action eq 'getcontrol' ) { print_all_control(); }
31 | elsif( $action eq 'getall' ) { print_all_access(); }
32 | elsif( $action eq 'addcamera' ) { add_camera_access( $ARGV[1] || '/Applications/STF Coordinator.app' ); }
33 | elsif( $action eq 'delcamera' ) { del_camera_access( $ARGV[1] || '/Applications/STF Coordinator.app' ); }
34 | elsif( $action eq 'hascamera' ) { has_camera_access( $ARGV[1] || '/Applications/STF Coordinator.app' ); }
35 | elsif( $action eq 'addcontrol' ) { add_control( $ARGV[1], $ARGV[2] ); }
36 | elsif( $action eq 'delcontrol' ) { del_control( $ARGV[1], $ARGV[2] ); }
37 | elsif( $action eq 'usage' ) {
38 | my $w = "\033[97m";
39 | my $o = "\033[0m";
40 | print < [parameter(s) of action]
43 |
44 | Actions:
45 | ${w}getcamera$o
46 | Show all of the apps with access to the camera
47 |
48 | ${w}getcontrol$o
49 | Show which apps can control which other apps
50 |
51 | ${w}getall$o
52 | Show all entries in the access DB
53 |
54 | ${w}addcamera$o
55 | Give camera permission to the specified app
56 | Example: addcamera /Applications/Utilities/Terminal.app
57 |
58 | ${w}addcontrol$o
59 | Give app1 permission to control app2 using OSA / Applescript
60 |
61 | ${w}delcamera$o
62 | Remove camera permissions for app
63 |
64 | ${w}delcontrol$o
65 | Remove permissions of app1 to control app2
66 | DONE
67 | }
68 |
69 | sub get_access {
70 | my $where = shift;
71 |
72 | my $selectCols = "";
73 | for my $col ( @cols ) {
74 | if( $usehex{ $col } ) {
75 | $selectCols .= "hex($col) as $col,";
76 | }
77 | else {
78 | $selectCols .= "quote($col) as $col,";
79 | }
80 | }
81 | chop $selectCols;
82 |
83 | my $wheretext = '';
84 |
85 | if( $where ) {
86 | my @conds;
87 | for my $key ( keys %$where ) {
88 | my $val = $where->{ $key };
89 | push( @conds, "$key=$val" );
90 | }
91 | $wheretext = "where " . join( ',', @conds );
92 | }
93 |
94 | my $lines = `sqlite3 "$db" -line "select $selectCols from access $wheretext;"`;
95 | $lines .= "\n \n";
96 | my $row = {};
97 | my @rows;
98 | for my $line ( split( '\n', $lines ) ) {
99 | if( $line eq "" ) {
100 | push( @rows, $row );
101 | $row = {};
102 | next;
103 | }
104 | if( $line =~ m/([a-z()_]+) = (.+)$/ ) {
105 | my $name = $1;
106 | my $val = $2;
107 | if( $usehex{ $name } ) {
108 | my $raw = pack("H*", $val);
109 | $row->{ $name } = pipe_in_out( $raw, "csreq -r- -t" );
110 | }
111 | else {
112 | $row->{ $name } = $val;
113 | }
114 | }
115 | }
116 | return \@rows;
117 | }
118 |
119 | sub has_camera_access {
120 | my $app = shift;
121 | my $rows = get_access( { service => "'kTCCServiceCamera'" } );
122 | my $idents = get_app_idents();
123 | for my $row ( @$rows ) {
124 | if( $row->{allowed} ) {
125 | my $clientNoQuote = substr( $row->{client}, 1, -1 );
126 | if( $idents->{ $clientNoQuote } ) {
127 | my $full = $idents->{ $clientNoQuote };
128 | if( $full eq $app ) {
129 | print "yes\n";
130 | exit( 0 );
131 | }
132 | }
133 | }
134 | }
135 | print "no\n";
136 | exit( 1 );
137 | }
138 |
139 | sub print_all_camera_access {
140 | my $rows = get_access( { service => "'kTCCServiceCamera'" } );
141 | #print Dumper( $rows );
142 | my $idents = get_app_idents();
143 | for my $row ( @$rows ) {
144 | print_camera_access( $idents, $row );
145 | print "\n";
146 | }
147 | }
148 |
149 | sub print_all_control {
150 | my $rows = get_access( { service => "'kTCCServiceAppleEvents\'" } );
151 | print Dumper( $rows );
152 | my $idents = get_app_idents();
153 |
154 | for my $row ( @$rows ) {
155 | print_control( $idents, $row );
156 | print "\n";
157 | }
158 | }
159 |
160 | sub print_control {
161 | my ( $idents, $row ) = @_;
162 |
163 | my $allowed = $row->{allowed};
164 | my $csreq = $row->{csreq};
165 | my $client = $row->{client};
166 | my $indir = $row->{indirect_object_identifier};
167 | my $indir_ident = $row->{indirect_object_code_identity};
168 |
169 | if( $allowed ) {
170 | print "Controlling App = $client\nControlling App Identity = $csreq\n";
171 | my $clientNoQuote = substr( $client, 1, -1 );
172 | if( $idents->{ $clientNoQuote } ) {
173 | my $full = $idents->{ $clientNoQuote };
174 | print " Possible Match = $full\n";
175 | my $pIdent = app_to_ident( $full );
176 | if( $pIdent ) {
177 | print " Possible Match Identity = $pIdent\n";
178 | }
179 | }
180 |
181 | print "Controlled App = $indir\nControlled App Identity = $indir_ident\n";
182 | my $indirNoQuote = substr( $indir, 1, -1 );
183 | if( $idents->{ $indirNoQuote } ) {
184 | my $full = $idents->{ $indirNoQuote };
185 | print " Possible Match = $full\n";
186 | my $pIdent = app_to_ident( $full );
187 | if( $pIdent ) {
188 | print " Possible Match Identity = $pIdent\n";
189 | }
190 | }
191 | }
192 | }
193 |
194 | sub app_to_ident {
195 | my $app = shift;
196 | my @matchLines = `codesign -d -r- "$app" 2>/dev/null`;
197 | for my $line ( @matchLines ) {
198 | if( $line =~ m/designated => (.+)/ ) {
199 | return $1;
200 | }
201 | }
202 | return "";
203 | }
204 |
205 | sub print_all_access {
206 | my $rows = get_access( 0 );
207 | print Dumper( $rows );
208 |
209 | }
210 |
211 | sub print_camera_access {
212 | my ( $idents, $row ) = @_;
213 | my $allowed = $row->{allowed};
214 | my $csreq = $row->{csreq};
215 | my $client = $row->{client};
216 |
217 | if( $allowed ) {
218 | print "CS Req = $csreq\nClient = $client\n";
219 | my $clientNoQuote = substr( $client, 1, -1 );
220 | if( $idents->{ $clientNoQuote } ) {
221 | my $full = $idents->{ $clientNoQuote };
222 | print " Possible Match = $full\n";
223 | my $pIdent = app_to_ident( $full );
224 | if( $pIdent ) {
225 | print " Possible Match Identity = $pIdent\n";
226 | }
227 | }
228 | }
229 | }
230 |
231 | sub pipe_in_out {
232 | my ( $in, $cmd ) = @_;
233 |
234 | my $out = '';
235 | my $pid = open2( \*SUB_OUT, \*SUB_IN, $cmd );
236 |
237 | print SUB_IN "$in\cD";
238 |
239 | while( ) {
240 | $out .= $_;
241 | }
242 | chomp $out;
243 |
244 | waitpid( $pid, 0 );
245 |
246 | return $out;
247 | }
248 |
249 | sub get_app_idents {
250 | my %idents;
251 |
252 | opendir( my $dh, "/Applications" );
253 | my @files = readdir( $dh );
254 | closedir( $dh );
255 | for my $file ( @files ) {
256 | next if( $file =~ m/^\.+$/ );
257 | next if( $file !~ m/\.app$/ );
258 | my $full = "/Applications/$file";
259 | my $ident = get_app_bundle( $full );
260 | if( $ident ) {
261 | $idents{ $ident } = $full;
262 | }
263 | }
264 | return \%idents;
265 | }
266 |
267 | sub get_app_bundle {
268 | my $full = shift;
269 | my @lines = `plutil -extract CFBundleIdentifier xml1 "$full/Contents/Info.plist" -o -`;
270 | for my $line ( @lines ) {
271 | if( $line =~ m|(.+)| ) {
272 | return $1;
273 | }
274 | }
275 | return "";
276 | }
277 |
278 | sub add_camera_access {
279 | my $app = shift;
280 | my $appIdent = app_to_ident( $app );
281 | #print "Ident: $appIdent\n";
282 | my $hex = ident_to_csreq( $appIdent );
283 | #print "$hex\n";
284 |
285 | my $bundle = get_app_bundle( $app );
286 | #print "Bundle: $bundle\n";
287 |
288 | `sqlite3 "$db" "delete from access where service='kTCCServiceCamera' and client='$bundle'"`;
289 | sql_insert( 'access', {
290 | service => "'kTCCServiceCamera'",
291 | client => "'$bundle'",
292 | client_type => 0,
293 | allowed => 1,
294 | prompt_count => 1,
295 | csreq => "x'$hex'",
296 | policy_id => "'NULL'",
297 | indirect_object_identifier => "'UNUSED'",
298 | flags => 0
299 | } );
300 | }
301 |
302 | sub del_camera_access {
303 | my $app = shift;
304 | my $bundle = get_app_bundle( $app );
305 | `sqlite3 "$db" "delete from access where service='kTCCServiceCamera' and client='$bundle'"`;
306 | }
307 |
308 | sub ident_to_csreq {
309 | my $ident = shift;
310 |
311 | open( my $fh, "| csreq -r- -b ./test" );
312 | print $fh $ident;
313 | close( $fh );
314 | open( my $bh, "<./test" );
315 | binmode( $bh );
316 | my $data;
317 | {
318 | local $/ = undef;
319 | $data = <$bh>;
320 | }
321 | close( $bh );
322 | my $hex = uc( unpack("H*", $data) );
323 |
324 | return $hex;
325 | }
326 |
327 | sub add_control {
328 | my ( $app, $app2 ) = @_;
329 | my $appIdent = app_to_ident( $app );
330 | my $app2Ident = app_to_ident( $app2 );
331 | #print "Ident: $appIdent\n";
332 | my $hex = ident_to_csreq( $appIdent );
333 | my $hex2 = ident_to_csreq( $app2Ident );
334 | #print "$hex\n";
335 |
336 | my $bundle = get_app_bundle( $app );
337 | my $bundle2 = get_app_bundle( $app2 );
338 | #print "Bundle: $bundle\n";
339 |
340 | `sqlite3 "$db" "delete from access where service='kTCCServiceAppleEvents' and client='$bundle' and indirect_object_identifier='$bundle2'"`;
341 | sql_insert( 'access', {
342 | service => "'kTCCServiceAppleEvents'",
343 | client => "'$bundle'",
344 | client_type => 0,
345 | allowed => 1,
346 | prompt_count => 1,
347 | csreq => "x'$hex'",
348 | policy_id => "'NULL'",
349 | indirect_object_identifier => "'$bundle2'",
350 | indirect_object_code_identity => "x'$hex2'",
351 | flags => "NULL"
352 | } );
353 | }
354 |
355 | sub del_control {
356 | my ( $app, $app2 ) = @_;
357 | my $bundle = get_app_bundle( $app );
358 | my $bundle2 = get_app_bundle( $app2 );
359 |
360 | `sqlite3 "$db" "delete from access where service='kTCCServiceAppleEvents' and client='$bundle' and indirect_object_identifier='$bundle2'"`;
361 | }
362 |
363 | sub sql_insert {
364 | my ( $table, $vals ) = @_;
365 |
366 | my @keys = sort keys %$vals;
367 |
368 | my @valset;
369 | for my $key ( @keys ) {
370 | my $val = $vals->{ $key };
371 | push( @valset, $val );
372 | }
373 | my $keytext = join(',', @keys );
374 | my $valtext = join(',', @valset );
375 | `sqlite3 "$db" "insert into $table ($keytext) values($valtext)"`;
376 | }
--------------------------------------------------------------------------------
/version.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0"
3 | }
4 |
--------------------------------------------------------------------------------
/video_pipes/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/dryark/stf_ios_support/2f32cbbf1506a740eb21a496c1cdc11d875558f7/video_pipes/.gitkeep
--------------------------------------------------------------------------------
/view_log.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "bufio"
5 | "bytes"
6 | "flag"
7 | "fmt"
8 | "io"
9 | "io/ioutil"
10 | "encoding/json"
11 | "os"
12 | "strings"
13 | "github.com/fsnotify/fsnotify"
14 | log "github.com/sirupsen/logrus"
15 | )
16 |
17 | type Config struct {
18 | Log LogConfig
19 | //LogFile string `json:"log_file"`
20 | //LinesLogFile string `json:"lines_log_file"`
21 | }
22 |
23 | type LogConfig struct {
24 | Main string `json:"main"`
25 | ProcLines string `json:"proc_lines"`
26 | WDAWrapperStdout string `json:"wda_wrapper_stdout"`
27 | WDAWrapperStderr string `json:"wda_wrapper_stderr"`
28 | }
29 |
30 | func main() {
31 | var configFile = flag.String( "config", "config.json", "Config file path" )
32 | var findProc = flag.String( "proc" , "" , "Process to view log of" )
33 | flag.Parse()
34 |
35 | config := read_config( *configFile )
36 |
37 | fileName := config.Log.ProcLines
38 |
39 | if *findProc == "" {
40 | fmt.Println("specify a log to view / tail ( view_log -proc [proc] ):\n wdaproxy\n stf_device_ios\n device_trigger\n video_enabler\n stf_provider\n ffmpeg\n wda\n device_trigger\n")
41 | os.Exit( 0 )
42 | }
43 |
44 | if *findProc == "wda" {
45 | fileName = "bin/wda/req_log.json"
46 | }
47 |
48 | watcher, err := fsnotify.NewWatcher()
49 | if err != nil {
50 | log.Fatal(err)
51 | }
52 |
53 | fh, err := os.Open( fileName )
54 | if err != nil {
55 | panic(err)
56 | }
57 | defer fh.Close()
58 |
59 | size := fileSize( fh )
60 | //fh.Seek( size, io.SeekStart )
61 |
62 | scanner := bufio.NewScanner( fh )
63 | for scanner.Scan() {
64 | checkLine( []byte( scanner.Text() ), *findProc )
65 | }
66 |
67 | err = watcher.Add( fileName )
68 | if err != nil {
69 | log.Fatal(err)
70 | }
71 | for {
72 | select {
73 | case event := <-watcher.Events:
74 | if event.Op & fsnotify.Write == fsnotify.Write {
75 | //fmt.Println("modify")
76 | newSize := fileSize( fh )
77 |
78 | newBytes := newSize - size
79 |
80 | if newBytes > 0 {
81 | //fmt.Printf(" dif: %d\n", newBytes )
82 |
83 | //f.Seek(pos, io.SeekStart)
84 | buf := make( []byte, newBytes )
85 | fh.Read( buf )
86 | //fmt.Printf(" \"%s\"\n", string( buf ) )
87 |
88 | checkLine( buf, *findProc )
89 |
90 | size = newSize
91 | }
92 | }
93 | }
94 | }
95 | }
96 |
97 | func read_config( configPath string ) *Config {
98 | fh, serr := os.Stat( configPath )
99 | if serr != nil {
100 | log.WithFields( log.Fields{
101 | "type": "err_read_config",
102 | "error": serr,
103 | "config_path": configPath,
104 | } ).Fatal("Could not read specified config path")
105 | }
106 | var configFile string
107 | switch mode := fh.Mode(); {
108 | case mode.IsDir():
109 | configFile = fmt.Sprintf("%s/config.json", configPath)
110 | case mode.IsRegular():
111 | configFile = configPath
112 | }
113 |
114 | configFh, err := os.Open( configFile )
115 | if err != nil {
116 | log.WithFields( log.Fields{
117 | "type": "err_read_config",
118 | "config_file": configFile,
119 | "error": err,
120 | } ).Fatal("failed reading config file")
121 | }
122 | defer configFh.Close()
123 |
124 | jsonBytes, _ := ioutil.ReadAll( configFh )
125 | config := Config{}
126 |
127 | defaultJson := `{
128 | "log":{
129 | "main": "logs/coordinator",
130 | "proc_lines": "logs/procs",
131 | "wda_wrapper_stdout": "./logs/wda_wrapper_stdout",
132 | "wda_wrapper_stderr": "./logs/wda_wrapper_stderr"
133 | }
134 | }`
135 |
136 | err = json.Unmarshal( []byte( defaultJson ), &config )
137 | if err != nil {
138 | log.Fatal( "1 ", err )
139 | }
140 |
141 | json.Unmarshal( jsonBytes, &config )
142 | return &config
143 | }
144 |
145 | func checkLine( data []byte, findProc string ) {
146 | var dat map[string]interface{}
147 |
148 | startJ := strings.Index( string(data), "{" )
149 | endJ := strings.LastIndex( string(data), "}" )
150 |
151 | part := string(data)[ startJ : (endJ + 1) ]
152 |
153 | decoder := json.NewDecoder( strings.NewReader( part ) )
154 | for {
155 | err := decoder.Decode( &dat )
156 | if err == io.EOF {
157 | break
158 | }
159 | if err != nil {
160 | panic(err)
161 | }
162 |
163 | if findProc == "wda" {
164 | //fmt.Println( part )
165 | typeif := dat["type"]
166 | if typeif != nil {
167 | typ := typeif.(string)
168 | //fmt.Println( typ )
169 | if typ == "req.start" {
170 | if dat["body_in"] != nil {
171 | inStr := dat["body_in"].(string)
172 |
173 | fmt.Printf("Req URI: %s\n", dat["uri"].(string) )
174 | if inStr[:1] == "{" {
175 | var prettyJSON bytes.Buffer
176 | error := json.Indent(&prettyJSON, []byte( inStr ), "", " ")
177 | if error != nil {
178 | fmt.Println( inStr )
179 | } else {
180 | fmt.Println( prettyJSON.String() )
181 | }
182 |
183 | //dec2 :=- json.NewDecoder( strings.NewReader( dat["body_in"].(string) ) )
184 | //err = dec2.Decode( &dat )
185 | } else {
186 | fmt.Println( inStr )
187 | }
188 | }
189 | } else if typ == "req.done" {
190 | if dat["body_out"] != nil {
191 | outStr := dat["body_out"].(string)
192 | fmt.Printf("Response to URI: %s\n", dat["uri"].(string) )
193 |
194 | fmt.Println( outStr )
195 | }
196 | }
197 | }
198 | } else {
199 | proc := dat["proc"].(string)
200 | if proc == findProc {
201 | //fmt.Println(dat)
202 | line := dat["line"].(string)
203 | fmt.Println( line )
204 | }
205 | }
206 | }
207 | }
208 |
209 | func fileSize( fh *os.File ) (int64) {
210 | newinfo, err := fh.Stat()
211 | if err != nil {
212 | panic(err)
213 | }
214 | return newinfo.Size()
215 | }
--------------------------------------------------------------------------------
/wda_wrapper/Makefile:
--------------------------------------------------------------------------------
1 | TARGET = ../bin/wda_wrapper
2 |
3 | all: $(TARGET)
4 |
5 | $(TARGET): wda_wrapper.go go.sum
6 | go build -o $(TARGET) .
7 |
8 | go.sum:
9 | go get
10 | go get .
11 |
12 | clean:
13 | $(RM) $(TARGET) go.sum
--------------------------------------------------------------------------------
/wda_wrapper/go.mod:
--------------------------------------------------------------------------------
1 | module wda_wrapper
2 |
3 | go 1.12
4 |
5 | require (
6 | github.com/go-cmd/cmd v1.2.0
7 | github.com/jviney/go-proc v0.2.0
8 | github.com/sirupsen/logrus v1.4.2
9 | github.com/zeromq/goczmq v4.1.0+incompatible
10 | )
11 |
--------------------------------------------------------------------------------
/wda_wrapper/wda_wrapper.go:
--------------------------------------------------------------------------------
1 | package main
2 |
3 | import (
4 | "flag"
5 | "fmt"
6 | "encoding/json"
7 | "os"
8 | "os/exec"
9 | "os/signal"
10 | "strconv"
11 | "strings"
12 | "syscall"
13 | "time"
14 | zmq "github.com/zeromq/goczmq"
15 | log "github.com/sirupsen/logrus"
16 | gocmd "github.com/go-cmd/cmd"
17 | )
18 |
19 | var exit bool
20 | var reqSock *zmq.Sock
21 | var reqOb *zmq.ReadWriter
22 |
23 | func main() {
24 | exit = false
25 |
26 | var wdaPort = flag.Int( "port", 8100, "WDA Port" )
27 | var uuid = flag.String( "uuid", "", "IOS Device UUID" )
28 | var iosVersion = flag.String( "iosVersion", "", "IOS Version" )
29 | var wdaRoot = flag.String( "wdaRoot", "", "WDA Folder Path" )
30 | flag.Parse()
31 |
32 | coro_sigterm()
33 | setup_zmq()
34 | proc_wdaproxy( *wdaPort, *uuid, *iosVersion, *wdaRoot )
35 | }
36 |
37 |
38 | func proc_wdaproxy(
39 | wdaPort int,
40 | uuid string,
41 | iosVersion string,
42 | wdaRoot string ) {
43 |
44 | log.WithFields( log.Fields{
45 | "type": "proc_start",
46 | "proc": "wda_wrapper",
47 | "wdaPort": wdaPort,
48 | "uuid": uuid,
49 | "iosVersion": iosVersion,
50 | "wdaRoot": wdaRoot,
51 | } ).Info("wda wrapper started")
52 |
53 | backoff := Backoff{}
54 |
55 | stdoutChan := make(chan string, 100)
56 | stderrChan := make(chan string, 100)
57 |
58 | go func() {
59 | for {
60 | line := <- stdoutChan
61 |
62 | if strings.Contains( line, "is implemented in both" ) {
63 | } else if strings.Contains( line, "Couldn't write value" ) {
64 | } else if strings.Contains( line, "GET /status " ) {
65 | } else if strings.Contains( line, "] Error" ) {
66 | msgCoord( map[string]string{
67 | "type": "wda_error",
68 | "line": line,
69 | "uuid": uuid,
70 | } )
71 | } else {
72 | log.WithFields( log.Fields{
73 | "type": "proc_stdout",
74 | "line": line,
75 | } ).Info("")
76 | msgCoord( map[string]string{
77 | "type": "wda_stdout",
78 | "line": line,
79 | "uuid": uuid,
80 | } )
81 | // send line to linelog ( through zmq )
82 | }
83 |
84 | if exit { break }
85 | }
86 | } ()
87 |
88 | go func() {
89 | for {
90 | line := <- stderrChan
91 |
92 | if strings.Contains( line, "[WDA] successfully started" ) {
93 | // send message that WDA has started to coordinator
94 | msgCoord( map[string]string{
95 | "type": "wda_started",
96 | "uuid": uuid,
97 | } )
98 | }
99 |
100 | // send line to coordinator
101 | log.WithFields( log.Fields{
102 | "type": "proc_stderr",
103 | "line": line,
104 | } ).Error("")
105 | msgCoord( map[string]string{
106 | "type": "wda_stderr",
107 | "line": line,
108 | "uuid": uuid,
109 | } )
110 |
111 | if exit { break }
112 | }
113 | } ()
114 |
115 | for {
116 | ops := []string{
117 | "-p", strconv.Itoa( wdaPort ),
118 | "-q", strconv.Itoa( wdaPort ),
119 | "-d",
120 | "-W", ".",
121 | "-u", uuid,
122 | fmt.Sprintf("--iosversion=%s", iosVersion),
123 | }
124 |
125 | log.WithFields( log.Fields{
126 | "type": "proc_cmdline",
127 | "cmd": "../wdaproxy",
128 | "args": ops,
129 | } ).Info("")
130 |
131 | cmd := exec.Command( "../wdaproxy", ops... )
132 |
133 | cmd.Dir = wdaRoot
134 |
135 | stdStream := gocmd.NewOutputStream(stdoutChan)
136 | cmd.Stdout = stdStream
137 |
138 | errStream := gocmd.NewOutputStream(stderrChan)
139 | cmd.Stderr = errStream
140 |
141 | backoff.markStart()
142 | err := cmd.Start()
143 | if err != nil {
144 | log.WithFields( log.Fields{
145 | "type": "proc_err",
146 | "error": err,
147 | } ).Error("Error starting wdaproxy")
148 | backoff.markEnd()
149 | backoff.wait()
150 | continue
151 | }
152 |
153 | // send message that it has started
154 | msgCoord( map[string]string{
155 | "type": "wdaproxy_started",
156 | "uuid": uuid,
157 | } )
158 |
159 | cmd.Wait()
160 |
161 | backoff.markEnd()
162 |
163 | // send message that it has ended
164 | log.WithFields( log.Fields{
165 | "type": "proc_end",
166 | } ).Info("Wdaproxy ended")
167 | msgCoord( map[string]string{
168 | "type": "wdaproxy_ended",
169 | "uuid": uuid,
170 | } )
171 |
172 | if exit { break }
173 |
174 | backoff.wait()
175 | }
176 |
177 | close_zmq()
178 | }
179 |
180 | type Backoff struct {
181 | fails int
182 | start time.Time
183 | elapsedSeconds float64
184 | }
185 |
186 | func ( self *Backoff ) markStart() {
187 | self.start = time.Now()
188 | }
189 |
190 | func ( self *Backoff ) markEnd() ( float64 ) {
191 | elapsed := time.Since( self.start )
192 | seconds := elapsed.Seconds()
193 | self.elapsedSeconds = seconds
194 | return seconds
195 | }
196 |
197 | func ( self *Backoff ) wait() {
198 | sleeps := []int{ 0, 0, 2, 5, 10 }
199 | numSleeps := len( sleeps )
200 | if self.elapsedSeconds < 20 {
201 | self.fails = self.fails + 1
202 | index := self.fails
203 | if index >= numSleeps {
204 | index = numSleeps - 1
205 | }
206 | sleepLen := sleeps[ index ]
207 | if sleepLen != 0 {
208 | time.Sleep( time.Second * time.Duration( sleepLen ) )
209 | }
210 | } else {
211 | self.fails = 0
212 | }
213 | }
214 |
215 | func setup_zmq() {
216 | reqSock = zmq.NewSock(zmq.Push)
217 |
218 | spec := "tcp://127.0.0.1:7300"
219 | reqSock.Connect( spec )
220 |
221 | var err error
222 | reqOb, err = zmq.NewReadWriter(reqSock)
223 | if err != nil {
224 | log.WithFields( log.Fields{
225 | "type": "zmq_connect_err",
226 | "err": err,
227 | } ).Error("ZMQ Send Error")
228 | }
229 |
230 | reqOb.SetTimeout(1000)
231 |
232 | zmqRequest( []byte("dummy") )
233 | }
234 |
235 | func close_zmq() {
236 | reqSock.Destroy()
237 | reqOb.Destroy()
238 | }
239 |
240 | func msgCoord( content map[string]string ) {
241 | data, _ := json.Marshal( content )
242 | zmqRequest( data )
243 | }
244 |
245 | func zmqRequest( jsonOut []byte ) {
246 | err := reqSock.SendMessage( [][]byte{ jsonOut } )
247 | if err != nil {
248 | log.WithFields( log.Fields{
249 | "type": "zmq_send_err",
250 | "err": err,
251 | } ).Error("ZMQ Send Error")
252 | }
253 | }
254 |
255 | func coro_sigterm() {
256 | c := make(chan os.Signal, 2)
257 | signal.Notify(c, os.Interrupt, syscall.SIGTERM)
258 | go func() {
259 | <- c
260 | exit = true
261 |
262 | time.Sleep( time.Millisecond * 1000 )
263 |
264 | os.Exit(0)
265 | }()
266 | }
--------------------------------------------------------------------------------