From 9f127e3f67b3238a69c2ddfe8d68b62e8690adbc Mon Sep 17 00:00:00 2001 From: Nick Abalov Date: Fri, 11 May 2018 17:25:42 +0100 Subject: [PATCH 001/131] Initial commit --- LICENSE | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..4e1ee57f --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2018 Badoo Tech + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. From 9f212029d8e6c2a8263f2d0b4f9b7299f94eed7e Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Wed, 23 May 2018 14:02:56 +0100 Subject: [PATCH 002/131] Initial push Co-authored-by: Vyacheslav Frolov Co-authored-by: Tim Baverstock --- .gitignore | 33 + README.md | 65 ++ bootstrap.sh | 17 + client/rb/.rubocop.yml | 19 + client/rb/Gemfile | 3 + client/rb/Gemfile.lock | 19 + .../ios-device-server-client/device_client.rb | 158 +++++ .../ios-device-server-client/device_info.rb | 21 + .../device_provider.rb | 227 +++++++ .../ios-device-server-client/remote_device.rb | 238 +++++++ device-server/build.gradle | 154 +++++ device-server/config/multihost-sample.json | 17 + device-server/config/physical-sample.json | 22 + device-server/docker/build.sh | 17 + .../gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54333 bytes .../gradle/wrapper/gradle-wrapper.properties | 6 + device-server/gradlew | 172 ++++++ device-server/gradlew.bat | 84 +++ device-server/jar_launcher.sh | 23 + device-server/run_device_server.sh | 13 + device-server/scripts/build_fbsimctl.sh | 66 ++ device-server/scripts/update_wda.sh | 91 +++ device-server/settings.gradle | 2 + .../java/deviceserver/LogHighlighter.java | 31 + .../AuthenticationPipelineExtensions.kt | 57 ++ .../automation/deviceserver/Configuration.kt | 18 + .../automation/deviceserver/DeviceServer.kt | 225 +++++++ .../deviceserver/DeviceServerConfig.kt | 9 + .../automation/deviceserver/JsonMapper.kt | 35 ++ .../automation/deviceserver/LogMarkers.kt | 16 + .../automation/deviceserver/NodeConfig.kt | 38 ++ .../badoo/automation/deviceserver/Program.kt | 9 + .../deviceserver/WaitTimeoutError.kt | 3 + .../deviceserver/command/ChildProcess.kt | 53 ++ .../deviceserver/command/CommandResult.kt | 11 + .../deviceserver/command/IShellCommand.kt | 33 + .../command/IShellCommandListener.kt | 10 + .../command/RemoteShellCommand.kt | 105 ++++ .../deviceserver/command/ShellCommand.kt | 73 +++ .../command/ShellCommandException.kt | 3 + .../command/ShellCommandListener.kt | 60 ++ .../deviceserver/command/ShellUtils.kt | 24 + .../command/SshConnectionException.kt | 3 + .../controllers/DevicesController.kt | 89 +++ .../controllers/StatusController.kt | 27 + .../automation/deviceserver/data/CrashLog.kt | 3 + .../deviceserver/data/DesiredCapabilities.kt | 10 + .../deviceserver/data/DeviceAllocatedPorts.kt | 12 + .../automation/deviceserver/data/DeviceDTO.kt | 20 + .../deviceserver/data/DeviceInfo.kt | 17 + .../automation/deviceserver/data/DeviceRef.kt | 3 + .../deviceserver/data/DeviceState.kt | 14 + .../automation/deviceserver/data/ErrorDto.kt | 19 + .../deviceserver/data/SimulatorStatus.kt | 9 + .../deviceserver/data/SimulatorStatusDTO.kt | 11 + .../host/DefaultSimulatorHostProvider.kt | 9 + .../deviceserver/host/DevicesNode.kt | 300 +++++++++ .../deviceserver/host/HostFactory.kt | 68 ++ .../automation/deviceserver/host/IRemote.kt | 48 ++ .../deviceserver/host/ISimulatorFactory.kt | 25 + .../host/ISimulatorHostProvider.kt | 12 + .../deviceserver/host/ISimulatorProvider.kt | 11 + .../deviceserver/host/ISimulatorsNode.kt | 33 + .../automation/deviceserver/host/Remote.kt | 131 ++++ .../deviceserver/host/SimulatorProvider.kt | 44 ++ .../deviceserver/host/SimulatorsNode.kt | 220 +++++++ .../management/DesiredCapabilitiesMatcher.kt | 42 ++ .../host/management/DeviceManager.kt | 134 ++++ .../host/management/IAutoreleaseLooper.kt | 5 + .../host/management/IDeviceManager.kt | 26 + .../host/management/IHostFactory.kt | 8 + .../host/management/ISimulatorHostChecker.kt | 124 ++++ .../host/management/NodeRegistrar.kt | 65 ++ .../host/management/NodeRegistry.kt | 87 +++ .../host/management/NodeWrapper.kt | 113 ++++ .../host/management/PortAllocator.kt | 43 ++ .../host/management/RuntimeVersion.kt | 16 + .../errors/DeviceNotFoundException.kt | 3 + .../errors/NoAliveNodesException.kt | 3 + .../errors/OverCapacityException.kt | 3 + .../host/management/util/AutoreleaseLooper.kt | 56 ++ .../deviceserver/ios/ActiveDevices.kt | 131 ++++ .../deviceserver/ios/DeviceStatus.kt | 9 + .../deviceserver/ios/IActiveDevices.kt | 21 + .../automation/deviceserver/ios/WdaClient.kt | 113 ++++ .../deviceserver/ios/device/Device.kt | 338 ++++++++++ .../ios/device/DeviceFbsimctlProc.kt | 36 ++ .../ios/device/DeviceInfoProvider.kt | 10 + .../deviceserver/ios/device/DeviceSlot.kt | 19 + .../deviceserver/ios/device/DeviceSlots.kt | 175 ++++++ .../ios/device/DeviceWebDriverAgent.kt | 26 + .../deviceserver/ios/device/KnownDevice.kt | 14 + .../deviceserver/ios/fbsimctl/FBSimctl.kt | 156 +++++ .../deviceserver/ios/fbsimctl/FBSimctlDTO.kt | 37 ++ .../ios/fbsimctl/FBSimctlErrors.kt | 5 + .../ios/fbsimctl/FBSimctlResponseParser.kt | 138 +++++ .../deviceserver/ios/fbsimctl/IFBSimctl.kt | 35 ++ .../ios/fbsimctl/IFBSimctlResponseParser.kt | 10 + .../deviceserver/ios/proc/FbsimctlProc.kt | 81 +++ .../ios/proc/FbsimctlProcError.kt | 5 + .../deviceserver/ios/proc/ILongRunningProc.kt | 11 + .../deviceserver/ios/proc/LongRunningProc.kt | 40 ++ .../ios/proc/LongRunningProcessListener.kt | 94 +++ .../ios/proc/SimulatorWebDriverAgent.kt | 19 + .../deviceserver/ios/proc/WebDriverAgent.kt | 76 +++ .../ios/proc/WebDriverAgentError.kt | 5 + .../deviceserver/ios/simulator/ISimulator.kt | 30 + .../deviceserver/ios/simulator/Simulator.kt | 581 ++++++++++++++++++ .../ios/simulator/SimulatorError.kt | 3 + .../ios/simulator/SimulatorThreads.kt | 9 + .../ios/simulator/backup/ISimulatorBackup.kt | 8 + .../ios/simulator/backup/SimulatorBackup.kt | 128 ++++ .../simulator/backup/SimulatorBackupError.kt | 3 + .../simulator/video/SimulatorVideoRecorder.kt | 162 +++++ .../video/SimulatorVideoRecordingException.kt | 3 + .../deviceserver/util/CustomHttpClient.kt | 46 ++ .../automation/deviceserver/util/HttpCodes.kt | 10 + .../deviceserver/util/HttpResult.kt | 9 + .../automation/deviceserver/util/Support.kt | 77 +++ .../src/main/resources/application.conf | 15 + .../src/main/resources/logback-test.xml | 19 + device-server/src/main/resources/logback.xml | 19 + .../deviceserver/DeviceServerConfigTest.kt | 23 + .../automation/deviceserver/NodeConfigTest.kt | 47 ++ .../badoo/automation/deviceserver/TestUtil.kt | 38 ++ .../command/RemoteShellCommandTest.kt | 129 ++++ .../deviceserver/command/ShellCommandTest.kt | 58 ++ .../deviceserver/command/ShellUtilsTest.kt | 12 + .../controllers/DevicesControllerTest.kt | 213 +++++++ .../controllers/StatusControllerTest.kt | 20 + .../data/DesiredCapabilitiesTest.kt | 63 ++ .../deviceserver/data/ErrorDtoKtTest.kt | 26 + .../deviceserver/host/NodeRegistryTest.kt | 88 +++ .../deviceserver/host/NodeWrapperTest.kt | 90 +++ .../deviceserver/host/RemoteTest.kt | 40 ++ .../host/SimulatorProviderTest.kt | 105 ++++ .../deviceserver/host/SimulatorsNodeTest.kt | 411 +++++++++++++ .../DesiredCapabilitiesMatcherTest.kt | 43 ++ .../deviceserver/ios/DeviceManagerTest.kt | 198 ++++++ .../deviceserver/ios/HostFactoryTest.kt | 62 ++ .../deviceserver/ios/SessionTest.kt | 99 +++ .../deviceserver/ios/fbsimctl/FBSimctlTest.kt | 36 ++ .../fbsimctl/FbSimctlResponseParserTest.kt | 109 ++++ .../ios/proc/DeviceFbsimctlProcTest.kt | 66 ++ .../deviceserver/ios/proc/FbsimctlProcTest.kt | 77 +++ .../ios/simulator/SimulatorBackupTest.kt | 92 +++ .../video/SimulatorVideoRecorderTest.kt | 59 ++ .../deviceserver/util/CustomHttpClientTest.kt | 20 + .../deviceserver/util/SupportKtTest.kt | 43 ++ .../org.mockito.plugins.MockMaker | 1 + 150 files changed, 9140 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100755 bootstrap.sh create mode 100644 client/rb/.rubocop.yml create mode 100644 client/rb/Gemfile create mode 100644 client/rb/Gemfile.lock create mode 100644 client/rb/lib/ios-device-server-client/device_client.rb create mode 100644 client/rb/lib/ios-device-server-client/device_info.rb create mode 100644 client/rb/lib/ios-device-server-client/device_provider.rb create mode 100644 client/rb/lib/ios-device-server-client/remote_device.rb create mode 100644 device-server/build.gradle create mode 100644 device-server/config/multihost-sample.json create mode 100644 device-server/config/physical-sample.json create mode 100755 device-server/docker/build.sh create mode 100644 device-server/gradle/wrapper/gradle-wrapper.jar create mode 100644 device-server/gradle/wrapper/gradle-wrapper.properties create mode 100755 device-server/gradlew create mode 100644 device-server/gradlew.bat create mode 100755 device-server/jar_launcher.sh create mode 100755 device-server/run_device_server.sh create mode 100755 device-server/scripts/build_fbsimctl.sh create mode 100755 device-server/scripts/update_wda.sh create mode 100644 device-server/settings.gradle create mode 100644 device-server/src/main/java/deviceserver/LogHighlighter.java create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/AuthenticationPipelineExtensions.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/Configuration.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServerConfig.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/JsonMapper.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/LogMarkers.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/NodeConfig.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/Program.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/WaitTimeoutError.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ChildProcess.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/CommandResult.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/IShellCommand.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/IShellCommandListener.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommand.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommandException.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommandListener.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellUtils.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/SshConnectionException.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/StatusController.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/CrashLog.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DesiredCapabilities.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceAllocatedPorts.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceDTO.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceInfo.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceRef.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceState.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/ErrorDto.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SimulatorStatus.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SimulatorStatusDTO.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DefaultSimulatorHostProvider.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorFactory.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorHostProvider.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorProvider.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorProvider.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DesiredCapabilitiesMatcher.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IAutoreleaseLooper.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IHostFactory.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeWrapper.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/PortAllocator.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/RuntimeVersion.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/errors/DeviceNotFoundException.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/errors/NoAliveNodesException.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/errors/OverCapacityException.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/util/AutoreleaseLooper.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/DeviceStatus.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/IActiveDevices.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/WdaClient.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceFbsimctlProc.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceInfoProvider.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlot.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlots.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceWebDriverAgent.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/KnownDevice.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlDTO.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlErrors.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlResponseParser.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctl.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctlResponseParser.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProcError.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/ILongRunningProc.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProc.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProcessListener.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgentError.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorError.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorThreads.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/backup/ISimulatorBackup.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/backup/SimulatorBackup.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/backup/SimulatorBackupError.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecordingException.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClient.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/HttpCodes.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/HttpResult.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/Support.kt create mode 100644 device-server/src/main/resources/application.conf create mode 100644 device-server/src/main/resources/logback-test.xml create mode 100644 device-server/src/main/resources/logback.xml create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/DeviceServerConfigTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/NodeConfigTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/TestUtil.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommandTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ShellCommandTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ShellUtilsTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/DevicesControllerTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/StatusControllerTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/DesiredCapabilitiesTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/ErrorDtoKtTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeRegistryTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeWrapperTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/RemoteTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorProviderTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/management/DesiredCapabilitiesMatcherTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/DeviceManagerTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/HostFactoryTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/SessionTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FbSimctlResponseParserTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/DeviceFbsimctlProcTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProcTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorBackupTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorderTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClientTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/util/SupportKtTest.kt create mode 100644 device-server/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..4ce626da --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +.DS_Store +.idea/shelf +/android.tests.dependencies +/confluence/target +/dependencies +/dist +/local +/gh-pages +/ideaSDK +/clionSDK +/android-studio/sdk +out/ +/tmp +workspace.xml +*.versionsBackup +/idea/testData/debugger/tinyApp/classes* +/jps-plugin/testData/kannotator +/ultimate/dependencies +/ultimate/ideaSDK +/ultimate/out +/ultimate/tmp +/js/js.translator/testData/out/ +/js/js.translator/testData/out-min/ +.gradle/ +build/ +!**/src/**/build +!**/test/**/build +*.iml +!**/testData/**/*.iml +.idea/ + +# built WebDriverAgent files +ios/facebook diff --git a/README.md b/README.md new file mode 100644 index 00000000..cfae55bd --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# iOS Remote Device Server + +A server for managing, booting, and controlling simulators and devices on remote host machines. + +## Features +- Enables control of simulators and devices connected to remote host machines +- Enables tests to run using remote simulators and devices +- Enables custom actions on simulators like clearing safari cookies or fast reset of a simulator +- Hides away satisfying desired capabilities, i.e. based on requested model or os will choose an appropriate host to create a simulator + +## Requirements +### Java +* [Download](http://google.com/#q=download+java+se) and install Java SDK 10 +* set environment variable JAVA_HOME +```bash +export JAVA_HOME=$(/usr/libexec/java_home -v 10 -F) +``` + +## Usage + +Build and run Device Server +```bash +./bootstrap.sh +cd device-server +./run_device_server.sh +``` + +Allocate Device +```bash +curl -X POST -d '{"model":"iPhone 6", "headless":false}' http://localhost:4567/devices +``` + +Query Device Server +```bash +curl http://localhost:4567/status +curl http://localhost:4567/devices +``` + +Release device by reference +```bash +curl -X DELETE http://localhost:4567/devices/${DEVICE_REF} +``` + +Ruby sample +```ruby +require 'ios-device-server-client/remote_device' + +server_url = 'http://localhost:4567' + +provider = IosDeviceServerClient::DeviceProvider.new(server_url) + +rv = provider.create(model: 'iPhone 6', os: 'iOS 11.0', headless: false) +remote_device = IosDeviceServerClient::RemoteDevice.new(server_url, rv['ref']) + +begin + remote_device.await(timeout: 30) + remote_device.open_url('https://github.com/badoo/ios-device-server') + readline +ensure + remote_device.release +end +``` + + +- [ ] command line utility will be published soon diff --git a/bootstrap.sh b/bootstrap.sh new file mode 100755 index 00000000..97a857a3 --- /dev/null +++ b/bootstrap.sh @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -e +set -u + +echo "Building fbsimctl..." +./device-server/scripts/build_fbsimctl.sh + +echo "Building WebDriverAgent..." + +# Building WebDriverAgent for devices is skipped by default, because it requires signing patch to set developer team and bundle id. +export NO_DEVICE_BUILD=${NO_DEVICE_BUILD:-1} + +./device-server/scripts/update_wda.sh + +echo "Checking Java Version" +/usr/libexec/java_home -v 10 -F diff --git a/client/rb/.rubocop.yml b/client/rb/.rubocop.yml new file mode 100644 index 00000000..5bf258b5 --- /dev/null +++ b/client/rb/.rubocop.yml @@ -0,0 +1,19 @@ +Style/Documentation: + Enabled: false + +Metrics/LineLength: + Max: 120 + +Style/GuardClause: + Enabled: false + +Metrics/ClassLength: + CountComments: false + Max: 200 + +Metrics/MethodLength: + CountComments: false + Max: 20 + +Metrics/AbcSize: + Max: 20 \ No newline at end of file diff --git a/client/rb/Gemfile b/client/rb/Gemfile new file mode 100644 index 00000000..97c6a7fa --- /dev/null +++ b/client/rb/Gemfile @@ -0,0 +1,3 @@ +source 'https://rubygems.org' + +gem 'selenium-webdriver', '3.5.0' diff --git a/client/rb/Gemfile.lock b/client/rb/Gemfile.lock new file mode 100644 index 00000000..9922595d --- /dev/null +++ b/client/rb/Gemfile.lock @@ -0,0 +1,19 @@ +GEM + remote: https://rubygems.org/ + specs: + childprocess (0.8.0) + ffi (~> 1.0, >= 1.0.11) + ffi (1.9.18) + rubyzip (1.2.1) + selenium-webdriver (3.5.0) + childprocess (~> 0.5) + rubyzip (~> 1.0) + +PLATFORMS + ruby + +DEPENDENCIES + selenium-webdriver (= 3.5.0) + +BUNDLED WITH + 1.16.1 diff --git a/client/rb/lib/ios-device-server-client/device_client.rb b/client/rb/lib/ios-device-server-client/device_client.rb new file mode 100644 index 00000000..2e6f4e9a --- /dev/null +++ b/client/rb/lib/ios-device-server-client/device_client.rb @@ -0,0 +1,158 @@ +require 'base64' +require 'json' +require 'net/http' +require 'selenium-webdriver' +require 'uri' + +require_relative 'device_info' + +module IosDeviceServerClient + class FbsimctlClientError < RuntimeError + end + + class DeviceClient + module ScreenshotType + PNG = 1 + JPEG = 2 + end + + attr_reader :user_ports + + # @return [DeviceClient] + def self.from_hash(device) + fb_endpoint = device['fbsimctl_endpoint'] + wda_endpoint = device['wda_endpoint'] + user_ports = device['user_ports'] + DeviceClient.new(fb_endpoint, wda_endpoint, user_ports) + end + + def initialize(fb_endpoint, wda_endpoint, user_ports) + uri = fb_endpoint + uri += '/' unless uri.end_with?('/') + @client = FbHttpServerClient.new(uri) + @wda_endpoint = wda_endpoint + @user_ports = user_ports + end + + def create_driver(caps) + client = Selenium::WebDriver::Remote::Http::Default.new + client.open_timeout = 15 # seconds + client.read_timeout = 120 # seconds + Selenium::WebDriver.for(:remote, url: @wda_endpoint, desired_capabilities: caps, http_client: client) + end + + def screenshot(file_type = ScreenshotType::PNG) + path = case file_type + when ScreenshotType::PNG + 'screenshot.png' + when ScreenshotType::JPEG + 'screenshot.jpeg' + else + raise(ArgumentError, "Unknown file type #{file_type}, should be one of #{ScreenshotType.constants}") + end + @client.get_binary(path) + end + + def open_url(url) + res = @client.post('open', url: url) + raise_for_status(res) + end + + def install_app(app_bundle_path) + raise("App bundle #{app_bundle_path} is not a file. Please provide path to a zipped app bundle") unless File.file?(app_bundle_path) + length = File.size(app_bundle_path) + res = File.open(app_bundle_path, 'rb') do |stream| + @client.post_binary('install', stream, length) + end + raise_for_status(res) + end + + def list_apps + res = @client.get('list_apps') + raise_for_status(res) + res + end + + def app_installed?(bundle_id) + rv = list_apps + app = rv['events'][0]['subject'].find { |x| x['bundle']['bundle_id'] == bundle_id } + # FIXME: check for data container does not work on real devices, do we still need it for sims to ensure app completed installation? + # && app['data_container'] + !app.nil? + end + + def uninstall_app(bundle_id) + res = @client.post('uninstall', bundle_id: bundle_id) + raise_for_status(res) + end + + def launch_app(bundle_id) + res = @client.post('launch', bundle_id: bundle_id) + raise_for_status(res) + end + + def relaunch_app(bundle_id) + res = @client.post('relaunch', bundle_id: bundle_id) + raise_for_status(res) + end + + def terminate_app(bundle_id) + res = @client.post('terminate', bundle_id: bundle_id) + raise_for_status(res) + end + + def set_location(lat:, lon:) + res = @client.post('set_location', latitude: lat, longitude: lon) + raise_for_status(res) + end + + private + + def raise_for_status(res) + msg = JSON.pretty_unparse(res) rescue res # rubocop:disable Style/RescueModifier + raise(FbsimctlClientError, "fbsimctl request failed #{msg}") unless res.is_a?(Hash) && res['status'] == 'success' + end + end + + class FbHttpServerClient + def initialize(endpoint) + @endpoint = endpoint + end + + def get(path) + uri = URI.join(@endpoint, path) + res = Net::HTTP.get_response(uri) + JSON.parse(res.body) + end + + def get_binary(path) + uri = URI.join(@endpoint, path) + http = Net::HTTP.new(uri.host, uri.port) + request = Net::HTTP::Get.new(uri.request_uri) + res = http.request(request).response + res.value + res.body + end + + def post(path, payload) + data = JSON.dump(payload) + uri = URI.join(@endpoint, path) + http = Net::HTTP.new(uri.host, uri.port) + request = Net::HTTP::Post.new(uri.request_uri) + request.body = data + res = http.request(request).response + JSON.parse(res.body) unless res.body.empty? + end + + def post_binary(path, stream, length, read_timeout: 60 * 2) + uri = URI.join(@endpoint, path) + http = Net::HTTP.new(uri.host, uri.port) + http.read_timeout = read_timeout + request = Net::HTTP::Post.new(uri.request_uri, 'Content-Length' => length.to_s) + # request.body = binary + request.body_stream = stream + res = http.request(request).response + JSON.parse(res.body) + end + end +end diff --git a/client/rb/lib/ios-device-server-client/device_info.rb b/client/rb/lib/ios-device-server-client/device_info.rb new file mode 100644 index 00000000..f3545434 --- /dev/null +++ b/client/rb/lib/ios-device-server-client/device_info.rb @@ -0,0 +1,21 @@ +module IosDeviceServerClient + class DeviceInfo + attr_reader :udid + attr_reader :model + attr_reader :name + attr_reader :os + attr_reader :arch + + def initialize(udid:, model:, name:, os:, arch:) + @udid = udid + @model = model + @name = name + @os = os + @arch = arch + end + + def to_s + "#{udid} | #{@name} | #{@model} | #{os} | #{arch}" + end + end +end diff --git a/client/rb/lib/ios-device-server-client/device_provider.rb b/client/rb/lib/ios-device-server-client/device_provider.rb new file mode 100644 index 00000000..106bb0ad --- /dev/null +++ b/client/rb/lib/ios-device-server-client/device_provider.rb @@ -0,0 +1,227 @@ +require 'base64' +require 'json' +require 'net/http' +require 'uri' + +require_relative 'device_client' + +module IosDeviceServerClient + class DeviceProviderFactory + # @return [DeviceProvider] + def self.create(server_url, read_timeout: 120) + DeviceProvider.new(server_url, read_timeout: read_timeout, credentials: DeviceServerCredentials.from_env) + end + end + + class DeviceServerCredentials + attr_reader :token + + def initialize(token) + if token.nil? || token.strip.empty? + raise(ArgumentError, 'token cannot be nil or empty') + end + @token = token + end + + def self.from_env + token = ENV.fetch('DEVICE_SERVER_AUTH_TOKEN', '').strip + token.empty? ? nil : DeviceServerCredentials.new(token) + end + + def to_s + "<#{self.class}: #{@token}>" + end + end + + class DeviceProvider + attr_reader :credentials + + # @param [String] endpoint + # @param [Fixnum] read_timeout + # @param [DeviceServerCredentials] credentials + def initialize(endpoint, read_timeout: 120, credentials: nil) + if endpoint.nil? || endpoint.empty? + raise(ArgumentError, 'endpoint cannot be nil or empty') + end + + @endpoint = URI(endpoint) + @read_timeout = read_timeout + @credentials = credentials + end + + def server_url + @endpoint + end + + def capacity(dc) + with_http do |http| + http.post('/devices/-/capacity', JSON.dump(dc)) + end + end + + def create(dc) + with_http do |http| + headers = { 'Content-Type' => 'application/json' } + + if credentials + headers['Authorization'] = auth_header_value + end + + request = Net::HTTP::Post.new('/devices', headers) + request.body = JSON.dump(dc) + http.request(request) + end + end + + def get(device_ref) + raise_if_ref_is_empty(device_ref) + + with_http do |http| + http.get("/devices/#{device_ref}") + end + end + + def endpoint_for(device_ref, port) + raise_if_ref_is_empty(device_ref) + + res = with_http do |http| + http.get("/devices/#{device_ref}/endpoint/#{port}") + end + res['endpoint'] + end + + def last_crash_log(device_ref) + raise_if_ref_is_empty(device_ref) + + with_http do |http| + http.get("/devices/#{device_ref}/crashes/last") + end + end + + def reset(device_ref) + raise_if_ref_is_empty(device_ref) + + with_http do |http| + http.post("/devices/#{device_ref}", JSON.dump(action: 'reset')) + end + end + + def clear_safari_cookies(device_ref) + raise_if_ref_is_empty(device_ref) + + with_http do |http| + http.post("/devices/#{device_ref}", JSON.dump(action: 'clear_safari_cookies')) + end + end + + def approve_access(device_ref, bundle_ids) + raise_if_ref_is_empty(device_ref) + + payload = bundle_ids.map { |x| { bundle_id: x } } + with_http do |http| + http.post("/devices/#{device_ref}/permissions", JSON.dump(payload)) + end + end + + def video_start(device_ref) + raise_if_ref_is_empty(device_ref) + + with_http do |http| + http.post("/devices/#{device_ref}/video", JSON.dump(start: true)) + end + end + + def video_stop(device_ref) + raise_if_ref_is_empty(device_ref) + + with_http do |http| + http.post("/devices/#{device_ref}/video", JSON.dump(start: false)) + end + end + + def video_get(device_ref) + raise_if_ref_is_empty(device_ref) + + Net::HTTP.start(@endpoint.host, @endpoint.port) do |http| + res = http.get("/devices/#{device_ref}/video") + raise_for_status(res) + res.body + end + end + + def state(device_ref) + raise_if_ref_is_empty(device_ref) + + with_http do |http| + http.get("/devices/#{device_ref}/state") + end + end + + def release(device_ref) + raise_if_ref_is_empty(device_ref) + + with_http do |http| + http.delete("/devices/#{device_ref}") + end + end + + def release_user_devices + with_http do |http| + request = Net::HTTP::Delete.new('/devices/') + request['Authorization'] = auth_header_value + http.request(request) + end + end + + # @note internal use only + def list + with_http do |http| + http.get('/devices') + end + end + + private + + def auth_header_value + if @credentials.nil? + raise(ArgumentError, 'Can not set auth header if on credentials were provided') + end + + "Bearer #{Base64.strict_encode64(@credentials.token)}" + end + + def raise_if_ref_is_empty(device_ref) + raise(ArgumentError, 'device_ref cannot be nil or empty') if device_ref.nil? || device_ref.strip.empty? + end + + def with_http(&_block) + res = Net::HTTP.start(@endpoint.host, @endpoint.port) do |http| + http.open_timeout = 5 + http.read_timeout = @read_timeout + yield(http) + end + raise_for_status(res) + JSON.parse(res.body) + rescue Errno::ECONNREFUSED => ex + raise(DeviceProviderError, "Device Server can not be reached. Please ensure Device Server is started. #{ex}") + end + + def raise_for_status(res) + return if res.is_a? Net::HTTPSuccess + + msg = JSON.pretty_unparse(JSON.parse(res.body)) rescue res.body # rubocop:disable Style/RescueModifier + + if res.code == '429' + raise(DeviceProviderCapacityError, "Device Server Capacity Error #{res.code}: #{msg}") + else + raise(DeviceProviderError, "Device Server Error #{res.code}: #{msg}") + end + end + end + + class DeviceProviderError < RuntimeError + end + + class DeviceProviderCapacityError < DeviceProviderError + end +end diff --git a/client/rb/lib/ios-device-server-client/remote_device.rb b/client/rb/lib/ios-device-server-client/remote_device.rb new file mode 100644 index 00000000..8079bbf8 --- /dev/null +++ b/client/rb/lib/ios-device-server-client/remote_device.rb @@ -0,0 +1,238 @@ +require 'uri' +require 'base64' + +require_relative 'device_provider' +require_relative 'device_client' + +module IosDeviceServerClient + module RemoteDeviceError + class ConnectionError < Errno::ECONNREFUSED + def initialize(device_ref, state, cause) + super("WebDriver connection refused for #{device_ref} in #{state}. Original error: #{cause}") + end + end + + class NotSupportedError < RuntimeError + def initialize(device_ref, capability) + super("Device #{device_ref} does not support #{capability}") + end + end + end + + class RemoteDevice + attr_reader :info + + # @return [String] + attr_reader :wda_endpoint + + # @return [Fixnum] + attr_reader :calabash_port + + # @return [String] + attr_reader :device_ref + + def initialize(server_endpoint, device_ref) + raise(ArgumentError, 'device_ref cannot be null') if device_ref.nil? + + @server = DeviceProviderFactory.create(server_endpoint) + @device_ref = device_ref + @device = nil # fbsimctl client + device = @server.get(@device_ref) + @actual_capabilities = device.fetch('capabilities', {}) + info = device['info'] + @wda_endpoint = device['wda_endpoint'] + @calabash_port = device['calabash_port'] + @info = DeviceInfo.new(udid: info['udid'], model: info['model'], name: info['name'], os: info['os'], arch: info['arch']) + end + + def to_s + "" + end + + # region: device management + + def release + @server.release(@device_ref) + end + + def reset(timeout: 60) + ensure_ready + @device = nil + @server.reset(@device_ref) + await(timeout: timeout) + end + + def await(timeout: 60) + last_state = nil + rv = wait_for(timeout: timeout, retry_frequency: 1.5) do + res = @server.state(@device_ref) + # short-circuit awaiting if device entered failed state (unrecoverable) + raise("Awaited device #{@device_ref} failed with #{res}") if res['state'] == 'failed' + last_state = res + res['ready'] + end + raise("Timed out waiting #{@device_ref} to be ready after #{timeout} s. Last state is #{last_state}") unless rv + @device = DeviceClient.from_hash(@server.get(@device_ref)) + end + + def set_location(lat:, lon:) + ensure_ready + if @actual_capabilities.fetch('set_location', true) + @device.set_location(lat: lat, lon: lon) + else + raise(RemoteDeviceError::NotSupportedError.new(@device_ref, __method__)) + end + end + + def clear_safari_cookies + ensure_ready + @server.clear_safari_cookies(@device_ref) + end + + # endregion + + # region: app management + def approve_access(bundle_ids) + ensure_ready + @server.approve_access(@device_ref, bundle_ids) + end + + def app_installed?(bundle_id) + ensure_ready + @device.app_installed?(bundle_id) + end + + def install_app(app_bundle_path) + ensure_ready + @device.install_app(app_bundle_path) + end + + def launch_app(bundle_id) + ensure_ready + @device.launch_app(bundle_id) + end + + def list_apps + ensure_ready + @device.list_apps + end + + def open_url(url) + ensure_ready + @device.open_url(url) + end + + def relaunch_app(bundle_id) + ensure_ready + @device.relaunch_app(bundle_id) + end + + def terminate_app(bundle_id) + ensure_ready + if @actual_capabilities.fetch('terminate_app', true) + @device.terminate_app(bundle_id) + else + raise(RemoteDeviceError::NotSupportedError.new(@device_ref, __method__)) + end + end + + def uninstall_app(bundle_id) + ensure_ready + @device.uninstall_app(bundle_id) + end + + # endregion + + # region: web driver, calabash, network + + def user_ports + # ports are known right after device creation, no need to await + if @device.nil? + device = @server.get(@device_ref) + return device['user_ports'] + end + @device.user_ports + end + + def create_driver(caps) + ensure_ready + @device.create_driver(caps) + rescue Errno::ECONNREFUSED => e + state = @server.state(@device_ref) rescue nil # rubocop:disable Style/RescueModifier + raise(RemoteDeviceError::ConnectionError.new(@device_ref, state, e)) + end + + def endpoint_for(port) + ensure_ready + @server.endpoint_for(@device_ref, port) + end + + # endregion + + # region: diagnostics + + def screenshot(filetype = DeviceClient::ScreenshotType::PNG, prefer_wda: true) + if prefer_wda + begin + return wda_screenshot + rescue RuntimeError => ex + puts "Error trying to retrieve screenshot using WDA #{ex}. Will fallback to fbsimctl" + end + end + + ensure_ready + @device.screenshot(filetype) + end + + def video_acquire + @server.video_get(@device_ref) + end + + def video_start + @server.video_start(@device_ref) + end + + def video_stop + @server.video_stop(@device_ref) + end + + def last_crash_log + ensure_ready + @server.last_crash_log(@device_ref) + end + + # endregion + + private + + def wda_screenshot + uri = URI.join(@wda_endpoint, 'screenshot') + res = Net::HTTP.start(uri.host, uri.port) do |http| + http.get(uri.request_uri) + end + + raise("WDA Screenshot request failed #{res.code}: #{res.body}") unless res.is_a? Net::HTTPSuccess + data = JSON.parse(res.body)['value'] + rv = Base64.decode64(data) + raise('WDA Screenshot returned empty data') if rv.empty? + rv + rescue Errno::ECONNREFUSED => ex + raise("WDA Screenshot request failed to connect. #{ex}") + end + + def ensure_ready + raise("#{@device_ref} is not ready. Have you forgot to #await device?") if @device.nil? + end + + def wait_for(timeout:, retry_frequency:, &_block) + finish = timeout && Time.now + timeout + begin + result = yield + break if result + sleep(retry_frequency) + now = Time.now + end until (finish && finish < now) + result + end + end +end diff --git a/device-server/build.gradle b/device-server/build.gradle new file mode 100644 index 00000000..297f21ca --- /dev/null +++ b/device-server/build.gradle @@ -0,0 +1,154 @@ +buildscript { + ext.kotlin_version = '1.2.41' + ext.kotlinx_version = '0.22.3' + ext.ktor_version = '0.9.1' + + repositories { + jcenter() + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +group 'com.badoo.automation' +version '1.0-SNAPSHOT' + +apply plugin: 'java' +apply plugin: 'kotlin' +apply plugin: 'application' + +mainClassName = 'com.badoo.automation.deviceserver.ProgramKt' + +sourceCompatibility = 1.8 + +repositories { + jcenter() + maven { url "https://dl.bintray.com/kotlin/kotlinx" } + maven { url "https://dl.bintray.com/kotlin/ktor" } +} + +dependencies { + compile group: 'org.jetbrains.kotlin', name: 'kotlin-stdlib-jdk8', version: kotlin_version + compile group: 'org.jetbrains.kotlin', name: 'kotlin-reflect', version: kotlin_version + compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-io', version: kotlinx_version + compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-jdk8', version: kotlinx_version + + compile group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.9.1' + + //region ktor dependencies + compile("io.ktor:ktor-server-netty:$ktor_version") { + exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-jre8" + exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-jre7" + exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-jdk8" + exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-io" + } + compile("io.ktor:ktor-features:$ktor_version") { + exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-jre8" + exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-jre7" + exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-jdk8" + exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-io" + } + compile("io.ktor:ktor-jackson:$ktor_version") { + exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-jre8" + exclude group: "org.jetbrains.kotlin", module: "kotlin-stdlib-jre7" + exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-jdk8" + exclude group: "org.jetbrains.kotlinx", module: "kotlinx-coroutines-io" + } + compile group: 'io.ktor', name: 'ktor-auth', version: ktor_version + //endregion + + //region log dependencies + + compile group: 'org.slf4j', name: 'slf4j-api', version: '1.7.25' + compile group: 'ch.qos.logback', name: 'logback-classic', version: '1.2.3' + compile group: 'net.logstash.logback', name: 'logstash-logback-encoder', version: '4.11' + //endregion + + //region json dependencies + compile 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.2' + //endregion + + //region process management dependencies + compile group: 'net.java.dev.jna', name: 'jna', version: "4.5.1" + compile group: 'com.zaxxer', name: 'nuprocess', version: "1.1.3" + //region + + //region test dependencies + testCompile group: "org.jetbrains.kotlin", name: "kotlin-test" + testCompile group: "org.jetbrains.kotlin", name: "kotlin-test-junit" + testCompile group: 'junit', name: 'junit', version: '4.12' + testCompile group: "com.nhaarman", name: "mockito-kotlin", version: "1.5.0" + testCompile group: 'org.mockito', name: 'mockito-core', version: '2.18.0' + testCompile group: 'org.mockito', name: 'mockito-inline', version: '2.18.0' + testCompile group: 'org.hamcrest', name: 'hamcrest-junit', version: '2.0.0.0' + //endregion +} + +compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} + +compileTestKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} + +kotlin { + experimental { + coroutines "enable" + } +} +jar { + manifest { + attributes 'Main-Class': mainClassName + } + + from { + configurations.compile.collect { it.isDirectory() ? it : zipTree(it) } + } +} + +/** + * For tests only + */ +run { + systemProperty 'wda.bundle.path', '../ios/facebook/simulators/WebDriverAgentRunner-Runner.app' + systemProperty 'wda.device.bundle.path', '../ios/facebook/devices/WebDriverAgentRunner-Runner.app' + systemProperty 'device.server.config.path', './config/.device_config' + systemProperty 'logback.configurationFile', 'logback.xml' +} + +test { + maxParallelForks = 4 +} + +kapt { + useBuildCache = true +} + +task wrapper(type: Wrapper) { + gradleVersion="4.5.1" +} + +// caching dependencies for docker +task resolveDependencies { + doLast { + project.buildscript.configurations.each { configuration -> + println "Downloading dependencies for project.buildscript.configuration => " + configuration + configuration.resolve() + } + + println "Downloading dependencies for project.configuration => :compile" + project.configurations['compile'].resolve() + println "Downloading dependencies for project.configuration => :compileKotlin" + project.configurations['compileKotlin'].resolve() + println "Downloading dependencies for project.configuration => :testCompile" + project.configurations['testCompile'].resolve() + println "Downloading dependencies for project.configuration => :compileTestKotlin" + project.configurations['compileTestKotlin'].resolve() + } +} diff --git a/device-server/config/multihost-sample.json b/device-server/config/multihost-sample.json new file mode 100644 index 00000000..b6055d20 --- /dev/null +++ b/device-server/config/multihost-sample.json @@ -0,0 +1,17 @@ +{ + "timeouts": { + "device": 600 + }, + "nodes": [ + { + "user": "qa", + "host": "host1", + "simulator_limit": 6 + }, + { + "user": "qa", + "host": "host2", + "simulator_limit": 6 + } + ] +} diff --git a/device-server/config/physical-sample.json b/device-server/config/physical-sample.json new file mode 100644 index 00000000..31b59c7a --- /dev/null +++ b/device-server/config/physical-sample.json @@ -0,0 +1,22 @@ +{ + "timeouts": { + "device": null + }, + "nodes": [ + { + "type": "devices", + "user": "", + "host": "localhost", + "whitelist_apps": [ + "com.apple.test.WebDriverAgentRunner-Runner" + ], + "devices": [ + { + "udid": "a000aaaa000a00aaa0a00000aa000a00aaa00a00", + "ip": "192.168.18.219", + "wifi_address": "00:00:00:00:00:00" + } + ] + } + ] +} diff --git a/device-server/docker/build.sh b/device-server/docker/build.sh new file mode 100755 index 00000000..42bf1537 --- /dev/null +++ b/device-server/docker/build.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +set -eu + +DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +PROJECT_ROOT="$(dirname "${DIR}")" + +readonly GRADLE_HOME="${HOME}/.gradle" + +docker run \ + --rm \ + --name="device-server-build" \ + --workdir=/home/gradle/device-server \ + -v ${PROJECT_ROOT}:/home/gradle/device-server \ + -v ${GRADLE_HOME}:/home/gradle/.gradle \ + gradle:4.6.0-jdk9 \ + gradle clean build --no-daemon -g /home/gradle/.gradle --info diff --git a/device-server/gradle/wrapper/gradle-wrapper.jar b/device-server/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..c44b679acd3f794ddbb3aa5e919244914911014a GIT binary patch literal 54333 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
K7|BUm`^7>V&Zk%^_d-%A~k@lFe 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

Qox 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 E17(ftEC2ui literal 0 HcmV?d00001 diff --git a/device-server/gradle/wrapper/gradle-wrapper.properties b/device-server/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..ef20e206 --- /dev/null +++ b/device-server/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +#Tue Feb 13 11:16:11 GMT 2018 +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-4.5.1-all.zip diff --git a/device-server/gradlew b/device-server/gradlew new file mode 100755 index 00000000..cccdd3d5 --- /dev/null +++ b/device-server/gradlew @@ -0,0 +1,172 @@ +#!/usr/bin/env sh + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/device-server/gradlew.bat b/device-server/gradlew.bat new file mode 100644 index 00000000..e95643d6 --- /dev/null +++ b/device-server/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/device-server/jar_launcher.sh b/device-server/jar_launcher.sh new file mode 100755 index 00000000..e565d927 --- /dev/null +++ b/device-server/jar_launcher.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +set -xe + +declare -r DEVICE_SERVER_CONFIG_PATH="${DEVICE_SERVER_CONFIG_PATH}" +declare -r DEVICE_SERVER_JAR="${DEVICE_SERVER_JAR}" + +declare -r WDA_RUNNER='../ios/facebook/simulators/WebDriverAgentRunner-Runner.app' +declare -r WDA_DEVICE_RUNNER='../ios/facebook/devices/WebDriverAgentRunner-Runner.app' +declare -r LOG_CONFIG='logback-test.xml' + +export JAVA_HOME=$(/usr/libexec/java_home -v 10) + +pushd "$( dirname "${BASH_SOURCE[0]}" )" + +exec java \ + -XX:+UseG1GC -XX:MaxGCPauseMillis=100 -XX:InitiatingHeapOccupancyPercent=45 \ + -XX:+HeapDumpOnOutOfMemoryError \ + -Dlogback.configurationFile=${LOG_CONFIG} \ + -Ddevice.server.config.path=${DEVICE_SERVER_CONFIG_PATH} \ + -Dwda.bundle.path=${WDA_RUNNER} \ + -Dwda.device.bundle.path=${WDA_DEVICE_RUNNER} \ + -jar ${DEVICE_SERVER_JAR} diff --git a/device-server/run_device_server.sh b/device-server/run_device_server.sh new file mode 100755 index 00000000..e9893e7b --- /dev/null +++ b/device-server/run_device_server.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +set -xe + +export DEVICE_SERVER_JAR='build/libs/device-server-1.0-SNAPSHOT.jar' +export JAVA_HOME=$(/usr/libexec/java_home -v 10) + +pushd "$( dirname "${BASH_SOURCE[0]}" )" + +echo "Building Device Server" +./gradlew jar --no-daemon + +./jar_launcher.sh \ No newline at end of file diff --git a/device-server/scripts/build_fbsimctl.sh b/device-server/scripts/build_fbsimctl.sh new file mode 100755 index 00000000..266973eb --- /dev/null +++ b/device-server/scripts/build_fbsimctl.sh @@ -0,0 +1,66 @@ +#!/usr/bin/env bash + +set -e +set -u + +# We are using forked version of fbsimctl with some fixes, see link below + +readonly REPOSITORY=https://github.com/NickAb/FBSimulatorControl.git +readonly REVISION=292a1bd # https://github.com/NickAb/FBSimulatorControl/commits/patched + +readonly VERSION_NAME=HEAD-${REVISION} +readonly FBSIMCTL_BASE_PATH=/usr/local/Cellar/fbsimctl +readonly DEST_DIR=${FBSIMCTL_BASE_PATH}/${VERSION_NAME} +readonly DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +readonly WORKING_DIR=$(mktemp -d) +readonly BUILD_DIR=${WORKING_DIR}/build + +clone() { + echo "Checking out ${REPOSITORY}#${REVISION} at ${WORKING_DIR}" + git clone ${REPOSITORY} "${WORKING_DIR}" + git checkout ${REVISION} +} + +build() { + echo "Building fbsimctl to ${BUILD_DIR}" + ./build.sh fbsimctl build "${BUILD_DIR}" +} + +copy_to_cellar() { + echo "Copying fbsimctl to Cellar ${DEST_DIR}" + rm -rf ${DEST_DIR} + mkdir -p ${DEST_DIR} + cp -rp "${BUILD_DIR}/bin" ${DEST_DIR}/ + cp -rp "${BUILD_DIR}/Frameworks" ${DEST_DIR}/ + cp -rp "${BUILD_DIR}/info.plist" ${DEST_DIR}/ +} + +cleanup() { + echo "Removing temp working directory" + rm -rf "${WORKING_DIR}" +} + +print_info() { + echo "" + echo "Built from ${REPOSITORY}#${REVISION}" + echo "with $(xcodebuild -version)" + echo "Results are at ${DEST_DIR}" + echo "" + echo "To set as default fbsimctl run:" + echo "brew switch fbsimctl ${VERSION_NAME}" + echo "" +} + +cli_build() { + pushd "${WORKING_DIR}" + + clone + build + copy_to_cellar + cleanup + print_info + + popd +} + +cli_build diff --git a/device-server/scripts/update_wda.sh b/device-server/scripts/update_wda.sh new file mode 100755 index 00000000..81902444 --- /dev/null +++ b/device-server/scripts/update_wda.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash + +set -e +set -u + +readonly SIGNING_PATCH=${SIGNING_PATCH:-} +readonly NO_DEVICE_BUILD=${NO_DEVICE_BUILD:-0} + +readonly REPOSITORY=https://github.com/facebook/WebDriverAgent.git +readonly REVISION=20246d3e +readonly DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" +readonly WORKING_DIR=$(mktemp -d) +readonly BASE_DEST=${DIR}/../../ios/facebook +readonly SIMULATOR_DEST=${BASE_DEST}/simulators +readonly DEVICE_DEST=${BASE_DEST}/devices +readonly VERSION_FILE=${BASE_DEST}/version.txt + + +cleanup() { + rm -rf "${WORKING_DIR}" +} + +finally () { + popd +} + +clone_wda() { + echo "Checking out ${REPOSITORY}#${REVISION} at ${WORKING_DIR}" + + git clone ${REPOSITORY} "${WORKING_DIR}" + git checkout ${REVISION} + ./Scripts/bootstrap.sh +} + +patch_wda() { + git status + git apply "${SIGNING_PATCH}" + git status +} + +update_wda_for_simulator() { + echo "Building WebDriverAgent for Simulators" + xcodebuild -project WebDriverAgent.xcodeproj -scheme WebDriverAgentRunner -destination 'generic/platform=iOS Simulator' -derivedDataPath build clean build-for-testing + + rm -rf "${SIMULATOR_DEST}" + mkdir -p "${SIMULATOR_DEST}" + + cp -r "${WORKING_DIR}/build/Build/Products/Debug-iphonesimulator/WebDriverAgentRunner-Runner.app" "${SIMULATOR_DEST}/WebDriverAgentRunner-Runner.app" +} + +update_wda_for_device() { + echo "Building WebDriverAgent for Devices" + xcodebuild -project WebDriverAgent.xcodeproj -scheme WebDriverAgentRunner -destination 'generic/platform=iOS' -derivedDataPath build clean build-for-testing + + rm -rf "${DEVICE_DEST}" + mkdir -p "${DEVICE_DEST}" + + cp -r "${WORKING_DIR}/build/Build/Products/Debug-iphoneos/WebDriverAgentRunner-Runner.app" "${DEVICE_DEST}/WebDriverAgentRunner-Runner.app" +} + +write_version_file() { + XCODE_VERSION=$(xcodebuild -version) + cat > "${VERSION_FILE}" << EOL +commit ${REVISION} +built with ${XCODE_VERSION} + +EOL +} + +cli_update() { + mkdir -p "${WORKING_DIR}" + pushd "${WORKING_DIR}" + + clone_wda + + if [[ "${NO_DEVICE_BUILD}" -eq 1 ]]; then + echo "Skipping build for devices"; + else + patch_wda + update_wda_for_device + fi; + + update_wda_for_simulator + + write_version_file + cleanup + + popd +} + +cli_update diff --git a/device-server/settings.gradle b/device-server/settings.gradle new file mode 100644 index 00000000..86921c24 --- /dev/null +++ b/device-server/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'device-server' + diff --git a/device-server/src/main/java/deviceserver/LogHighlighter.java b/device-server/src/main/java/deviceserver/LogHighlighter.java new file mode 100644 index 00000000..1762ca5e --- /dev/null +++ b/device-server/src/main/java/deviceserver/LogHighlighter.java @@ -0,0 +1,31 @@ +package deviceserver; + +import ch.qos.logback.classic.Level; +import ch.qos.logback.classic.spi.ILoggingEvent; +import ch.qos.logback.core.pattern.color.ForegroundCompositeConverterBase; + +import static ch.qos.logback.core.pattern.color.ANSIConstants.*; + +/** + * Rules for highlighting level in log messages. + */ +public class LogHighlighter extends ForegroundCompositeConverterBase { + @Override + protected String getForegroundColorCode(ILoggingEvent event) { + Level level = event.getLevel(); + switch (level.toInt()) { + case Level.ERROR_INT: + return BOLD + RED_FG; + case Level.WARN_INT: + return RED_FG; + case Level.INFO_INT: + return GREEN_FG; + case Level.DEBUG_INT: + return BLUE_FG; + case Level.TRACE_INT: + return CYAN_FG; + default: + return DEFAULT_FG; + } + } +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/AuthenticationPipelineExtensions.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/AuthenticationPipelineExtensions.kt new file mode 100644 index 00000000..1905f1f6 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/AuthenticationPipelineExtensions.kt @@ -0,0 +1,57 @@ +package com.badoo.automation.deviceserver + +import io.ktor.application.call +import io.ktor.auth.* +import io.ktor.request.ApplicationRequest +import io.ktor.response.respond + +class AnonymousPrincipal : Principal + +private fun HttpAuthHeader.Companion.bearerAuthChallenge(realm: String): HttpAuthHeader = + HttpAuthHeader.Parameterized("Bearer", mapOf(HttpAuthHeader.Parameters.Realm to realm)) + +fun AuthenticationPipeline.anonymousAuthentication() { + intercept(AuthenticationPipeline.RequestAuthentication) { context -> + if (context.principal == null) { + context.principal(AnonymousPrincipal()) + } + } +} + +fun AuthenticationPipeline.bearerAuthentication(realm: String, validate: suspend (String) -> UserIdPrincipal?) { + intercept(AuthenticationPipeline.RequestAuthentication) { context -> + val credentials = call.request.bearerAuthenticationToken() + val principal = credentials?.let { validate(it) } + + val cause = when { + credentials == null -> AuthenticationFailedCause.NoCredentials + principal == null -> AuthenticationFailedCause.InvalidCredentials + else -> null + } + + if (cause != null) { + context.challenge("Bearer", cause) { + call.respond(UnauthorizedResponse(HttpAuthHeader.bearerAuthChallenge(realm))) + it.complete() + } + } + if (principal != null) { + context.principal(principal) + } + } +} + +fun ApplicationRequest.bearerAuthenticationToken(): String? { + val parsed = parseAuthorizationHeader() + when (parsed) { + is HttpAuthHeader.Single -> { + if (parsed.authScheme != "Bearer") { + return null + } + + return parsed.blob + } + else -> return null + } +} + diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/Configuration.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/Configuration.kt new file mode 100644 index 00000000..bfacab92 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/Configuration.kt @@ -0,0 +1,18 @@ +package com.badoo.automation.deviceserver + +object Configuration { + + private const val wda_bundle_path_property = "wda.bundle.path" + private const val wda_device_bundle_path_property = "wda.device.bundle.path" + val WDA_BUNDLE_PATH: String = System.getProperty(wda_bundle_path_property) + ?: throw RuntimeException("Must set system property: -D$wda_bundle_path_property=" + + "/ABSOLUTE/PATH/ios/facebook/simulators/WebDriverAgentRunner-Runner.app") + + val WDA_DEVICE_BUNDLE_PATH: String = System.getProperty(wda_device_bundle_path_property) + ?: throw RuntimeException("Must set system property: -D$wda_device_bundle_path_property=" + + "/ABSOLUTE/PATH/ios/facebook/devices/WebDriverAgentRunner-Runner.app") + + private const val device_server_config_path = "device.server.config.path" + val DEVICE_SERVER_CONFIG_PATH: String = System.getProperty(device_server_config_path) + ?: throw RuntimeException("Must set system property: -D$device_server_config_path=./config/.device_config") +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt new file mode 100644 index 00000000..9238c577 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt @@ -0,0 +1,225 @@ +package com.badoo.automation.deviceserver + +import com.badoo.automation.deviceserver.controllers.DevicesController +import com.badoo.automation.deviceserver.controllers.StatusController +import com.badoo.automation.deviceserver.data.DesiredCapabilities +import com.badoo.automation.deviceserver.data.ErrorDto +import com.badoo.automation.deviceserver.data.toDto +import com.badoo.automation.deviceserver.host.management.DeviceManager +import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException +import com.badoo.automation.deviceserver.host.management.errors.NoAliveNodesException +import com.badoo.automation.deviceserver.host.management.errors.OverCapacityException +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.SerializationFeature +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule +import io.ktor.application.Application +import io.ktor.application.ApplicationCall +import io.ktor.application.call +import io.ktor.application.install +import io.ktor.auth.UnauthorizedResponse +import io.ktor.auth.UserIdPrincipal +import io.ktor.auth.authentication +import io.ktor.auth.principal +import io.ktor.features.CallLogging +import io.ktor.features.ContentNegotiation +import io.ktor.features.DefaultHeaders +import io.ktor.features.StatusPages +import io.ktor.http.HttpStatusCode +import io.ktor.jackson.jackson +import io.ktor.request.uri +import io.ktor.response.respond +import io.ktor.routing.* +import io.ktor.server.engine.ApplicationEngineEnvironmentReloading +import io.ktor.server.engine.ShutDownUrl +import org.slf4j.LoggerFactory +import java.io.File +import java.net.NetworkInterface +import java.util.* + +typealias EmptyMap = Map + +private fun jsonContent(call: ApplicationCall): JsonNode { + val json = call.request.receiveContent().inputStream() + return JsonMapper().readTree(json) +} + +private inline fun jsonContent(call: ApplicationCall): T { + return JsonMapper().fromJson(call.request.receiveContent().inputStream()) +} + +private fun param(call: ApplicationCall, s: String): String { + return call.parameters[s] ?: throw Exception("Parameter $s missing from ${call.request.uri}") +} + +private fun paramInt(call: ApplicationCall, s: String): Int { + try { + return param(call, s).toInt() + } catch (e: NumberFormatException) { + throw Exception("Parameter $s was not an integer in ${call.request.uri}") + } +} + +fun getAddresses(): List { + return NetworkInterface.getNetworkInterfaces().toList().flatMap { + it.inetAddresses.toList() + .filter { it.address.size == 4 } + .filter { !it.isLoopbackAddress } + .map { it.hostAddress + "/" + it.hostName } + } +} + +private fun serverConfig(): DeviceServerConfig { + if (Configuration.DEVICE_SERVER_CONFIG_PATH.isEmpty()) { + logger.info("Using default config") + return DeviceServerConfig(nodes = listOf(NodeConfig()), timeouts = emptyMap()) + } + + val configFile = File(Configuration.DEVICE_SERVER_CONFIG_PATH) + + if (!configFile.exists()) { + val msg = "Config file ${configFile.path} not found" + logger.error(msg) + throw RuntimeException(msg) + } + + logger.info("Using config file: ${configFile.path}") + return JsonMapper().fromJson(configFile.readText()) +} + +var routes: Route? = null + +private val logger = LoggerFactory.getLogger(DevicesController::class.java.simpleName) + +@Suppress("unused") +fun Application.module() { + val config = serverConfig() + + val deviceManager = DeviceManager(config) + deviceManager.startAutoRegisteringDevices() + deviceManager.launchAutoReleaseLoop() + + val devicesController = DevicesController(deviceManager) + val statusController = StatusController(deviceManager) + + install(DefaultHeaders) + install(CallLogging) + install(ContentNegotiation) { + jackson { + configure(SerializationFeature.INDENT_OUTPUT, true) + registerModule(JavaTimeModule()) + } + } + + install(ShutDownUrl.ApplicationCallFeature) { + shutDownUrl = "/quitquitquit" + exitCodeSupplier = { 1 } + } + + authentication { + bearerAuthentication("default") { token -> + val name = Base64.getDecoder().decode(token).toString(Charsets.ISO_8859_1) + when { + name.isEmpty() -> null + else -> UserIdPrincipal(name) + } + } + anonymousAuthentication() + } + + logger.info("Server: Installing routing...") + routes = install(Routing) { + get { + call.respond(statusController.welcomeMessage(routes)) + } + get("status") { + call.respond(statusController.getServerStatus()) + } + route("devices") { + get { + call.respond(devicesController.getDeviceRefs()) + } + post { + val user = call.principal() + call.respond(devicesController.createDevice(jsonContent(call), user)) + } + delete { + val user = call.principal() + if (user == null) { + call.respond(UnauthorizedResponse()) + } else { + call.respond(devicesController.releaseDevices(user)) + } + } + post("-/capacity") { + call.respond(devicesController.getTotalCapacity(jsonContent(call))) + } + route("{ref}") { + get { + call.respond(devicesController.getDeviceContactDetails(param(call, "ref"))) + } + post { + call.respond(devicesController.controlDevice(param(call, "ref"), jsonContent(call))) + } + delete { + call.respond(devicesController.deleteReleaseDevice(param(call, "ref"))) + } + post("permissions") { + call.respond(devicesController.setAccessToCameraAndThings(param(call, "ref"), jsonContent(call))) + } + get("endpoint/{port}") { + call.respond(devicesController.getEndpointFor(param(call, "ref"), paramInt(call, "port"))) + } + get("crashes/last") { + call.respond(devicesController.getLastCrashLog(param(call, "ref"))) + } + route("video") { + get { + //FIXME: should be a better way of streaming a file over HTTP. without caching bytes in server's memory. Investigating ByteReadChannel + //FIXME: see [call.respondFile] basically - read from ssh proc listener's ByteBuffer + call.respond(devicesController.getVideo(param(call, "ref"))) + } + post { + call.respond(devicesController.startStopVideo(param(call, "ref"), jsonContent(call))) + } + delete { + call.respond(devicesController.deleteVideo(param(call, "ref"))) + } + } + get("state") { + call.respond(devicesController.getDeviceState(param(call, "ref"))) + } + } + } + } + + logger.info("Server: Installing status pages...") + install(StatusPages) { + status(HttpStatusCode.NotFound) { + val msg = "${it.value} ${it.description} : ${call.request.uri}" + val error = ErrorDto("RouteNotFound",msg, emptyList()) + call.respond(HttpStatusCode.NotFound, + hashMapOf("error" to error) + ) + } + exception { exception: Throwable -> + val statusCode = when (exception) { + is IllegalArgumentException -> HttpStatusCode(422, "Unprocessable Entity") + is DeviceNotFoundException -> HttpStatusCode.NotFound + is NoAliveNodesException -> HttpStatusCode.TooManyRequests + is OverCapacityException -> HttpStatusCode.TooManyRequests + else -> HttpStatusCode.InternalServerError + } + call.respond(statusCode, + hashMapOf( + "error" to exception.toDto() + ) + ) + } + } + + logger.info("Server: Installation complete. Should be available at ${connectors()} ${getAddresses()}") +} + +private fun Application.connectors(): String { + return (this.environment as ApplicationEngineEnvironmentReloading).connectors.toString() +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServerConfig.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServerConfig.kt new file mode 100644 index 00000000..64d3e5e3 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServerConfig.kt @@ -0,0 +1,9 @@ +package com.badoo.automation.deviceserver + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties + +@JsonIgnoreProperties( ignoreUnknown = true ) +data class DeviceServerConfig( + val timeouts: Map, + val nodes: List +) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/JsonMapper.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/JsonMapper.kt new file mode 100644 index 00000000..26422307 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/JsonMapper.kt @@ -0,0 +1,35 @@ +package com.badoo.automation.deviceserver + +import com.fasterxml.jackson.core.JsonParser +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import java.io.InputStream + +class JsonMapper { + + private val mapper = jacksonObjectMapper() + .configure(JsonParser.Feature.ALLOW_TRAILING_COMMA, true) + .configure(JsonParser.Feature.ALLOW_NUMERIC_LEADING_ZEROS, true) + .configure(JsonParser.Feature.ALLOW_SINGLE_QUOTES, true) + .configure(JsonParser.Feature.ALLOW_COMMENTS, true) + .configure(JsonParser.Feature.ALLOW_YAML_COMMENTS, true)!! + + fun fromJson(string: String, clazz: Class): T { + return mapper.readValue(string, clazz) + } + + fun fromJson(stream: InputStream, clazz: Class): T { + return mapper.readValue(stream, clazz) + } + + fun readTree(stream: InputStream): JsonNode { + return mapper.readTree(stream) ?: throw RuntimeException("Failed to parse json") + } + + inline fun fromJson(string: String): T = fromJson(string, T::class.java) + inline fun fromJson(stream: InputStream): T = fromJson(stream, T::class.java) + + fun toJson(value: T): String { + return mapper.writeValueAsString(value) + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/LogMarkers.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/LogMarkers.kt new file mode 100644 index 00000000..71c97074 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/LogMarkers.kt @@ -0,0 +1,16 @@ +package com.badoo.automation.deviceserver + +/** + * Constants for log markers. not LogMarkers themselves. + * LogMarkers are used to filter messages (for example in Elastic) + * + * Consider using MDC (might be too complicated as set's context on thread level). +*/ +class LogMarkers { + companion object { + const val DEVICE_REF = "deviceRef" + const val UDID = "udid" + const val HOSTNAME = "hostname" + const val SSH_PROFILING = "sshProfiling" + } +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/NodeConfig.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/NodeConfig.kt new file mode 100644 index 00000000..dba0e708 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/NodeConfig.kt @@ -0,0 +1,38 @@ +package com.badoo.automation.deviceserver + +import com.badoo.automation.deviceserver.ios.device.KnownDevice +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty + +@JsonIgnoreProperties( ignoreUnknown = true ) +data class NodeConfig( + @JsonProperty("user") + val user: String = "", + + @JsonProperty("host") + val host: String = "localhost", + + @JsonProperty("simulator_limit") + val simulatorLimit: Int = 6, + + @JsonProperty("concurrent_boots") + val concurrentBoots: Int = 3, + + @JsonProperty("type") + val type: NodeConfig.NodeType = NodeType.Simulators, + + @JsonProperty("whitelist_apps") + val whitelistApps: Set = emptySet(), + + @JsonProperty("devices") + val knownDevices: List = emptyList() +) { + + enum class NodeType { + @JsonProperty("simulators") + Simulators, + + @JsonProperty("devices") + Devices + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/Program.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/Program.kt new file mode 100644 index 00000000..1c30b87d --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/Program.kt @@ -0,0 +1,9 @@ +package com.badoo.automation.deviceserver + +import io.ktor.server.engine.commandLineEnvironment +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty + +fun main(args: Array) { + embeddedServer(Netty, commandLineEnvironment(args)).start(wait = true) +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/WaitTimeoutError.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/WaitTimeoutError.kt new file mode 100644 index 00000000..55d75280 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/WaitTimeoutError.kt @@ -0,0 +1,3 @@ +package com.badoo.automation.deviceserver + +class WaitTimeoutError(message: String, e: Throwable? = null) : RuntimeException(message, e) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ChildProcess.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ChildProcess.kt new file mode 100644 index 00000000..3413b29f --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ChildProcess.kt @@ -0,0 +1,53 @@ +package com.badoo.automation.deviceserver.command + +import com.badoo.automation.deviceserver.LogMarkers +import com.badoo.automation.deviceserver.host.Remote +import com.badoo.automation.deviceserver.ios.proc.LongRunningProcessListener +import net.logstash.logback.marker.MapEntriesAppendingMarker +import org.slf4j.LoggerFactory +import java.time.Duration + +class ChildProcess private constructor( + command: List, + executor: IShellCommand, + remoteHostname: String, + private val processListener: LongRunningProcessListener +) { + private val logger = LoggerFactory.getLogger(javaClass.simpleName) + private val logMarker = MapEntriesAppendingMarker(mapOf(LogMarkers.HOSTNAME to remoteHostname)) + + init { + logger.debug(logMarker, "Starting long living process from command [$command]") + executor.startProcess(command, mapOf(), processListener = processListener) + logger.debug(logMarker, "Started long living process $this from command [$command]") + } + + override fun toString(): String = "< PID: ${processListener.pid}>" + + fun isAlive(): Boolean = processListener.isAlive + + fun kill(timeOut: Duration = Duration.ofSeconds(2)) { + logger.debug(logMarker, "Sending SIGTERM to process $this") + val result = processListener.destroy(false, timeOut) + if (result == Int.MIN_VALUE) { + logger.warn(logMarker, "Process $this did not terminate gracefully within [${timeOut.seconds}] seconds. Sending SIGKILL") + processListener.destroy(true, timeOut) + } + } + + fun writeStdin(string: String) { + processListener.writeStdin(string) + } + + companion object { + fun fromCommand( + remoteHost: String, userName: String, cmd: List, isInteractiveShell: Boolean, + out_reader: (line: String) -> Unit, + err_reader: (line: String) -> Unit + ): ChildProcess { + val executor = Remote.getRemoteCommandExecutor(hostName = remoteHost, userName = userName, isInteractiveShell = isInteractiveShell) + return ChildProcess(cmd, executor, remoteHost, LongRunningProcessListener(out_reader, err_reader)) + } + } +} + diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/CommandResult.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/CommandResult.kt new file mode 100644 index 00000000..7b6a6b0a --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/CommandResult.kt @@ -0,0 +1,11 @@ +package com.badoo.automation.deviceserver.command + +data class CommandResult( + // FIXME: Separate binary and string results for "capture" and "process open" executors + val stdOut: String, + val stdErr: String, + @Suppress("ArrayInDataClass") val stdOutBytes: ByteArray, + val exitCode: Int, + val isSuccess: Boolean = exitCode == 0, + val cmd: List = listOf() +) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/IShellCommand.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/IShellCommand.kt new file mode 100644 index 00000000..cd19630b --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/IShellCommand.kt @@ -0,0 +1,33 @@ +package com.badoo.automation.deviceserver.command + +import com.badoo.automation.deviceserver.ios.proc.LongRunningProcessListener +import org.slf4j.Marker +import java.time.Duration + +interface IShellCommand { + /** + * Will run passed shell command and return result. + * By default any command times out after default [timeOut]. + * If you expect the command to take more than default [timeOut] + * then specify [timeOut] for your command. + * + * Example: exec("/usr/local/bin/fbsimctl", listOf("--json", "list")) + * + * Will throw [SshConnectionException] if remote host is unreachable + */ + fun exec( + command: List, + environment: Map = mapOf(), + timeOut: Duration = Duration.ofSeconds(60), + returnFailure: Boolean = true, + logMarker: Marker? = null, + processListener: IShellCommandListener = ShellCommandListener() + ): CommandResult + + fun startProcess( + command: List, + environment: Map, + logMarker: Marker? = null, + processListener: LongRunningProcessListener + ) +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/IShellCommandListener.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/IShellCommandListener.kt new file mode 100644 index 00000000..c900ae4f --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/IShellCommandListener.kt @@ -0,0 +1,10 @@ +package com.badoo.automation.deviceserver.command + +import com.zaxxer.nuprocess.NuProcessHandler + +interface IShellCommandListener : NuProcessHandler { + val stdOut: String + val stdErr: String + val bytes: ByteArray // binary data. for ex.: video recording + val exitCode: Int +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt new file mode 100644 index 00000000..9bd2f7ff --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt @@ -0,0 +1,105 @@ +package com.badoo.automation.deviceserver.command + +import com.badoo.automation.deviceserver.LogMarkers +import com.badoo.automation.deviceserver.ios.proc.LongRunningProcessListener +import com.zaxxer.nuprocess.NuProcessBuilder +import net.logstash.logback.marker.MapEntriesAppendingMarker +import org.slf4j.Marker +import java.time.Duration + +class RemoteShellCommand( + private val remoteHost: String, + userName: String, + builderFactory: (cmd: List, env: Map) -> NuProcessBuilder = ::defaultNuProcessBuilder, + commonEnvironment: Map = mapOf(), + isInteractiveShell: Boolean = false, + isVerboseMode: Boolean = false, + connectionTimeout: Int = 10 +) : ShellCommand(builderFactory, commonEnvironment) { + private val userAtHost: String = if (userName.isBlank()) { remoteHost } else { "$userName@$remoteHost" } + override val logMarker: Marker get() = MapEntriesAppendingMarker(mapOf(LogMarkers.HOSTNAME to remoteHost)) + private val sshProfilingLogMarker = MapEntriesAppendingMarker(mapOf( + LogMarkers.HOSTNAME to remoteHost, + LogMarkers.SSH_PROFILING to "true" + )) + private val sshEnv: Map + private val sshCommandPrefix: List + init { + //ssh command prefix + val sshPrefix = arrayListOf() + sshPrefix.addAll(listOf( + SSH_COMMAND, + "-o", "ConnectTimeout=$connectionTimeout", + "-o", "PreferredAuthentications=publickey", + QUIET_MODE + )) + + if (isInteractiveShell) { + sshPrefix.addAll(FORCE_PSEUDO_TERMINAL_ALLOCATION) + } else { + sshPrefix.add(NO_PSEUDO_TERMINAL_ALLOCATION) + } + + if (isVerboseMode) { + sshPrefix.add("-vvv") + } + + sshPrefix.add(userAtHost) + sshCommandPrefix = ArrayList(sshPrefix) + + //ssh environment + val env = mutableMapOf() + val sshAuthSocket = System.getenv(SSH_AUTH_SOCK) + + if (sshAuthSocket != null) { + env[SSH_AUTH_SOCK] = sshAuthSocket + } + + sshEnv = HashMap(env) + } + + override fun exec(command: List, environment: Map, timeOut: Duration, + returnFailure: Boolean, logMarker: Marker?, + processListener: IShellCommandListener): CommandResult { + val cmd = getCommandWithSSHPrefix(command) + val start = System.currentTimeMillis() + val result = super.exec(cmd, getEnvironmentForSSH(), timeOut, returnFailure, logMarker, processListener) + val elapsed = System.currentTimeMillis() - start + logger.debug(sshProfilingLogMarker, "Execution of SSH command took $elapsed ms. Command: $cmd") + + if (result.exitCode == SSH_ERROR) { + // FIXME: Check stdout and stderr, if they are empty – ssh timeout, otherwise, it is likely to be command error + val message = "Probably SSH could not connect to node $remoteHost while running command [$command]. See stdout: <${result.stdOut}> stderr: <${result.stdErr}>" + logger.error(logMarker, message) + throw SshConnectionException(message) + } + + return result + } + + override fun startProcess(command: List, environment: Map, logMarker: Marker?, + processListener: LongRunningProcessListener) { + super.startProcess(getCommandWithSSHPrefix(command), getEnvironmentForSSH(), logMarker, processListener) + } + + private fun getEnvironmentForSSH(): HashMap { + val envWithSsh = HashMap(sshEnv) + envWithSsh.putAll(envWithSsh) + return envWithSsh + } + + private fun getCommandWithSSHPrefix(command: List): ArrayList { + val commandWithSshPrefix = ArrayList(sshCommandPrefix) + commandWithSshPrefix.addAll(command) + return commandWithSshPrefix + } + + private companion object { + const val SSH_COMMAND = "/usr/bin/ssh" + const val QUIET_MODE = "-q" + val FORCE_PSEUDO_TERMINAL_ALLOCATION = listOf("-t", "-t") + const val NO_PSEUDO_TERMINAL_ALLOCATION = "-T" + const val SSH_AUTH_SOCK = "SSH_AUTH_SOCK" + const val SSH_ERROR = 255 + } +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommand.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommand.kt new file mode 100644 index 00000000..eb0bdda0 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommand.kt @@ -0,0 +1,73 @@ +package com.badoo.automation.deviceserver.command + +import com.badoo.automation.deviceserver.LogMarkers +import com.badoo.automation.deviceserver.ios.proc.LongRunningProcessListener +import com.badoo.automation.deviceserver.util.ensure +import com.zaxxer.nuprocess.NuProcess +import com.zaxxer.nuprocess.NuProcessBuilder +import com.zaxxer.nuprocess.NuProcessHandler +import net.logstash.logback.marker.MapEntriesAppendingMarker +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import org.slf4j.Marker +import java.time.Duration +import java.util.concurrent.TimeUnit + +open class ShellCommand( + private val builderFactory: (cmd: List, env: Map) -> NuProcessBuilder = ::defaultNuProcessBuilder, //for testing + private val commonEnvironment: Map = mapOf() + ) : IShellCommand { + protected val logger: Logger = LoggerFactory.getLogger(javaClass.simpleName) + protected open val logMarker: Marker get() = MapEntriesAppendingMarker(mapOf(LogMarkers.HOSTNAME to "localhost")) + + companion object { + fun defaultNuProcessBuilder(cmd: List, env: Map): NuProcessBuilder = NuProcessBuilder(cmd, env) + } + + override fun exec(command: List, environment: Map, timeOut: Duration, + returnFailure: Boolean, logMarker: Marker?, processListener: IShellCommandListener): CommandResult { + val process: NuProcess = startProcessInternal(command, environment, processListener) + + var exitCode = Int.MIN_VALUE + + try { + exitCode = process.waitFor(timeOut.toMillis(), TimeUnit.MILLISECONDS) + } catch (e: InterruptedException) { + logger.warn(logMarker, "Error while running command: ${command.joinToString(" ")}", e) + } + + if (exitCode == Int.MIN_VALUE) { // waiting timed out + try { + process.destroy(true) + } catch (e: RuntimeException) { + logger.warn(logMarker, "Error while terminating command: ${command.joinToString(" ")}", e) + } + } + + val result = CommandResult( + stdOut = processListener.stdOut, + stdErr = processListener.stdErr, + stdOutBytes = processListener.bytes, + exitCode = processListener.exitCode, + cmd = command // Store actual command - including ssh stuff. + ) + ensure(processListener.exitCode == 0 || returnFailure) { + val errorMessage = "Error while running command: ${command.joinToString(" ")} Result=$result" + logger.error(logMarker, errorMessage) + ShellCommandException(errorMessage) + } + return result + } + + override fun startProcess(command: List, environment: Map, logMarker: Marker?, processListener: LongRunningProcessListener) { + startProcessInternal(command, environment, processListener) + } + + private fun startProcessInternal(command: List, environment: Map, processListener: NuProcessHandler): NuProcess { + logger.info(logMarker, "Executing command: ${command.joinToString(" ")}") + val cmdEnv = environment + commonEnvironment + val processBuilder = builderFactory(command, cmdEnv) + processBuilder.setProcessListener(processListener) + return processBuilder.start() + } +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommandException.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommandException.kt new file mode 100644 index 00000000..53889147 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommandException.kt @@ -0,0 +1,3 @@ +package com.badoo.automation.deviceserver.command + +class ShellCommandException(message: String?, cause: Throwable? = null) : RuntimeException(message, cause) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommandListener.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommandListener.kt new file mode 100644 index 00000000..3da19dfc --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommandListener.kt @@ -0,0 +1,60 @@ +package com.badoo.automation.deviceserver.command + +import com.zaxxer.nuprocess.NuAbstractProcessHandler +import com.zaxxer.nuprocess.NuProcess +import org.slf4j.LoggerFactory +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.nio.ByteBuffer +import java.nio.channels.Channels +import java.nio.channels.WritableByteChannel +import java.nio.charset.StandardCharsets.UTF_8 + +open class ShellCommandListener(initialStdOutBufferSize: Int = INITIAL_BYTE_ARRAY_SIZE) : IShellCommandListener, NuAbstractProcessHandler() { + private lateinit var nuProcess: NuProcess + // setting initial size to non-default for large amounts of data. to avoid too many Arrays.copyOf + private val stdOutBytes = ByteArrayOutputStream(initialStdOutBufferSize) + private val stdErrBytes = ByteArrayOutputStream(INITIAL_BYTE_ARRAY_SIZE) + private val stdOutBytesChannel: WritableByteChannel = Channels.newChannel(stdOutBytes) + private val stdErrBytesChannel: WritableByteChannel = Channels.newChannel(stdErrBytes) + private val logger = LoggerFactory.getLogger(javaClass.simpleName) + + override var exitCode: Int = Int.MIN_VALUE + override val stdOut: String get() = stdOutBytes.toString(UTF_8.name()) + override val stdErr: String get() = stdErrBytes.toString(UTF_8.name()) + override val bytes: ByteArray get() = stdOutBytes.toByteArray() + + override fun onPreStart(nuProcess: NuProcess) { + this.nuProcess = nuProcess + } + + override fun onExit(exitCode: Int) { + this.exitCode = exitCode + } + + override fun onStderr(buffer: ByteBuffer?, closed: Boolean) { + fetchOutput(buffer, stdErrBytesChannel) + super.onStderr(buffer, closed) + } + + override fun onStdout(buffer: ByteBuffer?, closed: Boolean) { + fetchOutput(buffer, stdOutBytesChannel) + super.onStdout(buffer, closed) + } + + private fun fetchOutput(buffer: ByteBuffer?, writer: WritableByteChannel) { + if (buffer != null && buffer.hasRemaining()) { + try { + writer.write(buffer) + } catch (e: IOException) { + // should be ok to catch only IOException, as NonWritableChannelException is not expected as channel is writable + logger.error("Failed to write data from buffer", e) + throw e + } + } + } + + companion object { + const val INITIAL_BYTE_ARRAY_SIZE = Short.MAX_VALUE.toInt() // should be ok for most cases. increase if too many array copy + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellUtils.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellUtils.kt new file mode 100644 index 00000000..09f72b49 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellUtils.kt @@ -0,0 +1,24 @@ +package com.badoo.automation.deviceserver.command + +class ShellUtils { + companion object { + private val escapeRegex = Regex("([^A-Za-z0-9_\\-.,:/@\\n])") + + /// Copied from shellwords.rb, line 123 + fun escape(s: String): String { + // An empty argument will be skipped, so return empty quotes. + if (s.isEmpty()) { + return "''" + } + + // Treat multibyte characters as is. It is caller's responsibility + // to encode the string in the right encoding for the shell + // environment. + + return s.replace(escapeRegex, { "\\" + it.value }) + // A LF cannot be escaped with a backslash because a backslash + LF + // combo is regarded as line continuation and simply ignored. + .replace("\n", "'\n'") + } + } +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/SshConnectionException.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/SshConnectionException.kt new file mode 100644 index 00000000..d1c130af --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/SshConnectionException.kt @@ -0,0 +1,3 @@ +package com.badoo.automation.deviceserver.command + +class SshConnectionException(message: String, cause: Throwable? = null) : RuntimeException(message, cause) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt new file mode 100644 index 00000000..3bfe8ac8 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt @@ -0,0 +1,89 @@ +package com.badoo.automation.deviceserver.controllers + +import com.badoo.automation.deviceserver.EmptyMap +import com.badoo.automation.deviceserver.data.DesiredCapabilities +import com.badoo.automation.deviceserver.data.DeviceDTO +import com.badoo.automation.deviceserver.data.DeviceRef +import com.badoo.automation.deviceserver.data.SimulatorStatusDTO +import com.badoo.automation.deviceserver.host.management.IDeviceManager +import com.fasterxml.jackson.databind.JsonNode +import io.ktor.auth.UserIdPrincipal + +class DevicesController(private val deviceManager: IDeviceManager) { + private val happy = emptyMap() + + fun getDeviceRefs(): List { + return deviceManager.getDeviceRefs() + } + + fun createDevice(desiredCapabilities: DesiredCapabilities, user: UserIdPrincipal?): DeviceDTO { + return deviceManager.createDeviceAsync(desiredCapabilities, user?.name) + } + + fun getDeviceContactDetails(ref: DeviceRef): DeviceDTO { + return deviceManager.getGetDeviceDTO(ref) + } + + fun controlDevice(ref: DeviceRef, jsonContent: JsonNode): EmptyMap { + val action = jsonContent["action"]?.asText() + when (action) { + "reset" -> deviceManager.resetAsyncDevice(ref) + "clear_safari_cookies" -> deviceManager.clearSafariCookies(ref) + else -> throw IllegalArgumentException("Unknown action $action") + } + return happy + } + + fun deleteReleaseDevice(ref: DeviceRef): EmptyMap { + deviceManager.deleteReleaseDevice(ref, "httpRequest") + return happy + } + + fun releaseDevices(user: UserIdPrincipal) { + deviceManager.releaseUserDevices(user.name, "httpRequest") + } + + fun setAccessToCameraAndThings(ref: DeviceRef, jsonContent: JsonNode): EmptyMap { + jsonContent.elements().forEach { deviceManager.approveAccess(ref, it["bundle_id"].textValue()) } + return happy + } + + fun getEndpointFor(ref: DeviceRef, port: Int): Map { + return mapOf("endpoint" to deviceManager.getEndpointFor(ref, port).toString()) + } + + fun getLastCrashLog(ref: DeviceRef): Map { + val log = deviceManager.getLastCrashLog(ref) + return mapOf("filename" to log.filename, "content" to log.content) + } + + fun startStopVideo(ref: DeviceRef, jsonContent: JsonNode): EmptyMap { + val start = jsonContent["start"] + if (start.isBoolean) { + when (start.asBoolean()) { + true -> deviceManager.startVideo(ref) + false -> deviceManager.stopVideo(ref) + } + } else { + throw IllegalArgumentException("Parameter start should be boolean true or false") + } + return happy + } + + fun getVideo(ref: DeviceRef): ByteArray { + return deviceManager.getVideo(ref) + } + + fun deleteVideo(ref: DeviceRef): EmptyMap { + deviceManager.deleteVideo(ref) + return happy + } + + fun getDeviceState(ref: DeviceRef): SimulatorStatusDTO { + return deviceManager.getDeviceState(ref) + } + + fun getTotalCapacity(desiredCapabilities: DesiredCapabilities): Map { + return deviceManager.getTotalCapacity(desiredCapabilities) + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/StatusController.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/StatusController.kt new file mode 100644 index 00000000..53c534fa --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/StatusController.kt @@ -0,0 +1,27 @@ +package com.badoo.automation.deviceserver.controllers + +import com.badoo.automation.deviceserver.host.management.IDeviceManager +import io.ktor.routing.Route + +class StatusController(private val deviceManager: IDeviceManager) { + fun welcomeMessage(routes: Route?): String { + return "Welcome to the device server.\n" + + childHierarchy(routes?.children ?: emptyList()) + "\n" + + "Minimal /status, but /quitquitquit works\n" + } + + fun getServerStatus(): Map { + val status = deviceManager.getStatus() + + return mapOf( + "status" to "ok", + "deviceManager" to status + ) + } + + private fun childHierarchy(children: List, margin: String = "\n. "): Map { + return children.sortedBy { it.toString() } + .map { margin + it to childHierarchy(it.children, "$margin. ").toString() } + .toMap() + } +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/CrashLog.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/CrashLog.kt new file mode 100644 index 00000000..9f394055 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/CrashLog.kt @@ -0,0 +1,3 @@ +package com.badoo.automation.deviceserver.data + +data class CrashLog(val filename: String, val content: String) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DesiredCapabilities.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DesiredCapabilities.kt new file mode 100644 index 00000000..139cfc82 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DesiredCapabilities.kt @@ -0,0 +1,10 @@ +package com.badoo.automation.deviceserver.data + +data class DesiredCapabilities( + val udid: String?, + val model: String?, + val os: String?, + val headless: Boolean = true, + val existing: Boolean = true, + val arch: String? = null +) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceAllocatedPorts.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceAllocatedPorts.kt new file mode 100644 index 00000000..9adb3ee0 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceAllocatedPorts.kt @@ -0,0 +1,12 @@ +package com.badoo.automation.deviceserver.data + +data class DeviceAllocatedPorts( + val fbsimctlPort: Int, + val wdaPort: Int, + val calabashPort: Int, + private val defaultCalabashPort: Int = 37265 +) { + fun toSet(): Set { + return setOf(fbsimctlPort, wdaPort, calabashPort, defaultCalabashPort) + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceDTO.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceDTO.kt new file mode 100644 index 00000000..c5a8f00c --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceDTO.kt @@ -0,0 +1,20 @@ +package com.badoo.automation.deviceserver.data + +import java.net.URI + +data class DeviceDTO( + val ref: DeviceRef, + val state: DeviceState, + val fbsimctl_endpoint: URI, + val wda_endpoint: URI, + val calabash_port: Int, + val user_ports: Set, // From PortAllocator + val info: DeviceInfo, + val last_error: ErrorDto?, + val capabilities: ActualCapabilities? +) + +data class ActualCapabilities( + val setLocation: Boolean, + val terminateApp: Boolean +) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceInfo.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceInfo.kt new file mode 100644 index 00000000..5abbced0 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceInfo.kt @@ -0,0 +1,17 @@ +package com.badoo.automation.deviceserver.data + +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDevice + +typealias UDID = String + +// FIXME: DeviceInfo is same as FBSimctlDevice +data class DeviceInfo ( + val udid: UDID, + val model: String, + val os: String, + val arch: String, + val name: String +) { + constructor(device: FBSimctlDevice): + this(device.udid, device.model, device.os, device.arch, device.name) +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceRef.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceRef.kt new file mode 100644 index 00000000..9401bd05 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceRef.kt @@ -0,0 +1,3 @@ +package com.badoo.automation.deviceserver.data + +typealias DeviceRef = String diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceState.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceState.kt new file mode 100644 index 00000000..320c825c --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceState.kt @@ -0,0 +1,14 @@ +package com.badoo.automation.deviceserver.data + +enum class DeviceState(val value: String) { + NONE("none"), + CREATING("creating"), + RESETTING("resetting"), + CREATED("created"), + REVIVING("reviving"), + FAILED("failed"); + + override fun toString(): String { + return value + } +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/ErrorDto.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/ErrorDto.kt new file mode 100644 index 00000000..b9443664 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/ErrorDto.kt @@ -0,0 +1,19 @@ +package com.badoo.automation.deviceserver.data + +data class ErrorDto( + val type: String, + val message: String?, + val stackTrace: List +) + +fun Throwable.toDto(): ErrorDto { + return ErrorDto( + type = this.javaClass.name, + message = this.message ?: "", + stackTrace = this.stackTrace + .takeWhile { + !it.className.startsWith("io.ktor") + } + .map { it.toString() } + ) +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SimulatorStatus.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SimulatorStatus.kt new file mode 100644 index 00000000..1c493046 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SimulatorStatus.kt @@ -0,0 +1,9 @@ +package com.badoo.automation.deviceserver.data + +data class SimulatorStatus( + var isReady: Boolean = false, + var wdaStatus: Boolean = false, + var fbsimctlStatus: Boolean = false, + @Volatile var wdaStatusRetries: Int = 0, + @Volatile var fbsimctlStatusRetries: Int = 0 +) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SimulatorStatusDTO.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SimulatorStatusDTO.kt new file mode 100644 index 00000000..a030d58c --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SimulatorStatusDTO.kt @@ -0,0 +1,11 @@ +package com.badoo.automation.deviceserver.data + +data class SimulatorStatusDTO ( + val ready: Boolean, + val wda_status: Boolean, + val fbsimctl_status: Boolean, + val state: String, + val last_error: ExceptionDTO? +) + +data class ExceptionDTO(val type: String, val message: String, val stackTrace: List) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DefaultSimulatorHostProvider.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DefaultSimulatorHostProvider.kt new file mode 100644 index 00000000..86e32e07 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DefaultSimulatorHostProvider.kt @@ -0,0 +1,9 @@ +package com.badoo.automation.deviceserver.host + +import java.io.File + +object DefaultSimulatorHostProvider : ISimulatorHostProvider { + override fun simulatorsNode(remote: IRemote, simulatorLimit: Int, concurrentBoots: Int, wdaPath: File): ISimulatorsNode { + return SimulatorsNode(remote, simulatorLimit = simulatorLimit, concurrentBoots = concurrentBoots, wdaPath = wdaPath) + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt new file mode 100644 index 00000000..7c94f2e4 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt @@ -0,0 +1,300 @@ +package com.badoo.automation.deviceserver.host + +import com.badoo.automation.deviceserver.LogMarkers +import com.badoo.automation.deviceserver.data.* +import com.badoo.automation.deviceserver.host.management.PortAllocator +import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException +import com.badoo.automation.deviceserver.ios.device.* +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl +import com.badoo.automation.deviceserver.ios.simulator.periodicTasksPool +import net.logstash.logback.marker.MapEntriesAppendingMarker +import org.slf4j.LoggerFactory +import java.io.File +import java.net.URL +import java.time.Duration +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Future +import java.util.concurrent.TimeUnit + +class DevicesNode( + private val remote: IRemote, + portAllocator: PortAllocator = PortAllocator(), + wdaPath: File, + knownDevices: List, + private val whitelistedApps: Set +) : ISimulatorsNode { + private val logger = LoggerFactory.getLogger(javaClass.simpleName) + private val logMarker = MapEntriesAppendingMarker( + mapOf( + LogMarkers.HOSTNAME to remote.hostName + ) + ) + + private val deviceRegistrationInterval = Duration.ofMinutes(1) + override val remoteAddress: String get() = remote.hostName + + private val deviceInfoProvider = DeviceInfoProvider(remote) + private val slots: DeviceSlots = + DeviceSlots(remote, wdaPath.absolutePath, portAllocator, deviceInfoProvider, knownDevices) + + private var deviceRegistrar: Future? = null + + private val activeRefs = ConcurrentHashMap() + + private val supportedArchitectures = listOf("arm64") + + override fun toString(): String { + return "<${this.javaClass.simpleName}: $remote>" + } + + override fun supports(desiredCaps: DesiredCapabilities): Boolean { + return desiredCaps.arch == null || supportedArchitectures.contains(desiredCaps.arch) + } + + override fun resetAsync(deviceRef: DeviceRef) { + throw(NotImplementedError("Reset is not supported by physical devices")) + } + + override fun approveAccess(deviceRef: DeviceRef, bundleId: String) { + throw(NotImplementedError("Approve Access is not supported by physical devices")) + } + + override fun clearSafariCookies(deviceRef: DeviceRef) { + throw(NotImplementedError("Clear Safari Cookies is not supported by physical devices")) + } + + override fun endpointFor(deviceRef: DeviceRef, port: Int): URL { + val device = slotByExternalRef(deviceRef).device + return device.endpointFor(port) + } + + private fun exceptionToDto(exception: Exception): ExceptionDTO { + return ExceptionDTO( + type = exception.javaClass.name, + message = exception.message ?: "", + stackTrace = exception.stackTrace.map { it.toString() } + ) + } + + override fun state(deviceRef: DeviceRef): SimulatorStatusDTO { + val device = slotByExternalRef(deviceRef).device + val status = device.status() + + return SimulatorStatusDTO( + ready = status.ready, + wda_status = status.wda_status, + fbsimctl_status = status.fbsimctl_status, + state = status.state, + last_error = if (status.last_error == null) null else exceptionToDto(status.last_error) + ) + } + + override fun isReachable(): Boolean { + return remote.isReachable() + } + + override fun count(): Int { + // FIXME: Remove from common interface, it might be needed for simulators node only + throw NotImplementedError("An operation is not implemented") + } + + override fun deleteRelease(deviceRef: DeviceRef, reason: String): Boolean { + synchronized(this) { + slotByExternalRef(deviceRef).release() + activeRefs.remove(deviceRef) + + return true + } + } + + override fun getDeviceDTO(deviceRef: DeviceRef): DeviceDTO { + val device = slotByExternalRef(deviceRef).device + return deviceToDto(deviceRef, device) + } + + override fun totalCapacity(desiredCaps: DesiredCapabilities): Int { + if (!supports(desiredCaps)) { + return 0 + } + + synchronized(this) { + return slots.totalCapacity(desiredCaps) + } + } + + override fun capacityRemaining(desiredCaps: DesiredCapabilities): Float { + if (!supports(desiredCaps)) { + return 0F + } + + synchronized(this) { + return slots.countUnusedSlots(desiredCaps).toFloat() + } + } + + override fun createDeviceAsync(desiredCaps: DesiredCapabilities): DeviceDTO { + var slot: DeviceSlot? = null + var ref: DeviceRef? = null + + synchronized(this) { + slot = slots.reserve(desiredCaps) + ref = newRef(slot!!.udid) + + activeRefs[ref!!] = slot!!.udid + } + + slot!!.device.renewAsync(whitelistedApps = whitelistedApps) + + return deviceToDto(ref!!, device = slot!!.device) + } + + override fun prepareNode() { + logger.info(logMarker, "Preparing node ${remote.hostName}") + checkPrerequisites() + if (!remote.isLocalhost()) { + copyWdaBundleToHost() + } + cleanup() + + // FIXME: We need to completely reset node state here due to changes in NodeWrapper logic + + slots.registerDevices() + + periodicTasksPool.scheduleWithFixedDelay( + { + try { + slots.registerDevices() + } catch (e: Exception) { + logger.warn(logMarker, "Failed to register devices: $e") + } + }, + deviceRegistrationInterval.toMillis(), + deviceRegistrationInterval.toMillis(), + TimeUnit.MILLISECONDS + ) + + logger.info(logMarker, "Prepared node ${remote.hostName}") + } + + override fun dispose() { + logger.info(logMarker, "Finalizing node $this") + deviceRegistrar?.cancel(true) + try { + slots.dispose() + } catch (e: Error) { + logger.error(logMarker, e.message) + } + + logger.info(logMarker, "Finalized node $this") + } + + override fun list(): List { + synchronized(this) { + val disconnected = mutableListOf() + + val list = activeRefs.map { (ref, udid) -> + val slot = slots.tryGetSlot(udid = udid) + if (slot == null) { + disconnected.add(ref) + return@map null + } else { + return@map deviceToDto(ref, slot.device) + } + } + + // TODO: replace with event driven removal of active refs on device disconnect + disconnected.forEach { ref -> activeRefs.remove(ref) } + + return list.filterNotNull().toList() + } + } + + // region diagnostics + + override fun lastCrashLog(deviceRef: DeviceRef): CrashLog { + val device = slotByExternalRef(deviceRef).device + return device.lastCrashLog() ?: CrashLog("", "") + } + + override fun videoRecordingDelete(deviceRef: DeviceRef): Unit = throw(NotImplementedError()) + + override fun videoRecordingGet(deviceRef: DeviceRef): ByteArray = throw(NotImplementedError()) + + override fun videoRecordingStart(deviceRef: DeviceRef): Unit = throw(NotImplementedError()) + + override fun videoRecordingStop(deviceRef: DeviceRef): Unit = throw(NotImplementedError()) + // endregion + + private fun deviceToDto(deviceRef: DeviceRef, device: Device): DeviceDTO { + return DeviceDTO( + ref = deviceRef, + state = device.deviceState, + fbsimctl_endpoint = device.fbsimctlEndpoint, + wda_endpoint = device.wdaEndpoint, + calabash_port = device.calabashPort, + user_ports = emptySet(), + info = device.deviceInfo, + last_error = device.lastException?.toDto(), + capabilities = ActualCapabilities( + setLocation = false, + terminateApp = false + ) + ) + } + + private fun newRef(udid: UDID): DeviceRef { + val unsafe = Regex("[^\\-_a-zA-Z\\d]") // TODO: Replace with UUID 4 + return "$udid-${remote.hostName}".replace(unsafe, "-") + } + + private fun slotByExternalRef(deviceRef: DeviceRef): DeviceSlot { + val udid = activeRefs[deviceRef] ?: throw(DeviceNotFoundException("Device $deviceRef not found")) + + return slots.getSlot(udid) + } + + private fun checkPrerequisites() { + val expectedXcodeVersions = setOf("Xcode 9.2", "Xcode 9.3") + + val xcodeVersion = remote.execIgnoringErrors(listOf("xcodebuild", "-version")) + + if (!expectedXcodeVersions.any { xcodeVersion.stdOut.contains(it) }) { + throw RuntimeException("Expecting ${expectedXcodeVersions.joinToString(", ")}, but it is $xcodeVersion") + } + + val expectedFbsimctlVersion = "HEAD-292a1bd" + + // temp solution, prerequisites should be satisfied without having to switch anything + val switchRes = remote.execIgnoringErrors( + listOf("/usr/local/bin/brew", "switch", "fbsimctl", expectedFbsimctlVersion), + env = mapOf("RUBYOPT" to "") + ) + + if (!switchRes.isSuccess) { + logger.warn(logMarker, "fbsimctl switch failed, see: $switchRes") + } + + val fbsimctlPath = remote.execIgnoringErrors(listOf("readlink", FBSimctl.FBSIMCTL_BIN)).stdOut + + val match = Regex("/fbsimctl/([-.\\w]+)/bin/fbsimctl").find(fbsimctlPath) + ?: throw RuntimeException("Could not read fbsimctl version from $fbsimctlPath") + val actualFbsimctlVersion = match.groupValues[1] + if (actualFbsimctlVersion != expectedFbsimctlVersion) { + throw RuntimeException("Expecting fbsimctl $expectedFbsimctlVersion, but it was $actualFbsimctlVersion ${match.groupValues}") + } + } + + private fun copyWdaBundleToHost() { + logger.debug(logMarker, "Setting up remote node: copying WebDriverAgent to node ${remote.hostName}") + remote.rsync( + HostFactory.WDA_DEVICE_BUNDLE.absolutePath, + HostFactory.REMOTE_WDA_DEVICE_BUNDLE_ROOT, + setOf("-r", "--delete") + ) + } + + private fun cleanup() { + // single instance of server on node is implied, so we can kill all simulators and fbsimctl processes + remote.execIgnoringErrors(listOf("pkill", "-9", "fbsimctl")) + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt new file mode 100644 index 00000000..c04810cd --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt @@ -0,0 +1,68 @@ +package com.badoo.automation.deviceserver.host + +import com.badoo.automation.deviceserver.Configuration +import com.badoo.automation.deviceserver.NodeConfig +import com.badoo.automation.deviceserver.host.management.IHostFactory +import org.slf4j.LoggerFactory +import java.io.File + +class HostFactory( + private val remoteProvider: (hostName: String, userName: String) -> IRemote = { hostName, userName -> Remote(hostName, userName) }, + private val simulatorHostProvider: ISimulatorHostProvider = DefaultSimulatorHostProvider +) : IHostFactory { + companion object { + val WDA_BUNDLE = File(Configuration.WDA_BUNDLE_PATH).canonicalFile!! // can't be null. Configuration will blow up otherwise + val WDA_DEVICE_BUNDLE = File(Configuration.WDA_DEVICE_BUNDLE_PATH).canonicalFile!! + val WDA_XCTEST = File("PlugIns/WebDriverAgentRunner.xctest") + const val REMOTE_WDA_BUNDLE_ROOT = "/tmp/web_driver_agent/" + const val REMOTE_WDA_DEVICE_BUNDLE_ROOT = "/tmp/web_driver_agent_devices/" + } + + private val logger = LoggerFactory.getLogger(javaClass.simpleName) + + override fun getHostFromConfig(config: NodeConfig): ISimulatorsNode { + logger.info("Trying to start node $config.") + + val hostName = config.host + val userName = config.user + val remote: IRemote = remoteProvider(hostName, userName) + + if (userName.isBlank() && !remote.isLocalhost()) { + throw RuntimeException("Config for non-localhost nodes must have non-empty 'user'. Current config: $config") + } + + if (config.type == NodeConfig.NodeType.Simulators) { + return simulatorHostProvider.simulatorsNode( + remote, + config.simulatorLimit, + config.concurrentBoots, + getWdaSimulatorsPath(remote.isLocalhost()) + ) + } else { + return DevicesNode( + remote, + whitelistedApps = config.whitelistApps, + wdaPath = getWdaDevicesPath(remote.isLocalhost()), + knownDevices = config.knownDevices + ) + } + } + + private fun getWdaSimulatorsPath(isLocalhost: Boolean): File { + return if (isLocalhost) { + File(WDA_BUNDLE, WDA_XCTEST.path) + } else { + val xcTestPath = File(WDA_BUNDLE.name, WDA_XCTEST.path).path + File(REMOTE_WDA_BUNDLE_ROOT, xcTestPath) + } + } + + private fun getWdaDevicesPath(isLocalhost: Boolean): File { + return if (isLocalhost) { + File(WDA_DEVICE_BUNDLE, WDA_XCTEST.path) + } else { + val xcTestPath = File(WDA_DEVICE_BUNDLE.name, WDA_XCTEST.path).path + File(REMOTE_WDA_DEVICE_BUNDLE_ROOT, xcTestPath) + } + } +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt new file mode 100644 index 00000000..8ea19171 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt @@ -0,0 +1,48 @@ +package com.badoo.automation.deviceserver.host + +import com.badoo.automation.deviceserver.command.CommandResult +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl +import java.io.File + +interface IRemote { + companion object { + private const val LOCALHOST = "localhost" + private const val LOCALHOST_NET_PREFIX = "127." + fun isLocalhost(hostName: String, userName: String): Boolean { + if (userName.isNotBlank()) { + return false // Use ssh if user was specified explicitly + } + + return hostName == LOCALHOST || hostName.startsWith(LOCALHOST_NET_PREFIX) + } + } + + val hostName: String + val userName: String + fun isReachable(): Boolean + fun isLocalhost(): Boolean = isLocalhost(hostName, userName) + + fun execIgnoringErrors(command: List, env: Map = emptyMap(), timeOutSeconds: Long = 30): CommandResult + = exec(command, env, returnFailure = true, timeOutSeconds = timeOutSeconds) + + fun exec(command: List, env: Map, returnFailure: Boolean, timeOutSeconds: Long): CommandResult + + fun shell(command: String, returnOnFailure: Boolean = true) : CommandResult + + /** + * Returns [CommandResult] file contents would be in [CommandResult.stdOutBytes] + * //FIXME: should be a better way of streaming a file over HTTP. without caching bytes in server's memory. Investigate ByteReadChannel + */ + fun captureFile(file: File): CommandResult + + fun pkill(matchString: String) + + /** + * Sends command to FBSimctl and expects JSON back from FBSimctl, + * + * @return Set> parsed JSON + */ + val fbsimctl: FBSimctl + fun isDirectory(path: String): Boolean + fun rsync(from: String, to: String, flags: Set) +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorFactory.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorFactory.kt new file mode 100644 index 00000000..f0597280 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorFactory.kt @@ -0,0 +1,25 @@ +package com.badoo.automation.deviceserver.host + +import com.badoo.automation.deviceserver.data.DeviceAllocatedPorts +import com.badoo.automation.deviceserver.data.DeviceInfo +import com.badoo.automation.deviceserver.data.DeviceRef +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDevice +import com.badoo.automation.deviceserver.ios.simulator.ISimulator +import com.badoo.automation.deviceserver.ios.simulator.Simulator +import kotlinx.coroutines.experimental.ThreadPoolDispatcher + +interface ISimulatorFactory { + fun newSimulator( + ref: DeviceRef, + remote: IRemote, + fbdev: FBSimctlDevice, + ports: DeviceAllocatedPorts, + deviceSetPath: String, + wdaPath: String, + concurrentBoot: ThreadPoolDispatcher, + headless: Boolean, + fbsimctlSubject: String + ): ISimulator { + return Simulator(ref, remote, DeviceInfo(fbdev), ports, deviceSetPath, wdaPath, concurrentBoot, headless, fbsimctlSubject) + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorHostProvider.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorHostProvider.kt new file mode 100644 index 00000000..bb132697 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorHostProvider.kt @@ -0,0 +1,12 @@ +package com.badoo.automation.deviceserver.host + +import java.io.File + +interface ISimulatorHostProvider { + fun simulatorsNode( + remote: IRemote, + simulatorLimit: Int, + concurrentBoots: Int, + wdaPath: File + ): ISimulatorsNode +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorProvider.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorProvider.kt new file mode 100644 index 00000000..f532723c --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorProvider.kt @@ -0,0 +1,11 @@ +package com.badoo.automation.deviceserver.host + +import com.badoo.automation.deviceserver.data.DesiredCapabilities +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDevice + +interface ISimulatorProvider { + fun findBy(udid: String): FBSimctlDevice? + fun list(): List + fun create(model: String?, os: String?, transitional: Boolean): FBSimctlDevice + fun match(desiredCaps: DesiredCapabilities, usedUdids: Set): FBSimctlDevice? +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt new file mode 100644 index 00000000..ce0571dd --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt @@ -0,0 +1,33 @@ +package com.badoo.automation.deviceserver.host + +import com.badoo.automation.deviceserver.data.* +import java.net.URL + +interface ISimulatorsNode { + fun supports(desiredCaps: DesiredCapabilities): Boolean + + fun resetAsync(deviceRef: DeviceRef) + + fun approveAccess(deviceRef: DeviceRef, bundleId: String) + fun clearSafariCookies(deviceRef: DeviceRef) + fun endpointFor(deviceRef: DeviceRef, port: Int): URL + fun lastCrashLog(deviceRef: DeviceRef): CrashLog + fun state(deviceRef: DeviceRef): SimulatorStatusDTO + + fun videoRecordingDelete(deviceRef: DeviceRef) + fun videoRecordingGet(deviceRef: DeviceRef): ByteArray + fun videoRecordingStart(deviceRef: DeviceRef) + fun videoRecordingStop(deviceRef: DeviceRef) + + val remoteAddress: String + fun isReachable(): Boolean + fun prepareNode() + fun count(): Int + fun list(): List + fun deleteRelease(deviceRef: DeviceRef, reason: String): Boolean + fun getDeviceDTO(deviceRef: DeviceRef): DeviceDTO + fun totalCapacity(desiredCaps: DesiredCapabilities): Int + fun capacityRemaining(desiredCaps: DesiredCapabilities): Float + fun createDeviceAsync(desiredCaps: DesiredCapabilities): DeviceDTO + fun dispose() +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt new file mode 100644 index 00000000..818dbec1 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt @@ -0,0 +1,131 @@ +package com.badoo.automation.deviceserver.host + +import com.badoo.automation.deviceserver.LogMarkers +import com.badoo.automation.deviceserver.command.* +import com.badoo.automation.deviceserver.host.IRemote.Companion.isLocalhost +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlResponseParser +import com.badoo.automation.deviceserver.util.ensure +import net.logstash.logback.marker.MapEntriesAppendingMarker +import org.slf4j.LoggerFactory +import java.io.File +import java.time.Duration + +class Remote( + override val hostName: String, + override val userName: String, + private val localExecutor: IShellCommand = ShellCommand(), + private val remoteExecutor: IShellCommand = getRemoteCommandExecutor(hostName, userName), + override val fbsimctl: FBSimctl = FBSimctl(remoteExecutor, FBSimctlResponseParser()) +) : IRemote { + companion object { + const val SSH_AUTH_SOCK = "SSH_AUTH_SOCK" + private const val INITIAL_BUFFER_SIZE = 10 * 1024 * 1024 //FIXME: looks arbitrary. taken as an average of video file sizes + + fun getRemoteCommandExecutor(hostName: String, userName: String, isInteractiveShell: Boolean = false): IShellCommand { + return if (isLocalhost(hostName, userName)) { + ShellCommand(commonEnvironment = mapOf("HOME" to System.getProperty("user.home"))) + } else { + RemoteShellCommand(hostName, userName, isInteractiveShell = isInteractiveShell) + } + } + } + + private val logger = LoggerFactory.getLogger(javaClass.simpleName) + private val logMarker = MapEntriesAppendingMarker(mapOf( + LogMarkers.HOSTNAME to hostName + )) + private val userAtHost = if (userName.isBlank()) hostName else "$userName@$hostName" + + override fun toString(): String = "" + + override fun isReachable(): Boolean { + //FIXME: We need a reliable way to determine if node is available. SSH request might just time-out if node is under heavy load. + var attempts = 3 + + do { + attempts-- + + if (isReachableBySSH()) { + return true + } + + } while (attempts > 0) + + logger.error(logMarker, "Node $hostName is NOT reachable by SSH.") + return false + } + + private fun isReachableBySSH(): Boolean { + try { + return remoteExecutor.exec(listOf("echo", "1"), returnFailure = true).isSuccess + } catch (e: SshConnectionException) { + return false + } + } + + override fun exec(command: List, env: Map, returnFailure: Boolean, timeOutSeconds: Long): CommandResult { + return remoteExecutor.exec(command, env, returnFailure = returnFailure, timeOut = Duration.ofSeconds(timeOutSeconds)) + } + + override fun shell(command: String, returnOnFailure: Boolean): CommandResult { + val cmd = when { + isLocalhost() -> listOf("bash", "-c", command) + else -> listOf("bash", "-c", ShellUtils.escape(command)) // workaround for how ssh executor is designed + } + + return remoteExecutor.exec(cmd, emptyMap(), returnFailure = returnOnFailure) + } + + //FIXME: should be a better way of streaming a file over HTTP. without caching bytes in server's memory. Investigating ByteReadChannel + override fun captureFile(file: File): CommandResult { + return remoteExecutor.exec( + listOf("cat", file.absolutePath), + returnFailure = true, + processListener = ShellCommandListener(INITIAL_BUFFER_SIZE) //FIXME: probably better to send just size, not the listener + ) + } + + override fun pkill(matchString: String) { + execIgnoringErrors(listOf("pkill", "-9", "-f", matchString)) + } + + override fun isDirectory(path: String): Boolean { + return remoteExecutor.exec(listOf("test", "-d", path), mapOf(), returnFailure = true).isSuccess + } + + override fun rsync(from: String, to: String, flags: Set) { + val cmd = mutableListOf("/usr/bin/rsync") + val rsyncFlags = mutableSetOf("--archive", "--partial") + rsyncFlags.addAll(flags) + + cmd.addAll(rsyncFlags) + cmd.add(from) + cmd.add("$userAtHost:$to") + + val env = environmentForRsync() + + logger.debug(logMarker, "Executing rsync command: ${cmd.joinToString(" ")}") + var result = localExecutor.exec(cmd, env) + + if (!result.isSuccess) { + logger.warn(logMarker, "Executing second time rsync command: ${cmd.joinToString(" ")}") + result = localExecutor.exec(cmd) + } + + ensure(result.isSuccess) { + logger.error(logMarker, "Executing rsync command failed. Result: [$result]") + RuntimeException("Remote $cmd failed with $result") + } + } + + private fun environmentForRsync(): MutableMap { + val env = mutableMapOf() + val sshAuthSocket = System.getenv(SSH_AUTH_SOCK) + + if (sshAuthSocket != null) { + env[SSH_AUTH_SOCK] = sshAuthSocket + } + return env + } +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorProvider.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorProvider.kt new file mode 100644 index 00000000..dda652d0 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorProvider.kt @@ -0,0 +1,44 @@ +package com.badoo.automation.deviceserver.host + +import com.badoo.automation.deviceserver.data.DesiredCapabilities +import com.badoo.automation.deviceserver.host.management.DesiredCapabilitiesMatcher +import com.badoo.automation.deviceserver.host.management.IDesiredCapabilitiesMatcher +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDevice + +class SimulatorProvider( + val remote: IRemote, + private val desiredCapsMatcher: IDesiredCapabilitiesMatcher = DesiredCapabilitiesMatcher() +) : ISimulatorProvider { + private var cache: List = emptyList() + + override fun match(desiredCaps: DesiredCapabilities, usedUdids: Set): FBSimctlDevice? { + val matchList = + when { + desiredCaps.udid != null -> listOfNotNull(findBy(desiredCaps.udid)) + desiredCaps.existing -> list().filter { desiredCapsMatcher.isMatch(it, desiredCaps) } + else -> return create(desiredCaps.model, desiredCaps.os, true) + } + + val firstMatch = matchList.find { !usedUdids.contains(it.udid) } + + if (firstMatch != null) return firstMatch + + return create(desiredCaps.model, desiredCaps.os, false) + } + + override fun findBy(udid: String): FBSimctlDevice? { + return remote.fbsimctl.listDevice(udid) + } + + override fun list(): List { + if (cache.isEmpty()) { + cache = remote.fbsimctl.listSimulators().filter { !it.model.isBlank() && !it.os.isBlank() } + } + return cache + } + + override fun create(model: String?, os: String?, transitional: Boolean): FBSimctlDevice { + cache = emptyList() + return remote.fbsimctl.create(model, os, transitional) + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt new file mode 100644 index 00000000..cf904261 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt @@ -0,0 +1,220 @@ +package com.badoo.automation.deviceserver.host + +import com.badoo.automation.deviceserver.LogMarkers.Companion.DEVICE_REF +import com.badoo.automation.deviceserver.LogMarkers.Companion.HOSTNAME +import com.badoo.automation.deviceserver.LogMarkers.Companion.UDID +import com.badoo.automation.deviceserver.data.* +import com.badoo.automation.deviceserver.host.management.ISimulatorHostChecker +import com.badoo.automation.deviceserver.host.management.PortAllocator +import com.badoo.automation.deviceserver.host.management.SimulatorHostChecker +import com.badoo.automation.deviceserver.host.management.errors.OverCapacityException +import com.badoo.automation.deviceserver.ios.simulator.ISimulator +import com.badoo.automation.deviceserver.ios.simulator.simulatorsThreadPool +import kotlinx.coroutines.experimental.launch +import kotlinx.coroutines.experimental.newFixedThreadPoolContext +import kotlinx.coroutines.experimental.runBlocking +import net.logstash.logback.marker.MapEntriesAppendingMarker +import org.slf4j.LoggerFactory +import java.io.File +import java.net.URL +import java.util.concurrent.ConcurrentHashMap + +class SimulatorsNode( + val remote: IRemote, + private val hostChecker: ISimulatorHostChecker = SimulatorHostChecker(remote), + private val simulatorLimit: Int, + concurrentBoots: Int, + private val wdaPath: File, + private val simulatorProvider: ISimulatorProvider = SimulatorProvider(remote), + private val portAllocator: PortAllocator = PortAllocator(), + private val simulatorFactory: ISimulatorFactory = object : ISimulatorFactory {} +) : ISimulatorsNode { + + override val remoteAddress: String get() = remote.hostName + + private val logger = LoggerFactory.getLogger(javaClass.simpleName) + private val logMarker = MapEntriesAppendingMarker(mapOf( + HOSTNAME to remote.hostName + )) + + override fun prepareNode() { + logger.info(logMarker, "Preparing node ${remote.hostName}") + hostChecker.checkPrerequisites() + + if (!remote.isLocalhost()) { + hostChecker.copyWdaBundleToHost() + } + hostChecker.cleanup() + hostChecker.setupHost() + logger.info(logMarker, "Prepared node ${remote.hostName}") + } + + private val supportedArchitectures = listOf("x86_64") + private val deviceSetPath: String by lazy { remote.fbsimctl.listDeviceSets() } + private val concurrentBoot = newFixedThreadPoolContext(concurrentBoots, "sim_boot_${remote.hostName}") + + private fun getDeviceFor(ref: DeviceRef): ISimulator { + return devicePool[ref]!! //FIXME: replace with explicit unwrapping + } + + private val devicePool = ConcurrentHashMap() + private val allocatedPorts = HashMap() + + override fun createDeviceAsync(desiredCaps: DesiredCapabilities): DeviceDTO { + synchronized(this) { // FIXME: synchronize in some other place? + if (devicePool.size >= simulatorLimit) { + val message = "$this was asked for a newSimulator, but is already at capacity $simulatorLimit" + logger.error(logMarker, message) + throw OverCapacityException(message) + } + + val usedUdids = devicePool.map { it.value.udid }.toSet() + val fbSimctlDevice = simulatorProvider.match(desiredCaps, usedUdids) + if (fbSimctlDevice == null) { + val message = "$this could not construct or match a simulator for $desiredCaps" + logger.error(logMarker, message) + throw RuntimeException(message) + } + + val ref = newRef(fbSimctlDevice.udid) + val ports = portAllocator.allocateDAP() + allocatedPorts[ref] = ports + + val simLogMarker = MapEntriesAppendingMarker(mapOf( + HOSTNAME to remote.hostName, + UDID to fbSimctlDevice.udid, + DEVICE_REF to ref + )) + + logger.debug(simLogMarker, "Will create simulator $ref") + + val simulator = simulatorFactory.newSimulator(ref, remote, fbSimctlDevice, ports, deviceSetPath, wdaPath.path, concurrentBoot, desiredCaps.headless, fbSimctlDevice.toString()) + simulator.prepareAsync() + devicePool[ref] = simulator + + logger.debug(simLogMarker, "Created simulator $ref") + + return simulatorToDTO(simulator) + } + } + + private fun simulatorToDTO(device: ISimulator): DeviceDTO { + with(device) { + return DeviceDTO( + ref, + state, + fbsimctlEndpoint, + wdaEndpoint, + calabashPort, + device.userPorts.toSet(), + device.info, + device.lastError?.toDto(), + capabilities = ActualCapabilities( + setLocation = true, + terminateApp = true + ) + ) + } + } + + private fun newRef(udid: String): DeviceRef = "$udid-${remote.hostName}".replace(Regex("[^-\\w]"), "-") + + override fun approveAccess(deviceRef: DeviceRef, bundleId: String) { + getDeviceFor(deviceRef).approveAccess(bundleId) + } + + override fun capacityRemaining(desiredCaps: DesiredCapabilities): Float { + return (simulatorLimit - count()) * 1F / simulatorLimit + } + + override fun clearSafariCookies(deviceRef: DeviceRef) { + getDeviceFor(deviceRef).clearSafariCookies() + } + + override fun count(): Int = devicePool.size + + override fun dispose() { + logger.info(logMarker, "Finalising simulator pool for ${remote.hostName}") + + val disposeJobs = devicePool.map { + launch(context = simulatorsThreadPool) { + try { + it.value.release("Finalising pool for ${remote.hostName}") + } catch (e: Throwable) { + logger.error(logMarker, "While releasing '${it.key}' for ${remote.hostName}: $e") + } + } + } + + runBlocking { + disposeJobs.forEach { it.join() } + } + + hostChecker.killDiskCleanupThread() + + logger.info(logMarker, "Finalised simulator pool for ${remote.hostName}") + } + + override fun endpointFor(deviceRef: DeviceRef, port: Int): URL { + return getDeviceFor(deviceRef).endpointFor(port) + } + + override fun getDeviceDTO(deviceRef: DeviceRef): DeviceDTO { + return simulatorToDTO(getDeviceFor(deviceRef)) + } + + override fun lastCrashLog(deviceRef: DeviceRef): CrashLog { + return getDeviceFor(deviceRef).lastCrashLog() + } + + override fun list(): List { + return devicePool.map { simulatorToDTO(it.value) } + } + + override fun isReachable(): Boolean = remote.isReachable() + + override fun deleteRelease(deviceRef: DeviceRef, reason: String): Boolean { + val iSimulator = devicePool[deviceRef] ?: return false + iSimulator.release("deleteRelease $reason $deviceRef") + devicePool.remove(deviceRef) + val entries = allocatedPorts[deviceRef] ?: return true + portAllocator.deallocateDAP(entries) + return true + } + + override fun resetAsync(deviceRef: DeviceRef) { + getDeviceFor(deviceRef).resetAsync() + } + + override fun state(deviceRef: DeviceRef): SimulatorStatusDTO { + return getDeviceFor(deviceRef).status() + } + + override fun supports(desiredCaps: DesiredCapabilities): Boolean { + return desiredCaps.arch == null || supportedArchitectures.contains(desiredCaps.arch) + } + + override fun totalCapacity(desiredCaps: DesiredCapabilities): Int { + return if (supports(desiredCaps)) simulatorLimit else 0 + } + + override fun videoRecordingDelete(deviceRef: DeviceRef) { + getDeviceFor(deviceRef).videoRecorder.delete() + } + + override fun videoRecordingGet(deviceRef: DeviceRef): ByteArray { + return getDeviceFor(deviceRef).videoRecorder.getRecording() + } + + override fun videoRecordingStart(deviceRef: DeviceRef) { + getDeviceFor(deviceRef).videoRecorder.start() + } + + override fun videoRecordingStop(deviceRef: DeviceRef) { + getDeviceFor(deviceRef).videoRecorder.stop() + } + + override fun toString(): String { + return "${javaClass.simpleName} at $remoteAddress" + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DesiredCapabilitiesMatcher.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DesiredCapabilitiesMatcher.kt new file mode 100644 index 00000000..0b61bb30 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DesiredCapabilitiesMatcher.kt @@ -0,0 +1,42 @@ +package com.badoo.automation.deviceserver.host.management + +import com.badoo.automation.deviceserver.data.DesiredCapabilities +import com.badoo.automation.deviceserver.data.DeviceInfo +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDevice + +interface IDesiredCapabilitiesMatcher { + fun isMatch(actual: FBSimctlDevice, desiredCaps: DesiredCapabilities): Boolean + fun isMatch(actual: DeviceInfo, desiredCaps: DesiredCapabilities): Boolean +} + +class DesiredCapabilitiesMatcher : IDesiredCapabilitiesMatcher { + override fun isMatch(actual: DeviceInfo, desiredCaps: DesiredCapabilities): Boolean { + if (desiredCaps.udid != null) { + return desiredCaps.udid == actual.udid + } + + with(desiredCaps) { + return (model == null || model == actual.model) && (os == null || isRuntimeMatch(os, actual.os)) + } + + } + + override fun isMatch(actual: FBSimctlDevice, desiredCaps: DesiredCapabilities): Boolean { + val deviceInfo = DeviceInfo(actual) + + return isMatch(deviceInfo, desiredCaps) + } + + internal fun isRuntimeMatch(desired: String, actual: String): Boolean { + val desiredRuntime = RuntimeVersion(desired) + val actualRuntime = RuntimeVersion(actual) + + if (desiredRuntime.name != actualRuntime.name) { + return false + } + + val significantCount = desiredRuntime.fragments.count() + + return desiredRuntime.fragments.take(significantCount) == actualRuntime.fragments.take(significantCount) + } +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt new file mode 100644 index 00000000..2bdc721c --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt @@ -0,0 +1,134 @@ +package com.badoo.automation.deviceserver.host.management + +import com.badoo.automation.deviceserver.DeviceServerConfig +import com.badoo.automation.deviceserver.data.* +import com.badoo.automation.deviceserver.host.HostFactory +import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException +import com.badoo.automation.deviceserver.host.management.util.AutoreleaseLooper +import com.badoo.automation.deviceserver.ios.ActiveDevices +import com.badoo.automation.deviceserver.ios.IActiveDevices +import org.slf4j.LoggerFactory +import java.net.URL +import java.time.Duration + +private val INFINITE_DEVICE_TIMEOUT: Duration = Duration.ofSeconds(Integer.MAX_VALUE.toLong()) + +class DeviceManager( + config: DeviceServerConfig, + nodeFactory: IHostFactory = HostFactory(), + activeDevices: IActiveDevices = ActiveDevices(), + private val autoreleaseLooper: IAutoreleaseLooper = AutoreleaseLooper() +) : IDeviceManager { + private val logger = LoggerFactory.getLogger(javaClass.simpleName) + private val deviceTimeoutInSecs: Duration + private val nodeRegistry = NodeRegistry(activeDevices) + private val autoRegistrar = NodeRegistrar( + nodesConfig = config.nodes, + nodeFactory = nodeFactory, + nodeRegistry = nodeRegistry + ) + + init { + val timeoutFromConfig: Long? = config.timeouts["device"]?.toLong() + + deviceTimeoutInSecs = + if (timeoutFromConfig != null && timeoutFromConfig > 0) { + Duration.ofSeconds(timeoutFromConfig) + } else { + INFINITE_DEVICE_TIMEOUT + } + } + + fun startAutoRegisteringDevices() { + autoRegistrar.startAutoRegistering() + } + + fun launchAutoReleaseLoop() { + autoreleaseLooper.autoreleaseLoop(this) + } + + override fun getStatus(): Map { + return mapOf( + "initialized" to nodeRegistry.getInitialRegistrationComplete(), + "sessions" to listOf(nodeRegistry.activeDevices.getStatus()).toString() + ) + } + + override fun readyForRelease(): List { + return nodeRegistry.activeDevices.readyForRelease() + } + + override fun nextReleaseAtSeconds(): Long { + return nodeRegistry.activeDevices.nextReleaseAtSeconds() + } + + override fun getTotalCapacity(desiredCaps: DesiredCapabilities): Map { + return nodeRegistry.capacitiesTotal(desiredCaps) + } + + override fun getGetDeviceDTO(ref: DeviceRef): DeviceDTO { + return nodeRegistry.activeDevices.getNodeFor(ref).getDeviceDTO(ref) + } + + override fun clearSafariCookies(ref: DeviceRef) { + nodeRegistry.activeDevices.getNodeFor(ref).clearSafariCookies(ref) + } + + override fun resetAsyncDevice(ref: DeviceRef) { + nodeRegistry.activeDevices.getNodeFor(ref).resetAsync(ref) + } + + override fun approveAccess(ref: DeviceRef, bundleId: String) { + nodeRegistry.activeDevices.getNodeFor(ref).approveAccess(ref, bundleId) + } + + override fun getEndpointFor(ref: DeviceRef, port: Int): URL { + return nodeRegistry.activeDevices.getNodeFor(ref).endpointFor(ref, port) + } + + override fun getLastCrashLog(ref: DeviceRef): CrashLog { + return nodeRegistry.activeDevices.getNodeFor(ref).lastCrashLog(ref) + } + + override fun startVideo(ref: DeviceRef) { + nodeRegistry.activeDevices.getNodeFor(ref).videoRecordingStart(ref) + } + + override fun stopVideo(ref: DeviceRef) { + nodeRegistry.activeDevices.getNodeFor(ref).videoRecordingStop(ref) + } + + override fun getVideo(ref: DeviceRef): ByteArray { + return nodeRegistry.activeDevices.getNodeFor(ref).videoRecordingGet(ref) + } + + override fun deleteVideo(ref: DeviceRef) { + nodeRegistry.activeDevices.getNodeFor(ref).videoRecordingDelete(ref) + } + + override fun getDeviceState(ref: DeviceRef): SimulatorStatusDTO { + return nodeRegistry.activeDevices.getNodeFor(ref).state(ref) + } + + override fun createDeviceAsync(desiredCaps: DesiredCapabilities, userId: String?): DeviceDTO { + return nodeRegistry.createDeviceAsync(desiredCaps, deviceTimeoutInSecs, userId) + } + + override fun deleteReleaseDevice(ref: DeviceRef, reason: String) { + try { // using try-catch here not to expose tryGetNodeFor + nodeRegistry.activeDevices.releaseDevice(ref, reason) + } catch (e: DeviceNotFoundException) { + logger.warn("Skipping $ref release because no node knows about it") + return + } + } + + override fun getDeviceRefs() : List { + return nodeRegistry.activeDevices.deviceList() + } + + override fun releaseUserDevices(userId: String, reason: String) { + val devices = nodeRegistry.activeDevices.getUserDeviceRefs(userId) + nodeRegistry.activeDevices.releaseDevices(devices, reason) + } +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IAutoreleaseLooper.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IAutoreleaseLooper.kt new file mode 100644 index 00000000..a807739a --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IAutoreleaseLooper.kt @@ -0,0 +1,5 @@ +package com.badoo.automation.deviceserver.host.management + +interface IAutoreleaseLooper { + fun autoreleaseLoop(deviceManager: IDeviceManager) +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt new file mode 100644 index 00000000..f736fa5b --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt @@ -0,0 +1,26 @@ +package com.badoo.automation.deviceserver.host.management + +import com.badoo.automation.deviceserver.data.* +import java.net.URL + +interface IDeviceManager { + fun getDeviceRefs() : List + fun createDeviceAsync(desiredCaps: DesiredCapabilities, userId: String?): DeviceDTO + fun deleteReleaseDevice(ref: DeviceRef, reason: String) + fun getGetDeviceDTO(ref: DeviceRef): DeviceDTO + fun clearSafariCookies(ref: DeviceRef) + fun resetAsyncDevice(ref: DeviceRef) + fun approveAccess(ref: DeviceRef, bundleId: String) + fun getEndpointFor(ref: DeviceRef, port: Int): URL + fun getLastCrashLog(ref: DeviceRef): CrashLog + fun startVideo(ref: DeviceRef) + fun stopVideo(ref: DeviceRef) + fun getVideo(ref: DeviceRef): ByteArray + fun deleteVideo(ref: DeviceRef) + fun getDeviceState(ref: DeviceRef): SimulatorStatusDTO + fun getTotalCapacity(desiredCaps: DesiredCapabilities): Map + fun nextReleaseAtSeconds(): Long + fun readyForRelease(): List + fun getStatus(): Map + fun releaseUserDevices(userId: String, reason: String) +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IHostFactory.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IHostFactory.kt new file mode 100644 index 00000000..ae2f94b5 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IHostFactory.kt @@ -0,0 +1,8 @@ +package com.badoo.automation.deviceserver.host.management + +import com.badoo.automation.deviceserver.NodeConfig +import com.badoo.automation.deviceserver.host.ISimulatorsNode + +interface IHostFactory { + fun getHostFromConfig(config: NodeConfig): ISimulatorsNode +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt new file mode 100644 index 00000000..770623e8 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt @@ -0,0 +1,124 @@ +package com.badoo.automation.deviceserver.host.management + +import com.badoo.automation.deviceserver.LogMarkers +import com.badoo.automation.deviceserver.host.HostFactory.Companion.REMOTE_WDA_BUNDLE_ROOT +import com.badoo.automation.deviceserver.host.HostFactory.Companion.WDA_BUNDLE +import com.badoo.automation.deviceserver.host.IRemote +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl +import com.badoo.automation.deviceserver.ios.simulator.periodicTasksPool +import net.logstash.logback.marker.MapEntriesAppendingMarker +import org.slf4j.LoggerFactory +import java.time.Duration +import java.util.concurrent.ScheduledFuture +import java.util.concurrent.TimeUnit + +interface ISimulatorHostChecker { + fun checkPrerequisites() + fun cleanup() + fun setupHost() + fun killDiskCleanupThread() + fun copyWdaBundleToHost() +} + +class SimulatorHostChecker( + val remote: IRemote, + private val diskCleanupInterval: Duration = Duration.ofMinutes(15) +) : ISimulatorHostChecker { + companion object { + private const val EXPECTED_XCODE_VERSION = "Xcode 9." + private const val EXPECTED_FBSIMCTL = "HEAD-292a1bd" + } + + private val logger = LoggerFactory.getLogger(javaClass.simpleName) + private val logMarker = MapEntriesAppendingMarker(mapOf( + LogMarkers.HOSTNAME to remote.hostName + )) + + private lateinit var cleanUpTask: ScheduledFuture<*> + + override fun copyWdaBundleToHost() { + logger.debug(logMarker, "Setting up remote node: copying WebDriverAgent to node ${remote.hostName}") + remote.rsync(WDA_BUNDLE.absolutePath, REMOTE_WDA_BUNDLE_ROOT, setOf("-r", "--delete")) + } + + override fun killDiskCleanupThread() { + if (::cleanUpTask.isInitialized) { + cleanUpTask.cancel(true) + } + } + + override fun checkPrerequisites() { + val xcodeVersion = remote.execIgnoringErrors(listOf("xcodebuild", "-version")) + + if (!xcodeVersion.stdOut.contains(EXPECTED_XCODE_VERSION)) { + throw RuntimeException("Expecting $EXPECTED_XCODE_VERSION, but received $xcodeVersion") + } + + // temp solution, prereq should be satisfied without having to switch anything + val rv = remote.execIgnoringErrors(listOf("/usr/local/bin/brew", "switch", "fbsimctl", EXPECTED_FBSIMCTL), env = mapOf("RUBYOPT" to "")) + if (!rv.isSuccess) { + logger.warn(logMarker, "fbsimctl switch failed, see: $rv") + } + + val fbsimctlPath = remote.execIgnoringErrors(listOf("readlink", FBSimctl.FBSIMCTL_BIN )).stdOut + val match = Regex("/fbsimctl/([-.\\w]+)/bin/fbsimctl").find(fbsimctlPath) + ?: throw RuntimeException("Could not read fbsimctl version from $fbsimctlPath") + val actualFbsimctlVersion = match.groupValues[1] + if (actualFbsimctlVersion != EXPECTED_FBSIMCTL) { + throw RuntimeException("Expecting fbsimctl $EXPECTED_FBSIMCTL, but it was $actualFbsimctlVersion ${match.groupValues}") + } + } + + override fun cleanup() { + try { + logger.info(logMarker, "Will kill abandoned long living fbsimctl processes") + remote.pkill("fbsimctl") + logger.info(logMarker, "Will shutdown booted simulators") + remote.fbsimctl.shutdownAllBooted() + logger.info(logMarker, "Done shutting down booted simulators") + } catch (e: Exception) { + logger.warn(logMarker, "Failed to shutdown simulator because: ${e.javaClass}: message: [${e.message}]") + } + + val deviceSetsPath = remote.fbsimctl.listDeviceSets() + check(!deviceSetsPath.isBlank()) { "Device sets must not be blank" } // fbsimctl.listDeviceSets will throw if empty. but paranoid mode on. + + val caches = listOf( + "/private/var/folders/*/*/*/*-*-*/*.app", + "/private/var/folders/*/*/*/fbsimctl-*", + "$deviceSetsPath/*/data/Library/Caches/com.apple.mobile.installd.staging/*/*.app" + ) + + val cleanUpRunnable: Runnable = object : Runnable { + override fun run() { + caches.forEach { + val command = "find $it -maxdepth 0 -mmin +60 -exec rm -rf {} \\;" + val r = remote.shell(command, returnOnFailure = true) // find returns non zero if nothing found + if (!r.isSuccess || r.stdErr.isNotEmpty() || r.stdOut.isNotEmpty()) { + logger.debug(logMarker, "[disc cleaner] $this returned non-empty $r") + } + } + } + } + cleanUpTask = periodicTasksPool.scheduleWithFixedDelay( + cleanUpRunnable, + 0, + diskCleanupInterval.toSeconds(), + TimeUnit.SECONDS) + } + + override fun setupHost() { + // disable node hardware keyboard, i.e. use on-screen one + remote.execIgnoringErrors("defaults write com.apple.iphonesimulator ConnectHardwareKeyboard -bool false".split(" ")) + + // disable simulator location + remote.execIgnoringErrors("defaults write com.apple.iphonesimulator LocationMode \"3101\"".split(" ")) + remote.execIgnoringErrors("defaults write com.apple.iphonesimulator ShowChrome -bool false".split(" ")) + // other options that might be useful are: + // EnableKeyboardSync = 0; + // GraphicsQualityOverride = 10; + // OptimizeRenderingForWindowScale = 0; + // ShowChrome = 1; + // SlowMotionAnimation = 0; + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt new file mode 100644 index 00000000..fc68f653 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt @@ -0,0 +1,65 @@ +package com.badoo.automation.deviceserver.host.management + +import com.badoo.automation.deviceserver.NodeConfig +import org.slf4j.LoggerFactory +import java.time.Duration +import java.util.concurrent.Executors +import java.util.concurrent.Future + +class NodeRegistrar( + nodesConfig: List, + nodeFactory: IHostFactory, + private val nodeRegistry: NodeRegistry, + private val registrationInterval: Duration = DEFAULT_REGISTRATION_INTERVAL +) { + companion object { + val DEFAULT_REGISTRATION_INTERVAL: Duration = Duration.ofSeconds(60) + } + + private val logger = LoggerFactory.getLogger(javaClass.simpleName) + private var autoRegisteringJob: Future<*>? = null + private val nodeWrappers: List = nodesConfig.map { + NodeWrapper(it, nodeFactory, nodeRegistry) + } + + fun startAutoRegistering() { + val executor = Executors.newSingleThreadExecutor() + autoRegisteringJob = executor.submit({ + while (!Thread.currentThread().isInterrupted) { + autoRegister() + Thread.sleep(registrationInterval.toMillis()) + } + }) + executor.shutdown() + } + + fun stop() { + //FIXME: Do proper clean up on server exit + autoRegisteringJob?.cancel(true) + autoRegisteringJob = null + } + + private fun autoRegister() { + val unregistered = nodeWrappers - nodeRegistry.getAll() + logger.debug("Going to auto register ${unregistered.map(NodeWrapper::toString)}") + val executor = Executors.newFixedThreadPool(unregistered.size) + val results: List> = unregistered.map { nodeWrapper -> + executor.submit({ + nodeWrapper.stop() + if (nodeWrapper.start()) { + nodeRegistry.add(nodeWrapper) + nodeWrapper.startPeriodicHealthCheck() + } + }) + } + executor.shutdown() + results.forEach { result -> + try { + result.get() + } catch (e: Throwable) { + logger.debug("Error while starting node ") + } + } + nodeRegistry.setInitialRegistrationComplete() + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt new file mode 100644 index 00000000..9077a6bb --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt @@ -0,0 +1,87 @@ +package com.badoo.automation.deviceserver.host.management + +import com.badoo.automation.deviceserver.LogMarkers +import com.badoo.automation.deviceserver.data.DesiredCapabilities +import com.badoo.automation.deviceserver.data.DeviceDTO +import com.badoo.automation.deviceserver.host.ISimulatorsNode +import com.badoo.automation.deviceserver.host.management.errors.NoAliveNodesException +import com.badoo.automation.deviceserver.ios.ActiveDevices +import com.badoo.automation.deviceserver.ios.IActiveDevices +import com.badoo.automation.deviceserver.ios.simulator.simulatorsThreadPool +import kotlinx.coroutines.experimental.Job +import kotlinx.coroutines.experimental.launch +import kotlinx.coroutines.experimental.runBlocking +import net.logstash.logback.marker.MapEntriesAppendingMarker +import org.slf4j.LoggerFactory +import org.slf4j.Marker +import java.time.Duration +import java.util.concurrent.ConcurrentHashMap + +class NodeRegistry(val activeDevices: IActiveDevices = ActiveDevices()) { + private var initialRegistrationComplete: Boolean = false + private val nodeWrappers = ConcurrentHashMap.newKeySet() + private val logger = LoggerFactory.getLogger(javaClass.simpleName) + + fun getInitialRegistrationComplete(): Boolean = initialRegistrationComplete + + fun setInitialRegistrationComplete() { + initialRegistrationComplete = true + } + + fun add(wrapper: NodeWrapper) { + if (nodeWrappers.contains(wrapper)) { + logger.warn("${wrapper.node.remoteAddress} is already registered") + } else { + nodeWrappers.add(wrapper) + logger.info("Registered node ${wrapper.node.remoteAddress}") + } + } + + fun removeIfPresent(wrapper: NodeWrapper) { + nodeWrappers.remove(wrapper) + logger.info("Unregistered node ${wrapper.node.remoteAddress}") + activeDevices.unregisterNodeDevices(wrapper.node) + } + + fun getAll(): Set { + return nodeWrappers + } + + fun getAlive(): Set { + return nodeWrappers.filter { it.isAlive() }.toSet() + } + + fun capacitiesTotal(desiredCapabilities: DesiredCapabilities): Map { + val count = getAlive() + .map { it.node.totalCapacity(desiredCapabilities) } + .sum() + + return mapOf("total" to count) + } + + fun createDeviceAsync(desiredCapabilities: DesiredCapabilities, deviceTimeout: Duration, userId: String?): DeviceDTO { + val node: ISimulatorsNode = getAlive() + .map { wrapper -> wrapper.node } + .maxBy { node -> node.capacityRemaining(desiredCapabilities) } + ?: throw NoAliveNodesException("No alive nodes are available to create device at the moment") + + val dto = node.createDeviceAsync(desiredCapabilities) + + val logMarker: Marker = MapEntriesAppendingMarker(mutableMapOf( + LogMarkers.DEVICE_REF to dto.ref, + LogMarkers.UDID to dto.info.udid + )) + logger.info(logMarker, "Create device started, register with timeout ${deviceTimeout.seconds} secs") + + activeDevices.registerDevice(dto.ref, node, deviceTimeout, userId) + + return dto + } + + fun dispose() { + //FIXME: Do proper clean up on server exit + val list: List = nodeWrappers.map { launch(simulatorsThreadPool) { it.stop() } } + runBlocking { list.forEach { it.join() } } + nodeWrappers.clear() + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeWrapper.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeWrapper.kt new file mode 100644 index 00000000..f5301e43 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeWrapper.kt @@ -0,0 +1,113 @@ +package com.badoo.automation.deviceserver.host.management + +import com.badoo.automation.deviceserver.LogMarkers.Companion.HOSTNAME +import com.badoo.automation.deviceserver.NodeConfig +import com.badoo.automation.deviceserver.host.ISimulatorsNode +import net.logstash.logback.marker.MapEntriesAppendingMarker +import org.slf4j.LoggerFactory +import java.time.Duration +import java.util.concurrent.Executors +import java.util.concurrent.Future +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +/** + * NodeWrapper starts/stops node depending on whether node is reachable + */ +class NodeWrapper( + private val config: NodeConfig, + hostFactory: IHostFactory, + private val registry: NodeRegistry, + private val maxHealthCheckAttempts: Int = 6, + private val nodeCheckInterval: Duration = Duration.ofSeconds(60) +) { + private val logger = LoggerFactory.getLogger(javaClass.simpleName) + private val logMarker = MapEntriesAppendingMarker( + mapOf( + HOSTNAME to config.host + ) + ) + private val lock = ReentrantLock(true) + @Volatile private var isStarted = false + private var healthCheckPeriodicTask: Future<*>? = null + val node: ISimulatorsNode = hostFactory.getHostFromConfig(config) + + fun isAlive(): Boolean = isStarted && node.isReachable() + + override fun toString(): String = "NodeWrapper for ${config.host}" + + fun start(): Boolean { + lock.withLock { + if (isStarted) { + logger.error(logMarker, "The node is already started. Node config: $config") + return false + } + + if (!node.isReachable()) { + logger.error(logMarker, "Failed to start the node from config: $config. Reason: unreachable node: $node.") + return false + } + + logger.info(logMarker, "Starting the node from config: $config") + try { + node.prepareNode() + logger.info(logMarker, "Successfully started the node from config: $config") + isStarted = true + } catch (e: Exception) { + logger.error(logMarker, "Failed to start the node from config: $config", e) + } + return isStarted + } + } + + fun stop() { + lock.withLock { + if (!isStarted) { + logger.error(logMarker, "The node is not started. Node config: $config") + return + } + + logger.info(logMarker, "Stopping the node from config: $config") + stopPeriodicHealthCheck() + node.dispose() + isStarted = false + logger.info(logMarker, "Successfully stopped the node from config: $config") + } + } + + fun startPeriodicHealthCheck() { + if (!isStarted) { + throw RuntimeException("Can not start polling stopped node. Call start() on node first") + } + + val executor = Executors.newSingleThreadExecutor() + var healthCheckAttempts = 0 + healthCheckPeriodicTask = executor.submit({ + while (!Thread.currentThread().isInterrupted) { + Thread.sleep(nodeCheckInterval.toMillis()) + + if (isAlive()) { + healthCheckAttempts = 0 + } else { + healthCheckAttempts++ + logger.debug(logMarker, "Node $this is down for last $healthCheckAttempts tries") + + if (healthCheckAttempts >= maxHealthCheckAttempts) { + registry.removeIfPresent(this) + val message = + "Removing node [${node.remoteAddress}]: cannot reach the node for $maxHealthCheckAttempts tries" + logger.error(logMarker, message) + throw RuntimeException(message) + } + } + + } + }) + executor.shutdown() + } + + private fun stopPeriodicHealthCheck() { + healthCheckPeriodicTask?.cancel(true) + healthCheckPeriodicTask = null + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/PortAllocator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/PortAllocator.kt new file mode 100644 index 00000000..ee772427 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/PortAllocator.kt @@ -0,0 +1,43 @@ +package com.badoo.automation.deviceserver.host.management + +import com.badoo.automation.deviceserver.data.DeviceAllocatedPorts + +class PortAllocator(min: Int = PORT_RANGE_START, max: Int = PORT_RANGE_END) { + companion object { + // this port range seems to be free and not in conflict with zabbix, etc. + const val PORT_RANGE_START = 41798 + const val PORT_RANGE_END = 42507 + } + + private var ports: Set = IntRange(min, max).toSet() + + fun allocateDAP(): DeviceAllocatedPorts { + val take = allocate(3) + return DeviceAllocatedPorts(take[0], take[1], take[2]) + } + + fun deallocateDAP(dap: DeviceAllocatedPorts) { + deallocate(listOf(dap.calabashPort, dap.fbsimctlPort, dap.wdaPort)) + } + + fun available(): Int { + return ports.size + } + + private fun allocate(entries: Int): List { + synchronized(this) { + if (ports.size < entries) { + throw RuntimeException("No more ports to allocate") + } + val take = ports.take(entries) + ports = ports.subtract(take) + return take + } + } + + private fun deallocate(entries: List) { + synchronized(this) { + ports = ports.plus(entries) + } + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/RuntimeVersion.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/RuntimeVersion.kt new file mode 100644 index 00000000..16adc484 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/RuntimeVersion.kt @@ -0,0 +1,16 @@ +package com.badoo.automation.deviceserver.host.management + +class RuntimeVersion(runtime: String) { + var name: String + private set + + var fragments: List + private set + + init { + val (a, b) = runtime.split(' ', ignoreCase = false, limit = 2) + name = a + fragments = b.split('.').toList() + } + +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/errors/DeviceNotFoundException.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/errors/DeviceNotFoundException.kt new file mode 100644 index 00000000..a934ed39 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/errors/DeviceNotFoundException.kt @@ -0,0 +1,3 @@ +package com.badoo.automation.deviceserver.host.management.errors + +class DeviceNotFoundException(message: String): RuntimeException(message) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/errors/NoAliveNodesException.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/errors/NoAliveNodesException.kt new file mode 100644 index 00000000..fe8c45ab --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/errors/NoAliveNodesException.kt @@ -0,0 +1,3 @@ +package com.badoo.automation.deviceserver.host.management.errors + +class NoAliveNodesException(message: String): RuntimeException(message) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/errors/OverCapacityException.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/errors/OverCapacityException.kt new file mode 100644 index 00000000..b3a4a2af --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/errors/OverCapacityException.kt @@ -0,0 +1,3 @@ +package com.badoo.automation.deviceserver.host.management.errors + +class OverCapacityException(message: String): RuntimeException(message) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/util/AutoreleaseLooper.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/util/AutoreleaseLooper.kt new file mode 100644 index 00000000..31dd0fe7 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/util/AutoreleaseLooper.kt @@ -0,0 +1,56 @@ +package com.badoo.automation.deviceserver.host.management.util + +import com.badoo.automation.deviceserver.host.management.IAutoreleaseLooper +import com.badoo.automation.deviceserver.host.management.IDeviceManager +import com.badoo.automation.deviceserver.util.executeWithTimeout +import kotlinx.coroutines.experimental.delay +import kotlinx.coroutines.experimental.launch +import kotlinx.coroutines.experimental.newFixedThreadPoolContext +import kotlinx.coroutines.experimental.runBlocking +import org.slf4j.LoggerFactory +import java.time.Duration +import java.util.concurrent.TimeUnit +import kotlin.math.max + +class AutoreleaseLooper : IAutoreleaseLooper { + private val autoreleaseThreadPool = newFixedThreadPoolContext(1, "AutoreleaseLoop") + private val logger = LoggerFactory.getLogger(javaClass.simpleName) + + override fun autoreleaseLoop(deviceManager: IDeviceManager) { + launch(autoreleaseThreadPool) { + while (isActive) { + try { + autoRelease(deviceManager) + } catch (t: Throwable) { + logger.warn("Autorelease thread ignored: $t") + } + + val seconds = max(1, (deviceManager.nextReleaseAtSeconds() - currentTimeSeconds())) + delay(seconds, TimeUnit.SECONDS) + } + } + } + + private fun currentTimeSeconds() = System.currentTimeMillis() / 1000 + + private fun autoRelease(deviceManager: IDeviceManager) { + val jobs = deviceManager.readyForRelease().map { deviceRef -> + launch { + val message = "Failed to release device $deviceRef" + try { + executeWithTimeout(Duration.ofMinutes(2), message) { + if (isActive) { + deviceManager.deleteReleaseDevice(deviceRef, "autoRelease") + } + } + } catch (e: RuntimeException) { + logger.error(message, e) + } + } + } + + runBlocking { + jobs.forEach { it.join() } + } + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt new file mode 100644 index 00000000..24f39a4f --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt @@ -0,0 +1,131 @@ +package com.badoo.automation.deviceserver.ios + +import com.badoo.automation.deviceserver.data.DeviceDTO +import com.badoo.automation.deviceserver.data.DeviceRef +import com.badoo.automation.deviceserver.host.ISimulatorsNode +import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException +import org.slf4j.LoggerFactory +import java.time.Duration +import java.util.concurrent.ConcurrentHashMap + +data class SessionEntry( + val ref: DeviceRef, + val node: ISimulatorsNode, + var updatedAtSeconds: Long, + val releaseTimeout: Duration, + val userId: String? +) + +class ActiveDevices( + private val sessionId: String = "defaultSessionId", + private val currentTimeSeconds: ()->Long = ::currentTimeSecondsProvider +) : IActiveDevices { + private val devices: MutableMap = ConcurrentHashMap() + private val logger = LoggerFactory.getLogger(javaClass.simpleName) + + companion object { + private val DEFAULT_RELEASE_TIMEOUT: Duration = Duration.ofSeconds(600) + fun currentTimeSecondsProvider(): Long = System.currentTimeMillis() / 1000 + } + + override fun deviceRefs(): Set { + return devices.keys + } + + override fun deviceList(): List { + return devices.map { + it.value.node.getDeviceDTO(it.value.ref) + } + } + + override fun registerDevice(ref: DeviceRef, node: ISimulatorsNode, releaseTimeout: Duration, userId: String?) { + devices[ref] = SessionEntry(ref, node, currentTimeSeconds(), releaseTimeout, userId) + } + + override fun unregisterNodeDevices(node: ISimulatorsNode) { + devices.entries + .filter { it.value.node == node } + .forEach { unregisterDeleteDevice(it.key) } + } + + private fun refreshSessionEntry(sessionEntry: SessionEntry) { + sessionEntry.updatedAtSeconds = currentTimeSeconds() + } + + private fun tryGetNodeFor(ref: DeviceRef): ISimulatorsNode? { + val sessionEntry = devices[ref] + if (sessionEntry != null) { + refreshSessionEntry(sessionEntry) + return sessionEntry.node + } else { + return null + } + } + + override fun getNodeFor(ref: DeviceRef): ISimulatorsNode { + val node = tryGetNodeFor(ref) + if (node == null) { + throw DeviceNotFoundException("Device [$ref] not found in [$sessionId] activeDevices") + } else { + return node + } + } + + override fun getStatus(): String { + return devices + .map { "\n" + it.key to it.value } + .toString() + } + + override fun unregisterDeleteDevice(ref: DeviceRef) { + devices.remove(ref) + } + + override fun readyForRelease(): List { + val secondsNow = currentTimeSeconds() + return devices.filter { with(it.value) { releaseTimeout.seconds + updatedAtSeconds <= secondsNow } } + .map { it.key } + .also { logger.info("Ready to release $it"); } + } + + override fun nextReleaseAtSeconds(): Long { + val sessionEntry = devices.minBy { + it.value.updatedAtSeconds + it.value.releaseTimeout.seconds + } + + val nextReleaseAtSeconds: Long + if (sessionEntry != null) { + nextReleaseAtSeconds = sessionEntry.value.updatedAtSeconds + sessionEntry.value.releaseTimeout.seconds + } else { + nextReleaseAtSeconds = currentTimeSeconds() + DEFAULT_RELEASE_TIMEOUT.seconds + } + + logger.info("nextReleaseAtSeconds = $nextReleaseAtSeconds seconds") + return nextReleaseAtSeconds + } + + override fun releaseDevice(ref: DeviceRef, reason: String) { + val session = sessionByRef(ref) + session.node.deleteRelease(session.ref, reason) + unregisterDeleteDevice(session.ref) + } + + override fun releaseDevices(entries: List, reason: String) { + entries.parallelStream().forEach { + try { + releaseDevice(it, reason) + } catch (e: RuntimeException) { + logger.warn("Failed to release device $it", e) + } + } + } + + override fun getUserDeviceRefs(userId: String): List { + return devices.filter { it.value.userId == userId }.map { it.key } + } + + private fun sessionByRef(ref: String): SessionEntry { + return devices[ref] ?: throw DeviceNotFoundException("Device [$ref] not found in active devices") + } + +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/DeviceStatus.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/DeviceStatus.kt new file mode 100644 index 00000000..172edb75 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/DeviceStatus.kt @@ -0,0 +1,9 @@ +package com.badoo.automation.deviceserver.ios + +data class DeviceStatus ( + val ready: Boolean, + val wda_status: Boolean, + val fbsimctl_status: Boolean, + val state: String, + val last_error: Exception? +) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/IActiveDevices.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/IActiveDevices.kt new file mode 100644 index 00000000..f8a51616 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/IActiveDevices.kt @@ -0,0 +1,21 @@ +package com.badoo.automation.deviceserver.ios + +import com.badoo.automation.deviceserver.data.DeviceDTO +import com.badoo.automation.deviceserver.data.DeviceRef +import com.badoo.automation.deviceserver.host.ISimulatorsNode +import java.time.Duration + +interface IActiveDevices { + fun deviceRefs(): Set + fun registerDevice(ref: DeviceRef, node: ISimulatorsNode, releaseTimeout: Duration, userId: String?) + fun getNodeFor(ref: DeviceRef): ISimulatorsNode + fun unregisterDeleteDevice(ref: DeviceRef) + fun readyForRelease(): List + fun nextReleaseAtSeconds(): Long + fun unregisterNodeDevices(node: ISimulatorsNode) + fun getStatus(): String + fun deviceList(): List + fun releaseDevice(ref: DeviceRef, reason: String) + fun getUserDeviceRefs(userId: String) : List + fun releaseDevices(entries: List, reason: String) +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/WdaClient.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/WdaClient.kt new file mode 100644 index 00000000..bf6402d0 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/WdaClient.kt @@ -0,0 +1,113 @@ +package com.badoo.automation.deviceserver.ios + +import com.badoo.automation.deviceserver.JsonMapper +import com.fasterxml.jackson.databind.JsonNode +import okhttp3.* +import java.net.URL +import java.time.Duration +import java.util.concurrent.TimeUnit +import kotlin.test.currentStackTrace + +class WdaClient( + private val commandExecutor: URL, + private var sessionId: String? = null, + openTimeout: Duration = Duration.ofSeconds(5), + readTimeout: Duration = Duration.ofSeconds(5) +) { + class WdaException(message: String): RuntimeException(message) + + private val client: OkHttpClient = OkHttpClient.Builder() + .connectTimeout(openTimeout.toMillis(), TimeUnit.MILLISECONDS) + .readTimeout(readTimeout.toMillis(), TimeUnit.MILLISECONDS) + .build() + + fun attachToSession() { + val rv = get("status") + raiseForWdStatus(rv) + + sessionId = rv["sessionId"].textValue() + } + + fun dismissAlert(): Boolean { + raiseIfNoSession() + + val rv = post("session/$sessionId/alert/dismiss", emptyMap()) + + return when { + rv["status"].intValue() == 0 -> true + rv["status"].intValue() == 27 -> false + else -> { + raiseForWdStatus(rv) + throw WdaException("Unexpected state $rv") + } + } + } + + fun alertText(): String? { + raiseIfNoSession() + + val rv = get("session/$sessionId/alert/text") + + return when { + rv["status"].intValue() == 0 -> rv["value"].textValue() + rv["status"].intValue() == 27 -> null + else -> { + raiseForWdStatus(rv) + throw WdaException("Unexpected state $rv") + } + } + } + + private fun get(path: String): JsonNode { + val url = URL(commandExecutor, path) + val request = Request.Builder() + .addHeader("Content-Type", "application/json") + .url(url) + .build() + + val response = client.newCall(request).execute() + + raiseForHttpStatus(response) + + return JsonMapper().readTree(response.body()!!.byteStream()) + } + + private fun post(path: String, params: Map): JsonNode { + val url = URL(commandExecutor, path) + val payload = JsonMapper().toJson(params) + + val request = Request.Builder() + .addHeader("Content-Type", "application/json") + .post(RequestBody.create(mediaType, payload)) + .url(url) + .build() + + val response = client.newCall(request).execute() + + raiseForHttpStatus(response) + + return JsonMapper().readTree(response.body()!!.byteStream()) + + } + + private val mediaType = MediaType.parse("application/json; charset=utf-8") + + private fun raiseIfNoSession() { + if (sessionId == null) { + + throw WdaException("${currentStackTrace()[1].methodName} requires session") + } + } + + private fun raiseForHttpStatus(response: Response) { + if (response.isSuccessful) return + + throw WdaException("WDA error ${response.code()}: ${response.body()!!.string()}") + } + + private fun raiseForWdStatus(json: JsonNode) { + if (json["status"].intValue() != 0) { + throw WdaException("WebDriver returned non zero status ${json["status"]}: ${json["value"]}") + } + } +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt new file mode 100644 index 00000000..b7812e51 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt @@ -0,0 +1,338 @@ +package com.badoo.automation.deviceserver.ios.device + +import com.badoo.automation.deviceserver.LogMarkers +import com.badoo.automation.deviceserver.WaitTimeoutError +import com.badoo.automation.deviceserver.data.* +import com.badoo.automation.deviceserver.host.IRemote +import com.badoo.automation.deviceserver.ios.DeviceStatus +import com.badoo.automation.deviceserver.ios.WdaClient +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDeviceState +import com.badoo.automation.deviceserver.ios.proc.WebDriverAgentError +import com.badoo.automation.deviceserver.util.executeWithTimeout +import com.badoo.automation.deviceserver.util.pollFor +import net.logstash.logback.marker.MapEntriesAppendingMarker +import org.slf4j.LoggerFactory +import org.slf4j.Marker +import java.net.URI +import java.net.URL +import java.time.Duration +import java.util.concurrent.Executors +import java.util.concurrent.Future + +class Device( + private val remote: IRemote, + val deviceInfo: DeviceInfo, + val allocatedPorts: DeviceAllocatedPorts, + wdaPath: String, + private val ipAddress: String +) { + val udid: String = deviceInfo.udid + val fbsimctlEndpoint = URI("http://${remote.hostName}:${allocatedPorts.fbsimctlPort}/$udid/") + val wdaEndpoint = URI("http://$ipAddress:$WDA_PORT") + val calabashPort = CALABASH_PORT + + @Volatile + var lastException: Exception? = null + private set + + @Volatile + var deviceState = DeviceState.NONE + private set(value) { + val oldState = field + field = value + logger.debug(logMarker, "$this $oldState -> $value") + } + + private val fbsimctlProc: DeviceFbsimctlProc = DeviceFbsimctlProc(remote, deviceInfo.udid, fbsimctlEndpoint, false) + private val wdaProc = DeviceWebDriverAgent(remote, wdaPath, deviceInfo.udid, wdaEndpoint) + + private val status = SimulatorStatus() + + private val logger = LoggerFactory.getLogger(javaClass.simpleName) + private val logMarker: Marker = MapEntriesAppendingMarker( + mapOf( + LogMarkers.UDID to udid, + LogMarkers.HOSTNAME to remote.hostName + ) + ) + + private var preparePromise: Future<*>? = null + private var renewPromise: Future<*>? = null + + init { + if (udid.isEmpty()) { + throw IllegalArgumentException("udid cannot be empty") + } + } + + override fun toString(): String = "" + + fun status(): DeviceStatus { + refreshStatus() + + return DeviceStatus( + ready = status.isReady, + state = deviceState.value, // FIXME: why get rid of type here + wda_status = status.wdaStatus, + fbsimctl_status = status.fbsimctlStatus, + last_error = lastException + ) + } + + private fun refreshStatus() { + val previousFbSimctlStatus = status.fbsimctlStatus + + status.isReady = false + status.fbsimctlStatus = false + status.wdaStatus = false + + if (deviceState != DeviceState.CREATED) { + return + } + + val currentState = remote.fbsimctl.listDevice(udid)?.state + val isBooted = currentState == FBSimctlDeviceState.BOOTED.value + + if (!isBooted) { + return + } + + val wdaStatus = wdaProc.isHealthy() + val fbsimctlStatus = fbsimctlProc.isHealthy() + + // check if WDA or fbsimctl crashed after being ok for some time + + if (wdaStatus) { + status.wdaStatusRetries = 0 + } else { + status.wdaStatusRetries += 1 + } + + val maxAttempts = 3 + + if (status.wdaStatusRetries >= maxAttempts) { + deviceState = DeviceState.FAILED + val message = "${this} WebDriverAgent crashed. Last $maxAttempts health checks failed" + logger.error(logMarker, message) + lastException = RuntimeException(message) + } + + if (previousFbSimctlStatus && !fbsimctlStatus) { + deviceState = DeviceState.FAILED + val message = "${this} fbsimctl crashed" + logger.error(logMarker, message) + lastException = RuntimeException(message) + } + + status.isReady = isBooted && wdaStatus && fbsimctlStatus + status.fbsimctlStatus = fbsimctlStatus + status.wdaStatus = wdaStatus + } + + fun endpointFor(port: Int): URL { + val ports = setOf(WDA_PORT, CALABASH_PORT) + + require(ports.contains(port)) { "Port $port is not in user ports range $ports" } + + return URL("http://$ipAddress:$port/") + } + + fun dispose() { + renewPromise?.cancel(true) + preparePromise?.cancel(true) + + logger.debug("Disposing device $this") + disposeResources() + } + + private fun disposeResources() { + ignoringDisposeErrors { fbsimctlProc.kill() } + ignoringDisposeErrors { wdaProc.kill() } + } + + private fun ignoringDisposeErrors(action: () -> Unit?) { + try { + action() + } catch (e: Throwable) { // FIXME: RuntimeError, SystemCallError in Ruby + logger.warn(logMarker, "Ignoring $this release error: $e") + } + } + + fun lastCrashLog(): CrashLog? { + // TODO unlike for simulators, crash logs for physical devices are not at $HOME/Library/Logs/DiagnosticReports + return null + } + + private fun executeAsync(action: () -> Unit?): Future<*>? { + val executor = Executors.newSingleThreadExecutor() + val future = executor.submit(action) + executor.shutdown() + + return future + } + + fun prepareAsync() { + if (preparePromise != null) { + return + } + + deviceState = DeviceState.NONE + + preparePromise = executeAsync { + try { + prepare() + } catch (e: Exception) { + deviceState = DeviceState.FAILED + lastException = e + logger.error(logMarker, e.message) + } + } + } + + fun renewAsync(whitelistedApps: Set) { + val currentStatus = status() + var prepareRequired = false + + if (currentStatus.state == DeviceState.FAILED.value) { + prepareRequired = true + deviceState = DeviceState.REVIVING + logger.warn(logMarker, "$this failed, will try to revive") + } + + renewPromise = executeAsync { + try { + if (currentStatus.wda_status) { + try { + ensureNoAlerts(maxAttempts = 3) + } catch (e: Exception) { + logger.warn(logMarker, "Ensuring alerts on $this ignored error $e") + } + } + + uninstallUserApps(whitelistedApps = whitelistedApps) + + if (prepareRequired) { + preparePromise?.cancel(true) + preparePromise = null + prepareAsync() + } + } catch (e: Exception) { + deviceState = DeviceState.FAILED + lastException = e + logger.error(logMarker, e.message) + } finally { + renewPromise = null + } + } + } + + private fun ensureNoAlerts(maxAttempts: Int) { + val client = WdaClient(commandExecutor = wdaEndpoint.toURL()) + client.attachToSession() + + for (attempt in 1..maxAttempts) { + val alertText = client.alertText() ?: return + + logger.debug(logMarker, "Will dismiss alert $alertText") + client.dismissAlert() + Thread.sleep(1000) + } + } + + private fun uninstallUserApps(whitelistedApps: Set) { + logger.debug(logMarker, "About to uninstall user apps on $this") + val listApps = remote.fbsimctl.listApps(udid) + val userApps = listApps.filter { it.install_type == "user" }.map { it.bundle.bundle_id } + val bundlesToUninstall = userApps.toSet() - whitelistedApps + logger.info(logMarker, "Uninstalling user apps: $bundlesToUninstall") + + bundlesToUninstall.forEach { + remote.fbsimctl.uninstallApp(udid, it) + } + logger.debug(logMarker, "Uninstalled user apps on $this") + } + + private fun prepare(timeout: Duration = PREPARE_TIMEOUT) { + lastException = null + status.wdaStatus = false + status.fbsimctlStatus = false + + logger.info(logMarker, "Starting to prepare $this") + + fbsimctlProc.kill() + wdaProc.kill() + + executeWithTimeout(timeout, name = "Preparing devices") { + startFbsimctl() + startWdaWithRetry() + + logger.info(logMarker, "Finished preparing $this") + deviceState = DeviceState.CREATED + } + + } + + private fun startFbsimctl() { + logger.info(logMarker, "Starting fbsimctl on $this") + + fbsimctlProc.kill() + fbsimctlProc.start() + + pollFor( + Duration.ofSeconds(10), + reasonName = "$this Fbsimctl health check", + retryInterval = Duration.ofSeconds(1), + logger = logger, + marker = logMarker + ) { + fbsimctlProc.isHealthy() + } + } + + private fun startWda() { + wdaProc.kill() + wdaProc.start() + + pollFor( + Duration.ofMinutes(1), + reasonName = "$this WebDriverAgent health check", + retryInterval = Duration.ofSeconds(2), + logger = logger, + marker = logMarker + ) { + if (wdaProc.isProcessAlive) { + wdaProc.isHealthy() + } else { + throw WaitTimeoutError("WebDriverAgent process is not alive") + } + } + } + + private fun startWdaWithRetry() { + status.wdaStatusRetries = 0 + + val maxRetries = 3 + + for (attempt in 1..maxRetries) { + try { + logger.info(logMarker, "Starting WebDriverAgent on $this. Attempt $attempt/$maxRetries") + startWda() + + break + } catch (e: Exception) { + if (e is WebDriverAgentError || e is WaitTimeoutError) { + logger.warn(logMarker, "Attempt $attempt to start WebDriverAgent for ${this} timed out: $e") + if (attempt == maxRetries) { + throw e + } + } else throw e + } + } + } + + private companion object { + private const val CALABASH_PORT = 37265 + private const val WDA_PORT = 8100 + private val PREPARE_TIMEOUT = Duration.ofMinutes(4) + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceFbsimctlProc.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceFbsimctlProc.kt new file mode 100644 index 00000000..c9283826 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceFbsimctlProc.kt @@ -0,0 +1,36 @@ +package com.badoo.automation.deviceserver.ios.device + +import com.badoo.automation.deviceserver.command.ChildProcess +import com.badoo.automation.deviceserver.host.IRemote +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl +import com.badoo.automation.deviceserver.ios.proc.FbsimctlProc +import java.net.URI + +class DeviceFbsimctlProc( + remote: IRemote, + udid: String, + fbsimctlEndpoint: URI, + headless: Boolean, + childFactory: ( + remoteHost: String, + username: String, + cmd: List, + isInteractiveShell: Boolean, + out_reader: (line: String) -> Unit, + err_reader: (line: String) -> Unit + ) -> ChildProcess = ChildProcess.Companion::fromCommand +) : FbsimctlProc(remote, udid, fbsimctlEndpoint, headless, childFactory) { + + override fun getFbsimctlCommand(headless: Boolean): List { + + return listOf( + FBSimctl.FBSIMCTL_BIN, + FBSimctl.RESPONSE_FORMAT, + udid, + "listen", + "--http", + fbsimctlEndpoint.port.toString() + ) + } + +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceInfoProvider.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceInfoProvider.kt new file mode 100644 index 00000000..3a227d71 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceInfoProvider.kt @@ -0,0 +1,10 @@ +package com.badoo.automation.deviceserver.ios.device + +import com.badoo.automation.deviceserver.data.DeviceInfo +import com.badoo.automation.deviceserver.host.IRemote + +class DeviceInfoProvider(private val remote: IRemote) { + fun list(): List { + return remote.fbsimctl.listDevices().map { DeviceInfo(it) } + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlot.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlot.kt new file mode 100644 index 00000000..2a1d4032 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlot.kt @@ -0,0 +1,19 @@ +package com.badoo.automation.deviceserver.ios.device + +class DeviceSlot(val device: Device) { + private var reserved = false + + val udid get() = device.udid + + fun reserve() { + reserved = true + } + + fun release() { + reserved = false + } + + fun isReserved(): Boolean { + return reserved + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlots.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlots.kt new file mode 100644 index 00000000..65d30017 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlots.kt @@ -0,0 +1,175 @@ +package com.badoo.automation.deviceserver.ios.device + +import com.badoo.automation.deviceserver.LogMarkers +import com.badoo.automation.deviceserver.data.DesiredCapabilities +import com.badoo.automation.deviceserver.data.DeviceInfo +import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.host.IRemote +import com.badoo.automation.deviceserver.host.management.DesiredCapabilitiesMatcher +import com.badoo.automation.deviceserver.host.management.PortAllocator +import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException +import com.badoo.automation.deviceserver.host.management.errors.OverCapacityException +import net.logstash.logback.marker.MapEntriesAppendingMarker +import org.slf4j.LoggerFactory +import org.slf4j.Marker +import java.util.concurrent.ConcurrentLinkedQueue + +class DeviceSlots( + val remote: IRemote, + val wdaPath: String, + private val portAllocator: PortAllocator, + private val deviceInfoProvider: DeviceInfoProvider, + knownDevicesList: List +) { + private val activeSlots = mutableListOf() + + private val dcMatcher = DesiredCapabilitiesMatcher() + + private val removedSlots = ConcurrentLinkedQueue() + + private val knownDevices = knownDevicesList.map { it.udid to it }.toMap() + + private val logger = LoggerFactory.getLogger(javaClass.simpleName) + private val logMarker: Marker = MapEntriesAppendingMarker( + mapOf( + LogMarkers.HOSTNAME to remote.hostName + ) + ) + + fun registerDevices() { + val connectedDevices = deviceInfoProvider.list() + val knownConnectedDevices = connectedDevices.filter { knownDevices.containsKey(it.udid) } + + val diff = diff(knownConnectedDevices) + + if (diff.removed.isNotEmpty()) { + logger.info(logMarker, "Will remove ${diff.removed} devices") + diff.removed.forEach { + removeSlotBy(it) + } + } + + if (diff.added.isNotEmpty()) { + logger.info(logMarker, "Will add ${diff.added} devices") + knownConnectedDevices.filter { diff.added.contains(it.udid) }.forEach { + addSlot(it) + } + } + } + + fun getSlot(udid: UDID): DeviceSlot { + val slot = tryGetSlot(udid = udid) + + if (slot == null) { + if (removedSlots.any { it.udid == udid }) { + throw(DeviceNotFoundException("Device $udid was removed because it disconnected")) + } else { + throw(DeviceNotFoundException("Device $udid not found")) + } + } + + return slot + } + + fun tryGetSlot(udid: UDID): DeviceSlot? { + return activeSlots.firstOrNull { it.udid == udid } + } + + fun reserve(desiredCapabilities: DesiredCapabilities): DeviceSlot { + val slot = availableSlots(desiredCapabilities = desiredCapabilities).firstOrNull() + + if (slot == null) { + val unused = activeSlots.filter { !it.isReserved() }.map { it.device.deviceInfo }.toList() + throw OverCapacityException("No unused devices matched $desiredCapabilities. Unused devices: $unused") + } + + slot.reserve() + return slot + } + + fun release(slotUdid: UDID) { + val slot = getSlot(udid = slotUdid) + slot.release() + } + + fun countUnusedSlots(desiredCapabilities: DesiredCapabilities): Int { + return availableSlots(desiredCapabilities = desiredCapabilities).size + } + + fun totalCapacity(desiredCapabilities: DesiredCapabilities): Int { + return activeSlots.count { dcMatcher.isMatch(it.device.deviceInfo, desiredCapabilities) } + } + + fun dispose() { + activeSlots.forEach { + it.device.dispose() + } + activeSlots.clear() + } + + private fun availableSlots(desiredCapabilities: DesiredCapabilities): List { + return activeSlots.filter { + !it.isReserved() && dcMatcher.isMatch(it.device.deviceInfo, desiredCapabilities) + }.toList() + } + + private data class Diff(val added: Set, val removed: Set) + + private fun diff(deviceInfos: List): Diff { + val current = activeSlots.map { it.udid }.toSet() + val new = deviceInfos.map { it.udid }.toSet() + + val added = new - current + val removed = current - new + + return Diff(added, removed) + } + + private fun addSlot(deviceInfo: DeviceInfo) { + val udid = deviceInfo.udid + + if (activeSlots.any { it.udid == udid }) { + throw RuntimeException("Device $udid is already registered") + } + + val allocatedPorts = portAllocator.allocateDAP() + + val device = Device( + remote =remote, + deviceInfo = deviceInfo, + allocatedPorts = allocatedPorts, + wdaPath = wdaPath, + ipAddress = knownDevices.getValue(udid).ipAddress + ) + + device.prepareAsync() + + val slot = DeviceSlot(device = device) + activeSlots.add(slot) + } + + private fun removeSlotBy(udid: UDID) { + val slot = activeSlots.find { it.udid == udid } + + if (slot == null) { + throw DeviceNotFoundException("Device $udid is already unregistered") + } + + removeSlot(slot) + } + + private fun removeSlot(slot: DeviceSlot) { + val allocatedPorts = slot.device.allocatedPorts + + slot.device.dispose() + portAllocator.deallocateDAP(allocatedPorts) + activeSlots.remove(slot) + + removedSlots.add(RemovedSlot(slot.udid)) + if (removedSlots.size > 1000) { + removedSlots.remove() + } + } + + private data class RemovedSlot(val udid: UDID) +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceWebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceWebDriverAgent.kt new file mode 100644 index 00000000..f1f349b7 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceWebDriverAgent.kt @@ -0,0 +1,26 @@ +package com.badoo.automation.deviceserver.ios.device + +import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.host.IRemote +import com.badoo.automation.deviceserver.ios.proc.WebDriverAgent +import java.net.URI +import java.nio.file.Paths + +class DeviceWebDriverAgent( + remote: IRemote, + wdaPath: String, + udid: UDID, + wdaEndpoint: URI, + hostApp: String = Paths.get(wdaPath).parent.parent.toString() +) : WebDriverAgent( + remote = remote, + wdaPath = wdaPath, + hostApp = hostApp, + udid = udid, + wdaEndpoint = wdaEndpoint +) { + override fun terminateHostApp() { + remote.fbsimctl.uninstallApp(udid, "com.apple.test.WebDriverAgentRunner-Runner") + Thread.sleep(1000) + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/KnownDevice.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/KnownDevice.kt new file mode 100644 index 00000000..08a01029 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/KnownDevice.kt @@ -0,0 +1,14 @@ +package com.badoo.automation.deviceserver.ios.device + +import com.badoo.automation.deviceserver.data.UDID +import com.fasterxml.jackson.annotation.JsonIgnoreProperties +import com.fasterxml.jackson.annotation.JsonProperty + +@JsonIgnoreProperties(value = ["wifi_address"]) +data class KnownDevice( + @JsonProperty("udid") + val udid: UDID, + + @JsonProperty("ip") + val ipAddress: String +) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt new file mode 100644 index 00000000..d2856891 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt @@ -0,0 +1,156 @@ +package com.badoo.automation.deviceserver.ios.fbsimctl + +import com.badoo.automation.deviceserver.command.CommandResult +import com.badoo.automation.deviceserver.command.IShellCommand +import com.badoo.automation.deviceserver.command.RemoteShellCommand +import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.util.ensure +import org.slf4j.LoggerFactory +import java.time.Duration + +class FBSimctl( + private val shellCommand: IShellCommand, + private val parser: IFBSimctlResponseParser = FBSimctlResponseParser() +) : IFBSimctl { + companion object { + private val SIMULATOR_SHUTDOWN_TIMEOUT: Duration = Duration.ofSeconds(60) + const val FBSIMCTL_BIN = "/usr/local/bin/fbsimctl" + const val RESPONSE_FORMAT = "--json" + private val NEW_LINE = System.lineSeparator()!! + } + + private val logger = LoggerFactory.getLogger(javaClass.simpleName) + + override fun listSimulators(): List { + val cmd = listOf("--simulators", "list") + return parser.parseDeviceList(fbsimctl(cmd, raiseOnError = false)) + } + + override fun listDevices(): List { + val cmd = listOf("--devices", "list") + // TODO: Untrusted devices will have empty name, model, os, etc. Consider rejecting them + return parser.parseDeviceList(fbsimctl(cmd, raiseOnError = false)) + } + + override fun listDevice(udid: UDID): FBSimctlDevice? { + val devices = parser.parseDeviceList(fbsimctl("list", udid, raiseOnError = false)) + + return if (devices.isEmpty()) { + null + } else { + devices.first() + } + } + + override fun listApps(udid: UDID): List = parser.parseApplicationsList(fbsimctl(cmd = "list_apps", udid = udid)) + + /** + * returns path to device sets + * E.g. "/Users/qa/Library/Developer/CoreSimulator/Devices" + */ + override fun listDeviceSets(): String { + val result = fbsimctl("list_device_sets", jsonFormat = false) + val deviceSets = result + .split(NEW_LINE) + .findLast { it.startsWith("/") } + + if (deviceSets == null || deviceSets.isBlank()) { + throw FBSimctlError("No device_sets returned by fbsimctl") + } + + return deviceSets + } + + override fun eraseSimulator(udid: UDID) = fbsimctl(cmd = "erase", udid = udid) + + override fun create(model: String?, os: String?, transitional: Boolean): FBSimctlDevice { + val args = mutableListOf("create") + + if (os != null) { args.add("'$os'") } + if (model != null) { args.add("'$model'") } + + val result = fbsimctl(args) + return parser.parseDeviceCreation(result, transitional) + } + + override fun diagnose(udid: UDID): FBSimctlDeviceDiagnosticInfo { + return parser.parseDiagnosticInfo(fbsimctl(cmd = "diagnose", udid = udid)) + } + + override fun shutdown(udid: UDID) { + fbsimctl("shutdown", udid, timeOut = SIMULATOR_SHUTDOWN_TIMEOUT, raiseOnError = false) + } + + override fun shutdownAllBooted() = fbsimctl("--simulators --state=booted shutdown") + + override fun delete(udid: UDID) = fbsimctl("delete", udid) + + override fun terminateApp(udid: UDID, bundleId: String, raiseOnError: Boolean) + = fbsimctl(listOf("terminate", bundleId), udid, raiseOnError = raiseOnError) + + override fun uninstallApp(udid: UDID, bundleId: String) { + fbsimctl(listOf("uninstall", bundleId), udid, raiseOnError = true) + } + + //region private fun + private fun fbsimctl(cmd: String, udid: UDID? = null, jsonFormat: Boolean = true, timeOut: Duration = Duration.ofSeconds(30), + raiseOnError: Boolean = true, isWarning: (stdOut: String) -> Boolean = { false }) + = fbsimctl( + cmd.split(" "), + udid, + jsonFormat, + timeOut = timeOut, + raiseOnError = raiseOnError, + isWarning = isWarning + ) + + private fun fbsimctl(cmd: List, udid: UDID? = null, jsonFormat: Boolean = true, timeOut: Duration = Duration.ofSeconds(30), + raiseOnError: Boolean = false, isWarning: (stdOut: String) -> Boolean = { false }): String { + + val fbsimctlCommand = buildFbsimctlCommand(jsonFormat, udid, cmd) + + var result = executeCommand(fbsimctlCommand, timeOut) + + if (result.exitCode == 255 && shellCommand is RemoteShellCommand) { // SSH_CONNECT_ERROR, but not necessary + result = executeCommand(fbsimctlCommand, timeOut) //FIXME: NOT a good place and not a good thing to do + } + + if (raiseOnError) { + val (warnings, errors) = filterWarnings(result.stdOut, isWarning) + warnings.forEach { logger.warn("Ignoring fbsimctl errors: ${it["subject"]}") } + ensure(errors.isEmpty()) { FBSimctlError("fbsimctl failed: $errors", null) } + if (result.exitCode != 0) { + throw FBSimctlError("Error while running command: ≤${fbsimctlCommand.joinToString(" ")}≥ " + + "Exit code:: [${result.exitCode}], stderr:: [${result.stdErr}] stdout:: [${result.stdOut}]", null) + } + } + + return result.stdOut.trim() // remove last new_line + } + + private fun executeCommand(fbsimctlCommand: ArrayList, timeOut: Duration): CommandResult { + return shellCommand.exec(fbsimctlCommand, timeOut = timeOut) + } + + private fun buildFbsimctlCommand(jsonFormat: Boolean, udid: UDID?, command: List): ArrayList { + val cmd = arrayListOf() + cmd.add(FBSIMCTL_BIN) + + if (jsonFormat) { + cmd.add(RESPONSE_FORMAT) + } + + if (udid != null) { + cmd.add(udid) + } + + cmd.addAll(command) + return cmd + } + + private fun filterWarnings(out: String, isWarning: (stdOut: String) -> Boolean) + = parser.parse(out) + .filter { it["event_name"] == "failure" } + .partition { isWarning(it["subject"] as String) } + //endregion +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlDTO.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlDTO.kt new file mode 100644 index 00000000..0c06f527 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlDTO.kt @@ -0,0 +1,37 @@ +package com.badoo.automation.deviceserver.ios.fbsimctl + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties + +@JsonIgnoreProperties(ignoreUnknown = true) +data class FBSimctlDevice( + val arch: String = "", + val state: String, + val model: String = "", + val name: String = "", + val udid: String, + val os: String = "" +) + +data class FBSimctlAppInfo( + val data_container: String?, + val bundle: FBSimctlAppInfoBundle, + val install_type: String? +) + +data class FBSimctlAppInfoBundle( + val path: String?, + val bundle_id: String, + val binary: Map?, + val name: String? +) + +enum class FBSimctlDeviceState(val value: String) { + BOOTED("Booted"), + SHUTDOWN("Shutdown") +} + +data class FBSimctlDeviceDiagnosticInfo( + val sysLogLocation: String?, + val coreSimulatorLogLocation: String?, + val videoLocation: String? +) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlErrors.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlErrors.kt new file mode 100644 index 00000000..9b7a917d --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlErrors.kt @@ -0,0 +1,5 @@ +package com.badoo.automation.deviceserver.ios.fbsimctl + +open class FBSimctlError(message: String?, cause: Throwable? = null) : IllegalStateException(message, cause) + +class FBSimctlResponseParseError(message: String?, cause: Throwable? = null) : FBSimctlError(message, cause) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlResponseParser.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlResponseParser.kt new file mode 100644 index 00000000..e5174705 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlResponseParser.kt @@ -0,0 +1,138 @@ +package com.badoo.automation.deviceserver.ios.fbsimctl + +import com.badoo.automation.deviceserver.JsonMapper +import com.fasterxml.jackson.core.JsonProcessingException + +class FBSimctlResponseParser : IFBSimctlResponseParser { + /** + * Generic response parsing + */ + override fun parse(response: String): List> { + val mapper = JsonMapper() + return response.lines() + .filter { isDiscreteEvent(it) } + .map { mapper.fromJson>(it) } + } + + /** + * Parses device list + */ + override fun parseDeviceList(response: String): List + = response.lines() + .filter { isDiscreteEvent(it) } + .filter { isOfEventName(it, "list") } + .map { fromJson(it, FBSimctlDeviceListResponse::class.java) } + .map { it.subject } + + override fun parseDeviceListHttp(response: String): FBSimctlDevice + = fromJson(response, FBSimctlDeviceListHttpResponse::class.java) + .subject.first().subject + + /** + * Parses applications list + */ + override fun parseApplicationsList(response: String): List + = response.lines() + .filter { isDiscreteEvent(it) } + .filter { isOfEventName(it, "list_apps") } + .map { fromJson(it, FBSimctlAppListResponse::class.java) } + .map { it.subject }.flatten() + + override fun parseDiagnosticInfo(response: String): FBSimctlDeviceDiagnosticInfo { + val result = parse(response) + + val sysLog = getFileLocation(result, "system_log") + val coreSimulatorLog = getFileLocation(result, "coresimulator") + val videoRecording = getFileLocation(result, "video") + + return FBSimctlDeviceDiagnosticInfo( + sysLogLocation = sysLog, + coreSimulatorLogLocation = coreSimulatorLog, + videoLocation = videoRecording + ) + } + + override fun parseDeviceCreation(response: String, isTransitional: Boolean): FBSimctlDevice { + val parsedResponse: FBSimctlCreateDeviceResponse? + + try { + parsedResponse = response.lines() + .filter { isOfEventName(it, "create") } + .filter { isEnded(it) } + .map { fromJson(it, FBSimctlCreateDeviceResponse::class.java) } + .firstOrNull { it.event_name == "create" && it.event_type == "ended" } + } catch (e: RuntimeException) { + throw FBSimctlResponseParseError("Failed to parse 'device create' response [$response]", e) + } + + if (parsedResponse == null) { + throw FBSimctlError("Failed to parse 'device create' response: [$response]") + } + + return parsedResponse.subject + } + + private fun getFileLocation(result: List>, fileType: String): String? { + val found = result + .map { + @Suppress("UNCHECKED_CAST") + it["subject"] as Map + } + .find { it["short_name"] == fileType } + + return found?.get("location") + } + //region private fun and stuff + + /** + * Filter junk lines with other events + */ + private fun isDiscreteEvent(it: String) = it.contains(":\"discrete\"") + + // + /** + * Filter events by name + * FIXME: quick workaround added to skip extraneous events and prevent parser from failing, + * refactor parsing so that it determines class based on event_name or skips non expected class events + */ + private fun isOfEventName(it: String, name: String) = it.contains(":\"$name\"") + + private fun isEnded(it: String) = it.contains("\"event_type\":\"ended\"") + + private fun fromJson(string: String, clazz: Class): T { + try { + return JsonMapper().fromJson(string, clazz) + } catch (e: JsonProcessingException) { + throw FBSimctlResponseParseError("Failed to parse fbsimctl response. " + + "Please check maybe response format has changed. DTO class: [${clazz.name}]. FBSimctl response: [$string]", e) + } + } + + private data class FBSimctlAppListResponse( + val event_name: String, + val timestamp: String, + val subject: List, + val event_type: String + ) + + private data class FBSimctlDeviceListResponse( + val event_name: String, + val timestamp: String, + val subject: FBSimctlDevice, + val event_type: String + ) + + private data class FBSimctlDeviceListHttpResponse( + val status: String, + val subject: List, + val events: List + ) + + private data class FBSimctlCreateDeviceResponse( + val event_name: String, + val timestamp: String, + val subject: FBSimctlDevice, + val event_type: String + ) + //endregion +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctl.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctl.kt new file mode 100644 index 00000000..e6bc02d1 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctl.kt @@ -0,0 +1,35 @@ +package com.badoo.automation.deviceserver.ios.fbsimctl + +import com.badoo.automation.deviceserver.data.UDID + +interface IFBSimctl { + /** + * List simulators + */ + fun listSimulators(): List + + /** + * List physical devices + */ + fun listDevices(): List + + /** + * List simulator or physical device matching specified udid + */ + fun listDevice(udid: UDID): FBSimctlDevice? + fun listApps(udid: UDID): List + /** + * returns path to device sets + * E.g. "/Users/qa/Library/Developer/CoreSimulator/Devices" + */ + fun listDeviceSets(): String + + fun eraseSimulator(udid: UDID): String + fun create(model: String?, os: String?, transitional: Boolean): FBSimctlDevice + fun diagnose(udid: UDID): FBSimctlDeviceDiagnosticInfo + fun shutdown(udid: UDID) + fun shutdownAllBooted(): String + fun delete(udid: UDID): String + fun terminateApp(udid: UDID, bundleId: String, raiseOnError: Boolean = false): String + fun uninstallApp(udid: UDID, bundleId: String) +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctlResponseParser.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctlResponseParser.kt new file mode 100644 index 00000000..19d1247b --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctlResponseParser.kt @@ -0,0 +1,10 @@ +package com.badoo.automation.deviceserver.ios.fbsimctl + +interface IFBSimctlResponseParser { + fun parseDeviceList(response: String): List + fun parseApplicationsList(response: String): List + fun parseDiagnosticInfo(response: String): FBSimctlDeviceDiagnosticInfo + fun parseDeviceCreation(response: String, isTransitional: Boolean): FBSimctlDevice + fun parse(response: String): List> + fun parseDeviceListHttp(response: String): FBSimctlDevice +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt new file mode 100644 index 00000000..4d40d2f4 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt @@ -0,0 +1,81 @@ +package com.badoo.automation.deviceserver.ios.proc + +import com.badoo.automation.deviceserver.command.ChildProcess +import com.badoo.automation.deviceserver.host.IRemote +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl +import com.badoo.automation.deviceserver.util.ensure +import com.badoo.automation.deviceserver.util.uriWithPath +import java.net.URI + +open class FbsimctlProc( + private val remote: IRemote, + protected val udid: String, + protected val fbsimctlEndpoint: URI, + val headless: Boolean, + private val childFactory: ( + remoteHost: String, + username: String, + cmd: List, + isInteractiveShell: Boolean, + out_reader: (line: String) -> Unit, + err_reader: (line: String) -> Unit + ) -> ChildProcess = ChildProcess.Companion::fromCommand +) : LongRunningProc(udid, remote.hostName) { + private val uri: URI = uriWithPath(fbsimctlEndpoint, "list") + + override fun toString(): String = "<$udid at ${remote.hostName}:${fbsimctlEndpoint.port}>" + + override fun start() { + ensure(childProcess == null) { FbsimctlProcError("Previous fbsimctl process $childProcess has not been killed") } + logger.debug(logMarker, "$this — Starting child process") + + childProcess = childFactory( + remote.hostName, + remote.userName, + getFbsimctlCommand(headless), + true, + { logger.info(logMarker, "${this@FbsimctlProc}: FbSimCtl : ${it.trim()}") }, + { logger.warn(logMarker, "${this@FbsimctlProc}: FbSimCtl : ${it.trim()}") } + ) + + logger.debug(logMarker, "$this FBSimCtl: $childProcess") + } + + override fun checkHealth(): Boolean { + return try { + val result = client.get(uri.toURL()) + + return if (result.isSuccess) { + true + } else { + logger.debug(logMarker, "Failed fbsimctl health check. Response: $result") + false + } + } catch (e: RuntimeException) { + logger.warn(logMarker, "Failed to determine fbsimctl device state. Exception: $e") + false + } + } + + protected open fun getFbsimctlCommand(headless: Boolean): List { + val cmd = mutableListOf( + FBSimctl.FBSIMCTL_BIN, + FBSimctl.RESPONSE_FORMAT, + udid, + "boot" + ) + + if (headless) { + cmd.add("--direct-launch") + } + + cmd.addAll(listOf( + "--", + "listen", + "--http", + fbsimctlEndpoint.port.toString() + )) + + return cmd + } +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProcError.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProcError.kt new file mode 100644 index 00000000..93f88b6c --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProcError.kt @@ -0,0 +1,5 @@ +package com.badoo.automation.deviceserver.ios.proc + +import java.lang.IllegalStateException + +class FbsimctlProcError(message: String) : IllegalStateException(message) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/ILongRunningProc.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/ILongRunningProc.kt new file mode 100644 index 00000000..d71cae53 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/ILongRunningProc.kt @@ -0,0 +1,11 @@ +package com.badoo.automation.deviceserver.ios.proc + +/** + * Long running process. E.g. {@see FbsimctlProc} + */ +interface ILongRunningProc { + val isProcessAlive: Boolean + fun start() + fun isHealthy(): Boolean + fun kill() +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProc.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProc.kt new file mode 100644 index 00000000..fa6faaee --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProc.kt @@ -0,0 +1,40 @@ +package com.badoo.automation.deviceserver.ios.proc + +import com.badoo.automation.deviceserver.LogMarkers +import com.badoo.automation.deviceserver.command.ChildProcess +import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.util.CustomHttpClient +import net.logstash.logback.marker.MapEntriesAppendingMarker +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import java.time.Duration + +abstract class LongRunningProc(udid: UDID, remoteHostName: String) : ILongRunningProc { + private val checkInterval: Duration = Duration.ofSeconds(10) + private val checkAttempts: Int = 6 + protected val logger: Logger = LoggerFactory.getLogger(javaClass.simpleName) + protected val logMarker = MapEntriesAppendingMarker(mapOf( + LogMarkers.HOSTNAME to remoteHostName, + LogMarkers.UDID to udid + )) + @Volatile protected var childProcess: ChildProcess? = null + override val isProcessAlive: Boolean get() = true == childProcess?.isAlive() + + override fun kill() { + if (childProcess == null) { + return + } + + logger.debug(logMarker, "$this — Killing child process $childProcess") + childProcess!!.kill() + childProcess = null + } + + override fun isHealthy(): Boolean { + return isProcessAlive && checkHealth() + } + + protected val client: CustomHttpClient = CustomHttpClient() + + protected abstract fun checkHealth(): Boolean +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProcessListener.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProcessListener.kt new file mode 100644 index 00000000..9d491515 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProcessListener.kt @@ -0,0 +1,94 @@ +package com.badoo.automation.deviceserver.ios.proc + +import com.zaxxer.nuprocess.NuProcess +import com.zaxxer.nuprocess.codec.NuAbstractCharsetHandler +import org.slf4j.LoggerFactory +import java.nio.ByteBuffer +import java.nio.CharBuffer +import java.nio.charset.CoderResult +import java.nio.charset.StandardCharsets.UTF_8 +import java.time.Duration +import java.util.concurrent.TimeUnit + +class LongRunningProcessListener( + private val outReader: (line: String) -> Unit, + private val errReader: (line: String) -> Unit +) : NuAbstractCharsetHandler(UTF_8.newEncoder(), UTF_8.newDecoder(), UTF_8.newDecoder()) { + private val logger = LoggerFactory.getLogger(javaClass.simpleName) + private val errStringBuilder = StringBuilder(BUFFER_SIZE) + private val outStringBuilder = StringBuilder(BUFFER_SIZE) + private lateinit var nuProcess: NuProcess + + val isAlive: Boolean get() = nuProcess.isRunning + val pid: Int get() = nuProcess.pid + var exitCode: Int = Int.MIN_VALUE + private set + + fun writeStdin(string: String) { + nuProcess.writeStdin(ByteBuffer.wrap(string.toByteArray())) + } + + fun destroy(force: Boolean, timeOut: Duration = Duration.ofSeconds(1)): Int { + try { + nuProcess.destroy(force) + } catch (e: RuntimeException) { + // destroy throws exception when it failed to send the signal to process (RuntimeException) + logger.debug("Exception while terminating process $this", e) + } + + return nuProcess.waitFor(timeOut.toMillis(), TimeUnit.MILLISECONDS) + } + + override fun onPreStart(nuProcess: NuProcess) { + this.nuProcess = nuProcess + super.onPreStart(nuProcess) + } + + override fun onExit(exitCode: Int) { + this.exitCode = exitCode + super.onExit(exitCode) + } + + //region read out and err + override fun onStderrChars(buffer: CharBuffer?, closed: Boolean, coderResult: CoderResult?) { + fetchOutput(buffer, closed, errStringBuilder, errReader) + super.onStderrChars(buffer, closed, coderResult) + } + + override fun onStdoutChars(buffer: CharBuffer?, closed: Boolean, coderResult: CoderResult?) { + fetchOutput(buffer, closed, outStringBuilder, outReader) + super.onStdoutChars(buffer, closed, coderResult) + } + + private fun fetchOutput( + buffer: CharBuffer?, + closed: Boolean, + stringBuilder: StringBuilder, + readerFunction: (string: String) -> Unit + ) { + if (buffer != null && buffer.hasRemaining()) { + val chars = CharArray(buffer.remaining()) + buffer.get(chars) // writes from buffer to char array + + for (c in chars) { // in case output is multi-line + if (NEW_LINE == c) { + readerFunction(stringBuilder.toString()) + stringBuilder.setLength(0) // clear + } else { + stringBuilder.append(c) + } + } + + if (closed && stringBuilder.isNotEmpty()) { // in case no new line at the end + readerFunction(stringBuilder.toString()) + stringBuilder.setLength(0) // clear + } + } + } + //endregion + + private companion object { + const val BUFFER_SIZE = 262144 // should be ok: example 200 simulators * 2buffers * 256KB == 100 MB + const val NEW_LINE: Char = '\n' + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt new file mode 100644 index 00000000..4dae6c78 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt @@ -0,0 +1,19 @@ +package com.badoo.automation.deviceserver.ios.proc + +import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.host.IRemote +import java.net.URI + +class SimulatorWebDriverAgent( + remote: IRemote, + wdaPath: String, + udid: UDID, + wdaEndpoint: URI, + hostApp: String = "com.apple.Preferences" +) : WebDriverAgent( + remote = remote, + wdaPath = wdaPath, + hostApp = hostApp, + udid = udid, + wdaEndpoint = wdaEndpoint +) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt new file mode 100644 index 00000000..f5b25fbe --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt @@ -0,0 +1,76 @@ +package com.badoo.automation.deviceserver.ios.proc + +import com.badoo.automation.deviceserver.command.ChildProcess +import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.host.IRemote +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl +import com.badoo.automation.deviceserver.util.ensure +import com.badoo.automation.deviceserver.util.uriWithPath +import java.net.URI + +open class WebDriverAgent( + protected val remote: IRemote, + protected val wdaPath: String, + protected val hostApp: String, + protected val udid: UDID, + private val wdaEndpoint: URI, + private val childFactory: ( + remoteHost: String, + userName: String, + cmd: List, + isInteractiveShell: Boolean, + out_reader: (line: String) -> Unit, + err_reader: (line: String) -> Unit + ) -> ChildProcess = ChildProcess.Companion::fromCommand +) : LongRunningProc(udid, remote.hostName) { + private val launchXctestCommand: List = listOf( + FBSimctl.FBSIMCTL_BIN, + FBSimctl.RESPONSE_FORMAT, + udid, + "launch_xctest", + wdaPath, + hostApp, + "--port", + wdaEndpoint.port.toString(), + "--", + "listen" + ) + private val uri: URI = uriWithPath(wdaEndpoint, "wda/healthcheck") + + override fun toString(): String = "<$udid at ${remote.hostName}:${wdaEndpoint.port}>" + + override fun start() { + ensure(childProcess == null) { WebDriverAgentError("Previous WebDriverAgent childProcess $childProcess has not been killed") } + ensure(remote.isDirectory(wdaPath)) { WebDriverAgentError("WebDriverAgent $wdaPath does not exist or is not a directory") } + logger.debug(logMarker, "$this — Starting child process") + + terminateHostApp() + + childProcess = childFactory( + remote.hostName, + remote.userName, + launchXctestCommand, + false, + { message -> logger.info(logMarker, "${this@WebDriverAgent}: WDA : ${message.trim()}") }, + { message -> logger.warn(logMarker, "${this@WebDriverAgent}: WDA : ${message.trim()}") } + ) + + Thread.sleep(5000) // 5 should be ok + logger.debug(logMarker, "$this WDA: $childProcess") + } + + protected open fun terminateHostApp() { + remote.fbsimctl.terminateApp(udid, bundleId = hostApp, raiseOnError = false) + Thread.sleep(1000) + } + + override fun checkHealth(): Boolean { + return try { + val result = client.get(uri.toURL()) + return result.isSuccess + } catch (e: RuntimeException) { + logger.warn(logMarker, "Failed to determine WDA driver state. Exception: $e") + false + } + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgentError.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgentError.kt new file mode 100644 index 00000000..7bd9fe11 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgentError.kt @@ -0,0 +1,5 @@ +package com.badoo.automation.deviceserver.ios.proc + +import java.lang.IllegalStateException + +class WebDriverAgentError(message: String) : IllegalStateException(message) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt new file mode 100644 index 00000000..31081731 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt @@ -0,0 +1,30 @@ +package com.badoo.automation.deviceserver.ios.simulator + +import com.badoo.automation.deviceserver.data.* +import com.badoo.automation.deviceserver.ios.simulator.video.SimulatorVideoRecorder +import java.net.URI +import java.net.URL + +interface ISimulator { + // FIXME: cleanup unnecessary properties from interface (copied attr_reader from ruby as is) + val ref: DeviceRef + val udid: UDID + val state: DeviceState + val fbsimctlEndpoint: URI + val wdaEndpoint: URI + val userPorts: DeviceAllocatedPorts + val info: DeviceInfo + val lastError: Exception? + val calabashPort: Int + val videoRecorder: SimulatorVideoRecorder + val fbsimctlSubject: String + + fun prepareAsync() + fun resetAsync() + fun status(): SimulatorStatusDTO + fun endpointFor(port: Int): URL + fun approveAccess(bundleId: String) + fun release(reason: String) + fun clearSafariCookies(): Map + fun lastCrashLog(): CrashLog +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt new file mode 100644 index 00000000..e0a9c699 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt @@ -0,0 +1,581 @@ +package com.badoo.automation.deviceserver.ios.simulator + +import com.badoo.automation.deviceserver.LogMarkers +import com.badoo.automation.deviceserver.WaitTimeoutError +import com.badoo.automation.deviceserver.data.* +import com.badoo.automation.deviceserver.host.IRemote +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDeviceState +import com.badoo.automation.deviceserver.ios.proc.FbsimctlProc +import com.badoo.automation.deviceserver.ios.proc.SimulatorWebDriverAgent +import com.badoo.automation.deviceserver.ios.simulator.backup.ISimulatorBackup +import com.badoo.automation.deviceserver.ios.simulator.backup.SimulatorBackup +import com.badoo.automation.deviceserver.ios.simulator.video.SimulatorVideoRecorder +import com.badoo.automation.deviceserver.util.executeWithTimeout +import com.badoo.automation.deviceserver.util.pollFor +import kotlinx.coroutines.experimental.* +import net.logstash.logback.marker.MapEntriesAppendingMarker +import org.slf4j.LoggerFactory +import org.slf4j.Marker +import java.io.File +import java.io.FileOutputStream +import java.io.IOException +import java.net.URI +import java.net.URL +import java.time.Duration +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock +import kotlin.system.measureTimeMillis + + +class Simulator ( + private val deviceRef: DeviceRef, + private val remote: IRemote, + deviceInfo: DeviceInfo, + private val allocatedPorts: DeviceAllocatedPorts, + private val deviceSetPath: String, + wdaPath: String, + private val concurrentBootsPool: ThreadPoolDispatcher, + headless: Boolean, + override val fbsimctlSubject: String +) : ISimulator +{ + private companion object { + private val PREPARE_TIMEOUT: Duration = Duration.ofMinutes(4) + private val RESET_TIMEOUT: Duration = Duration.ofMinutes(3) + private const val SAFARI_BUNDLE_ID = "com.apple.mobilesafari" + } + + //region public properties copied from Ruby + override val ref = deviceRef + override val udid = deviceInfo.udid + override val fbsimctlEndpoint = URI("http://${remote.hostName}:${allocatedPorts.fbsimctlPort}/$udid/") + override val wdaEndpoint= URI("http://${remote.hostName}:${allocatedPorts.wdaPort}/") + override val userPorts = allocatedPorts + override val info = deviceInfo + override val calabashPort: Int = allocatedPorts.calabashPort + override val videoRecorder: SimulatorVideoRecorder = SimulatorVideoRecorder(udid, remote) + //endregion + + //region instance state variables + private val deviceLock = ReentrantLock() + @Volatile private var deviceState: DeviceState = DeviceState.NONE // writing from separate thread + @Volatile private var lastException: Exception? = null // writing from separate thread + + private lateinit var criticalAsyncPromise: Job // 1-1 from ruby + private val fbsimctlProc: FbsimctlProc = FbsimctlProc(remote, deviceInfo.udid, fbsimctlEndpoint, headless) + private val wdaProc = SimulatorWebDriverAgent(remote, wdaPath, deviceInfo.udid, wdaEndpoint) + private val backup: ISimulatorBackup = SimulatorBackup(remote, udid, deviceSetPath) + private val simulatorStatus = SimulatorStatus() + private val logger = LoggerFactory.getLogger(javaClass.simpleName) + private val logMarker: Marker = MapEntriesAppendingMarker(mapOf( + LogMarkers.DEVICE_REF to deviceRef, + LogMarkers.UDID to udid, + LogMarkers.HOSTNAME to remote.hostName + )) + //endregion + + //region properties from ruby with backing mutable field + override val state get() = deviceState + override val lastError get() = lastException + //endregion + + override fun toString() = "" + + //region prepareAsync + override fun prepareAsync() { + executeCriticalAsync { + val elapsed = measureTimeMillis { + prepare(clean = true) + } + logger.info(logMarker, "Device ${Simulator@this.toString()} ready in ${elapsed / 1000} seconds") + } + } + + private fun prepare(timeout: Duration = PREPARE_TIMEOUT, clean: Boolean) { + logger.info(logMarker, "Starting to prepare ${Simulator@this}. Will wait for ${timeout.seconds} seconds") + lastException = null + wdaProc.kill() + shutdown() + + //FIXME: add checks for cancellation of criticalAsyncPromise + executeWithTimeout(timeout, "Preparing simulator") { + // erase simulator if there is no existing backup, this is to ensure backup is created from a clean state + logger.info(logMarker, "Launch prepare sequence for ${Simulator@this} asynchronously") + + if (backup.isExist()) { + if (clean) { + backup.restore() + } + } else { + eraseSimulatorAndCreateBackup() + } + + logTiming("simulator boot") { boot() } + + logTiming("starting WebDriverAgent") { startWdaWithRetry() } + + logger.info(logMarker, "Finished preparing $this") + deviceState = DeviceState.CREATED + } + } + + private fun startWdaWithRetry(pollTimeout: Duration = Duration.ofSeconds(30), retryInterval: Duration = Duration.ofSeconds(3)) { + val maxRetries = 3 + + for (attempt in 1..maxRetries) { + try { + logger.info(logMarker, "Starting WebDriverAgent on ${this@Simulator}") + + wdaProc.kill() + wdaProc.start() + + pollFor( + pollTimeout, + reasonName = "${this@Simulator} WebDriverAgent health check", + retryInterval = retryInterval, + logger = logger, + marker = logMarker + ) { + //FIXME: add short_circuit: and throw if wdaProc.childProcess is dead + if (wdaProc.isProcessAlive) { + wdaProc.isHealthy() + } else { + throw WaitTimeoutError("WebDriverAgent process is not alive") + } + } + + break + } + catch (e: WaitTimeoutError) { + logger.warn(logMarker, "Attempt $attempt to start WebDriverAgent for ${this@Simulator} timed out: $e") + if (attempt == maxRetries) { + throw e + } + } + } + + logger.info(logMarker, "Started WebDriverAgent on ${this@Simulator}") + } + + private fun eraseSimulatorAndCreateBackup() { + logger.info(logMarker, "Erasing simulator ${this@Simulator} before creating a backup") + remote.fbsimctl.eraseSimulator(udid) + + logger.info(logMarker, "Booting ${this@Simulator} before creating a backup") + logTiming("initial boot") { boot() } + + logger.info(logMarker, "Shutting down ${this@Simulator} before creating a backup") + shutdown() + + backup.create() + } + + private fun shutdown() { + logger.info(logMarker, "Shutting down ${Simulator@this}") + ignoringErrors({ fbsimctlProc.kill() }) + + if (remote.fbsimctl.listDevice(udid)?.state != FBSimctlDeviceState.SHUTDOWN.value) { + remote.fbsimctl.shutdown(udid) + pollFor(Duration.ofSeconds(50), "${this@Simulator} to shutdown", logger = logger, marker = logMarker) { + val fbSimctlDevice = remote.fbsimctl.listDevice(udid) + FBSimctlDeviceState.SHUTDOWN.value == fbSimctlDevice?.state + } + } + + logger.info(logMarker, "Successfully shut down ${Simulator@this}") + } + + private fun boot() { + logger.info(logMarker, "Booting ${Simulator@this} asynchronously") + val bootJob = async(context = concurrentBootsPool) { + truncateSystemLogIfExists() + + logger.info(logMarker, "Starting fbsimctl on ${this@Simulator}") + fbsimctlProc.start() // boots simulator + + var lastState: String? = null + + try { + pollFor( + Duration.ofSeconds(60), + reasonName = "${this@Simulator} initial boot", + shouldReturnOnTimeout = false, + logger = logger, + marker = logMarker + ) { + val simulatorInfo = remote.fbsimctl.listDevice(udid) + lastState = simulatorInfo?.state + lastState == FBSimctlDeviceState.BOOTED.value + } + } catch (e: WaitTimeoutError) { + throw WaitTimeoutError("${e.message}. Simulator is in wrong state of $lastState", e) + } + + var systemLogPath = "" + + pollFor(Duration.ofSeconds(20), "${this@Simulator} system log appeared", + shouldReturnOnTimeout = true, logger = logger, marker = logMarker) { + val diagnosticInfo = remote.fbsimctl.diagnose(udid) + val location = diagnosticInfo.sysLogLocation + if (location != null) { + systemLogPath = location + logger.info(logMarker, "Device ${this@Simulator} system log appeared") + true + } else { + logger.warn(logMarker, "Device ${this@Simulator} system log NOT appeared") + false + } + } + + pollFor(Duration.ofSeconds(30), "${this@Simulator} to be sufficiently booted", + shouldReturnOnTimeout = true, logger = logger, marker = logMarker) { + if (!systemLogPath.isBlank()) { + remote.execIgnoringErrors(listOf("grep", "-m1", "SpringBoard", systemLogPath)).isSuccess + } else { + false + } + } + + pollFor( + Duration.ofSeconds(60), + reasonName = "${this@Simulator} FbSimCtl health check", + retryInterval = Duration.ofSeconds(3), + logger = logger, + marker = logMarker + ) { + fbsimctlProc.isHealthy() + } + + logger.info(logMarker, "Device ${this@Simulator} is sufficiently booted") + } + + runBlocking { + bootJob.await() + } + } + + private fun truncateSystemLogIfExists() { + val sysLog = remote.fbsimctl.diagnose(udid).sysLogLocation ?: return + + if (remote.isLocalhost()) { + try { + FileOutputStream(sysLog).channel.use { + it.truncate(0) + } + } catch(e: IOException) { + logger.error(logMarker, "Error truncating sysLog $this", e) + } + } else { + try { + remote.shell("echo -n > $sysLog", returnOnFailure = true) + logger.debug(logMarker, "Truncated syslog of simulator ${Simulator@ this}") + } catch (e: RuntimeException) { + logger.error(logMarker, "Error while truncating syslog of simulator ${Simulator@ this}", e) + } + } + } + + private fun logTiming(actionName: String, function: () -> Unit) { + logger.info(logMarker, "Device ${Simulator@this} starting action <$actionName>") + val elapsed = measureTimeMillis(function) + logger.info(logMarker, "Device ${Simulator@this} action <$actionName> took ${elapsed / 1000} seconds") + } + //endregion + + //region reset async + override fun resetAsync() { + executeCritical { + deviceState = DeviceState.RESETTING + } + + executeCriticalAsync { + // FIXME: check for it.isActive to help to cancel long running tasks + val elapsed = measureTimeMillis { + resetFromBackup() + prepare(clean = false) // simulator is already clean as it was restored from backup in resetFromBackup + } + logger.info(logMarker, "Device ${this@Simulator} reset and ready in ${elapsed / 1000} seconds") + } + } + + private fun resetFromBackup(timeout: Duration = RESET_TIMEOUT) { + logger.info(logMarker, "Starting to reset $this") + + executeWithTimeout(timeout, "Resetting simulator") { + disposeResources() + shutdown() + + if (!backup.isExist()) { + logger.error(logMarker, "Could not find backup for $this") + throw SimulatorError("Could not find backup for $this") + } + + logTiming("replacing with backup") { + backup.restore() + } + } + + logger.info(logMarker, "Finished to reset $this") + } + //endregion + + //region helper functions — execute critical and async + private fun executeCriticalAsync(function: (context: CoroutineScope) -> Unit) { + criticalAsyncPromise = launch(context = simulatorsThreadPool) { + executeCritical { + function(this) + } + } + } + + private fun executeCritical(action: () -> Unit) { + if (deviceLock.isLocked) { + logger.info(logMarker, "Awaiting for previous action. Likely a criticalAsyncPromise $criticalAsyncPromise on ${Simulator@this}") + } + + deviceLock.withLock { + try { + action() + } catch (e: RuntimeException) { + deviceState = DeviceState.FAILED + lastException = e + // FIXME: force shutdown failed sim + logger.error(logMarker, "Execute critical block finished with exception. Message: [${e.message}]", e) + logger.warn(logMarker, "Host stats on ${Simulator@this} are:\n${getSystemStats()}") + } + } + } + + private fun getSystemStats(): String { + val uptime = remote.execIgnoringErrors(listOf("/usr/bin/uptime")) + val message = mutableListOf("uptime", uptime.stdOut) + + val istats = remote.execIgnoringErrors(listOf("istats", "--no-graphs"), env = mapOf("RUBYOPT" to "")) + + if (istats.isSuccess) { + message.add("istats") + message.add(istats.stdOut) + } + + return message.joinToString("\n") + } + //endregion + + //region simulator status + override fun status(): SimulatorStatusDTO { + refreshStatus() + return SimulatorStatusDTO( + ready = deviceState == DeviceState.CREATED && simulatorStatus.isReady, + wda_status = simulatorStatus.wdaStatus, + fbsimctl_status = simulatorStatus.fbsimctlStatus, + state = deviceState.value, + last_error = lastException?.toDTO() + ) + } + + private fun Exception.toDTO(): ExceptionDTO { + + return ExceptionDTO( + type = this.javaClass.name, + message = this.message ?: "", + stackTrace = stackTrace.map { it.toString() } + ) + } + + private fun refreshStatus() { + simulatorStatus.isReady = false + simulatorStatus.fbsimctlStatus = false + simulatorStatus.wdaStatus = false + + if (deviceState != DeviceState.CREATED) { + return + } + + val device = remote.fbsimctl.listDevice(udid) ?: return + + if (device.state != FBSimctlDeviceState.BOOTED.value) { + return + } + + runBlocking { + val isFbsimctlHealthyTask = async { fbsimctlProc.isHealthy() } + val isWdaHealthyTask = async { wdaProc.isHealthy() } + + val isFbsimctlHealthy: Boolean = isFbsimctlHealthyTask.await() + val isWdaHealthy: Boolean = isWdaHealthyTask.await() + + if (isFbsimctlHealthy) { + simulatorStatus.fbsimctlStatusRetries = 0 + } else { + simulatorStatus.fbsimctlStatusRetries += 1 + + if (simulatorStatus.fbsimctlStatusRetries > 3) { + executeCritical { + deviceState = DeviceState.FAILED + } + + val message = "${this@Simulator} Fbsimctl: simulator is not healthy, probably crashed." + logger.error(logMarker, message) + lastException = RuntimeException(message) + return@runBlocking + } + } + + if (isWdaHealthy) { + simulatorStatus.wdaStatusRetries = 0 + } else { + simulatorStatus.wdaStatusRetries += 1 + + if (simulatorStatus.wdaStatusRetries > 3) { + executeCritical { + deviceState = DeviceState.FAILED + } + + val message = "${this@Simulator} WebDriverAgent is not healthy, probably crashed." + logger.error(logMarker, message) + lastException = RuntimeException(message) + return@runBlocking + } + } + + simulatorStatus.isReady = isWdaHealthy && isFbsimctlHealthy + simulatorStatus.fbsimctlStatus = isFbsimctlHealthy + simulatorStatus.wdaStatus = isWdaHealthy + } + } + //endregion + + override fun endpointFor(port: Int): URL { + val ports = allocatedPorts.toSet() + require(ports.contains(port)) { "Port $port is not in user ports range $ports" } + + return URL("http://${remote.hostName}:$port/") + } + + //region approveAccess + override fun approveAccess(bundleId: String) { + updatePermission(bundleId, "kTCCServiceCamera") + updatePermission(bundleId, "kTCCServicePhotos") + updatePermission(bundleId, "kTCCServiceAddressBook") + } + + private fun updatePermission(bundleId: String, key: String) { + val path = File(deviceSetPath, udid) + val sqlCmd = "sqlite3 ${path.absolutePath}/data/Library/TCC/TCC.db" + val insert = + "$sqlCmd \"INSERT INTO access (service, client, client_type, allowed, prompt_count) VALUES ('$key','$bundleId',0,1,1);\"" + val update = "$sqlCmd \"UPDATE access SET allowed=1 where client='#{bundle_id}' AND service='#{key}'\"" + + // FIXME: should we fail if sqlite3 fails (insert or update) or shall we do a separate check for access to be granted? + remote.shell(insert) + val result = remote.shell(update) + + if (!result.isSuccess) { + logger.error(logMarker, "Device $this permission update failed ${result.stdErr}") + } + } + //endregion + + //region release + override fun release(reason: String) { + logger.info(logMarker, "Releasing device $this because $reason") + + // FIXME: add background thread to clear up junk we failed to delete + if (deviceLock.isLocked) { + logger.warn(logMarker, "Going to kill previous promise $criticalAsyncPromise running on $this") + } + + if (criticalAsyncPromise.isActive) { + // FIXME: unlike in Ruby canceling is not immediate, consider using thread instead of async + criticalAsyncPromise.cancel(CancellationException("Killing previous $criticalAsyncPromise running on $this due to release of the device")) + } + + executeCritical { + disposeResources() + shutdown() + } + logger.info(logMarker, "Released device $this") + } + + private fun disposeResources() { + ignoringErrors({ videoRecorder.dispose() }) + ignoringErrors({ wdaProc.kill() }) + } + + private fun ignoringErrors(action: () -> Unit?) { + try { + action() + } catch (e: Throwable) { // FIXME: RuntimeError, SystemCallError in Ruby + logger.warn(logMarker, "Ignoring $this release error: $e") + } + } + //endregion + + /** + * [see Deleting-Safari-Cookies-in-iOS-Simulator.html](http://www.ryanchapin.com/fv-b-4-744/Deleting-Safari-Cookies-in-iOS-Simulator.html) + */ + override fun clearSafariCookies(): Map { + val apps = remote.fbsimctl.listApps(udid) + check(!apps.isEmpty()) { "Could not list apps for $this" } + + val safari = apps.find { SAFARI_BUNDLE_ID == it.bundle.bundle_id } + checkNotNull(safari) { "$SAFARI_BUNDLE_ID not found in $apps for $this" } + + val cookiesPath = File(safari!!.data_container, listOf("Library", "Cookies", "Cookies.binarycookies").joinToString(File.separator)) + + val result = remote.execIgnoringErrors(listOf("rm", "-f", cookiesPath.absolutePath)) + check(result.isSuccess) { "Failed to remove safari cookies (${cookiesPath.absolutePath} on $remote for $this: $result" } + + return mapOf("status" to "true") + } + + //region last crash log + override fun lastCrashLog(): CrashLog { + val crashLogs = listCrashLogs() + + if (crashLogs.isEmpty()) { + //FIXME: in Ruby there is a JSON parse exception for not found case + return CrashLog("no crash logs found", "") + } + + val logFileName = crashLogs.first() + + if (logFileName.isBlank()) { + return CrashLog("no crash logs found", "") + } + + val result = remote.execIgnoringErrors(listOf("cat", logFileName)) + + if (!result.isSuccess) { + throw SimulatorError("Failed to read crash file $logFileName on $remote for $this: $result") + } else { + cleanupOldCrashLogs(crashLogs) // FIXME: this will delete current crash log which makes method non-idempotent + } + + return CrashLog(filename = File(logFileName).name, content = result.stdOut) + } + + private fun cleanupOldCrashLogs(crashLogs: List) { + crashLogs.forEach { logFileName -> + try { + remote.execIgnoringErrors(listOf("rm", "-f", "'$logFileName'"), timeOutSeconds = 5) + } catch (e: RuntimeException) { + logger.warn(logMarker, "Failed to delete crash log $logFileName: $e") + } + } + } + + private fun listCrashLogs(): List { + val cmd = "ls -t \$HOME/Library/Logs/DiagnosticReports/*.crash | xargs grep -l $udid || true" + + val result = remote.shell(cmd, returnOnFailure = true) + if (!result.isSuccess) { + SimulatorError("Failed to list crash logs for $this: $result") + } + return result.stdOut + .split("\n") + .map { it.trim() } + .filter { it.isNotBlank() } + } + //endregion +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorError.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorError.kt new file mode 100644 index 00000000..501f0bf3 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorError.kt @@ -0,0 +1,3 @@ +package com.badoo.automation.deviceserver.ios.simulator + +class SimulatorError(message: String) : RuntimeException(message) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorThreads.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorThreads.kt new file mode 100644 index 00000000..fb3b3d45 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorThreads.kt @@ -0,0 +1,9 @@ +package com.badoo.automation.deviceserver.ios.simulator + +import kotlinx.coroutines.experimental.ThreadPoolDispatcher +import kotlinx.coroutines.experimental.newFixedThreadPoolContext +import java.util.concurrent.ScheduledThreadPoolExecutor + +// lots of blocking and nothing memory consuming, so should be ok +val simulatorsThreadPool: ThreadPoolDispatcher = newFixedThreadPoolContext(100, "Simulator_Thread") +val periodicTasksPool = ScheduledThreadPoolExecutor(4) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/backup/ISimulatorBackup.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/backup/ISimulatorBackup.kt new file mode 100644 index 00000000..98b63822 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/backup/ISimulatorBackup.kt @@ -0,0 +1,8 @@ +package com.badoo.automation.deviceserver.ios.simulator.backup + +interface ISimulatorBackup { + fun isExist(): Boolean + fun create() + fun restore() + fun delete() +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/backup/SimulatorBackup.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/backup/SimulatorBackup.kt new file mode 100644 index 00000000..48c12855 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/backup/SimulatorBackup.kt @@ -0,0 +1,128 @@ +package com.badoo.automation.deviceserver.ios.simulator.backup + +import com.badoo.automation.deviceserver.JsonMapper +import com.badoo.automation.deviceserver.LogMarkers +import com.badoo.automation.deviceserver.command.CommandResult +import com.badoo.automation.deviceserver.command.ShellUtils +import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.host.IRemote +import com.badoo.automation.deviceserver.util.dateNowUTC +import com.fasterxml.jackson.core.JsonProcessingException +import net.logstash.logback.marker.MapEntriesAppendingMarker +import org.slf4j.LoggerFactory +import org.slf4j.Marker +import java.io.File +import java.io.IOException + +class SimulatorBackup( + private val remote: IRemote, + private val udid: UDID, + deviceSetPath: String +) : ISimulatorBackup { + private val srcPath: String = File(deviceSetPath, udid).absolutePath + private val backupPath: String = File(deviceSetPath, udid).absolutePath + "_BACKUP" + private val metaFilePath: String = File(backupPath, "data/device_server/meta.json").absolutePath + private val metaFileDirectory = File(metaFilePath).parent + private val logger = LoggerFactory.getLogger(javaClass.simpleName) + private val logMarker: Marker = MapEntriesAppendingMarker(mapOf( + LogMarkers.UDID to udid, + LogMarkers.HOSTNAME to remote.hostName + )) + + companion object { + private const val CURRENT_VERSION = 1 + } + + data class BackupMeta(val version: Int, val created: String) { + override fun toString(): String = JsonMapper().toJson(this) + } + + override fun toString() = "<${this.javaClass.simpleName}: $udid>" + + //region backup exists? + override fun isExist(): Boolean { + logger.debug(logMarker, "Checking that backup path exists: [$backupPath]") + if (!remote.isDirectory(backupPath)) { + return false + } + + val result = remote.execIgnoringErrors(listOf("cat", metaFilePath)) + + if (!result.isSuccess) { + logger.debug(logMarker, "$this could not read backup version: ${result.stdErr}") + return false + } + + val parsedMeta = parseMeta(result.stdOut.trim()) + return CURRENT_VERSION == parsedMeta?.version + } + + private fun parseMeta(stdOut: String): BackupMeta? { + return try { + JsonMapper().fromJson(stdOut) + } catch(e: JsonProcessingException) { + null + } + } + //endregion + + //region create backup + override fun create() { + remote.execIgnoringErrors(listOf("rm", "-rf", backupPath)) + val result = remote.execIgnoringErrors(listOf("cp", "-R", srcPath, backupPath)) + + ensureSuccess(result, "$this failed to create backup $backupPath: $result") + + writeMeta() + logger.debug(logMarker, "Created backup for simulator $udid at path: [$backupPath]") + } + + private fun writeMeta() { + val meta = BackupMeta(CURRENT_VERSION, dateNowUTC().withNano(0).toString()) + val content = JsonMapper().toJson(meta) + + when { + remote.isLocalhost() -> { + if (!File(metaFileDirectory).mkdirs()) { + throw SimulatorBackupError("$this could not create $metaFileDirectory directory") + } + + try { + File(metaFilePath).writeText(content) + } catch (e: IOException) { + throw SimulatorBackupError("$this could not write meta.json $e") + } + } + else -> { + remote.execIgnoringErrors("mkdir -p $metaFileDirectory".split(" ")) + val result = remote.shell( + "echo ${ShellUtils.escape(content)} > $metaFilePath", + returnOnFailure = true + ) + ensureSuccess(result, "$this could not write meta.json: ${result.stdErr}") + } + } + } + //endregion + + override fun restore() { + remote.execIgnoringErrors(listOf("rm", "-rf", srcPath)) + val result = remote.execIgnoringErrors(listOf("cp", "-R", backupPath, srcPath)) + + ensureSuccess(result, "$this failed to restore from backup $backupPath: $result") + logger.debug(logMarker, "Restored simulator $udid from backup at path: [$backupPath]") + } + + override fun delete() { + val result = remote.execIgnoringErrors("rm -rf $backupPath".split(" ")) + + ensureSuccess(result, "$this failed to delete backup $backupPath: $result") + logger.debug(logMarker, "Deleted backup for simulator $udid at path: [$backupPath]") + } + + private fun ensureSuccess(result: CommandResult, errorMessage: String) { + if (!result.isSuccess) { + throw SimulatorBackupError(errorMessage) + } + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/backup/SimulatorBackupError.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/backup/SimulatorBackupError.kt new file mode 100644 index 00000000..091e14c7 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/backup/SimulatorBackupError.kt @@ -0,0 +1,3 @@ +package com.badoo.automation.deviceserver.ios.simulator.backup + +class SimulatorBackupError(message: String) : RuntimeException(message) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt new file mode 100644 index 00000000..865100d7 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt @@ -0,0 +1,162 @@ +package com.badoo.automation.deviceserver.ios.simulator.video + +import com.badoo.automation.deviceserver.command.ChildProcess +import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.host.IRemote +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl.Companion.FBSIMCTL_BIN +import com.badoo.automation.deviceserver.ios.proc.LongRunningProc +import com.badoo.automation.deviceserver.util.ensure +import com.badoo.automation.deviceserver.util.pollFor +import java.io.File +import java.time.Duration +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +class SimulatorVideoRecorder( + private val udid: UDID, + private val remote: IRemote, + private val childFactory: (remoteHost: String, username: String, cmd: List, isInteractiveShell: Boolean, + out_reader: (line: String) -> Unit, err_reader: (line: String) -> Unit + ) -> ChildProcess? = ChildProcess.Companion::fromCommand, + private val recorderStopTimeout: Duration = RECORDER_STOP_TIMEOUT +) : LongRunningProc(udid, remote.hostName) { + @Volatile private var isStarted: Boolean = false + private val lock = ReentrantLock(true) + private val startVideoRecordingCommand = "set -x ;" + + "$FBSIMCTL_BIN $udid record start -- listen -- record stop -- diagnose &" + // background fbsimctl + "PID=$! && " + + "trap \"kill \$PID\" EXIT && " + // ensure we kill fbsimctl even when ssh is killed without sending input to stdin + "read && " + // block until there is an input on stdin + "kill -INT \$PID" // gracefully terminate fbsimctl recording + + override fun toString(): String = udid + + override fun checkHealth(): Boolean = childProcess?.isAlive() ?: false + + fun delete() { + logger.debug(logMarker, "Deleting video recording") + val info = remote.fbsimctl.diagnose(udid) + + if (info.videoLocation == null || info.videoLocation.isBlank()) { + logger.debug(logMarker, "No video recording to delete") + return + } + + val result = remote.execIgnoringErrors(listOf("rm", "-f", info.videoLocation)) + ensure(result.isSuccess) { + SimulatorVideoRecordingException("Could not delete stale recordings. Reason: $result") + } + } + + fun dispose() { + if (childProcess?.isAlive() != true) { + return + } + + logger.info(logMarker, "Terminating video recording process") + childProcess?.kill(Duration.ofSeconds(3)) + delete() + logger.info(logMarker, "Disposed video recording") + } + + override fun start() { + logger.info(logMarker, "Starting video recording") + lock.withLock { + if (isStarted) { + val message = "Video recording already started" + logger.error(logMarker, message) + throw SimulatorVideoRecordingException(message) + } + + delete() + childProcess = childFactory(remote.hostName, remote.userName, listOf(startVideoRecordingCommand), false, + { logger.debug(logMarker, "$udid: VideoRecorder : ${it.trim()}") }, + { logger.debug(logMarker, "$udid: VideoRecorder : ${it.trim()}") } + ) + + logger.info(logMarker, "Started video recording") + isStarted = true + } + } + + fun stop() { + lock.withLock { + if (!isStarted) { + val message = "Video recording has not yet started" + logger.warn(logMarker, message) + throw SimulatorVideoRecordingException(message) + } + + logger.info(logMarker, "Stopping video recording") + childProcess?.writeStdin("q\n") + pollFor(recorderStopTimeout, "Stop video recording", false, Duration.ofMillis(500), logger, logMarker) { + childProcess?.isAlive() == false + } + childProcess?.kill() + childProcess = null + logger.info(logMarker, "Stopped video recording") + isStarted = false + } + } + + fun getRecording(): ByteArray { + logger.info(logMarker, "Getting video recording") + val info = remote.fbsimctl.diagnose(udid) + + if (info.videoLocation == null) { + val message = "Could not find diagnostic video events in $info" + logger.error(logMarker, message) + throw SimulatorVideoRecordingException(message) + } + + logger.debug(logMarker, "Found video recording ${info.videoLocation}") + val videoFile: File = tryCompressVideo(File(info.videoLocation)) + + // TODO: is there a better way to read binary file over ssh without rsyncing? + // We should get rid of ssh and move to having 1 http server per 1 host and some proxy node to tie them together + // once we have proper deployment solution for our macOS machines + val result = remote.captureFile(videoFile) + if (!result.isSuccess) { + val message = "Could not read video file $result" + logger.error(message) + throw SimulatorVideoRecordingException(message) + } + logger.info(logMarker, "Received video recording. Size ${result.stdOutBytes.size} bytes") + return result.stdOutBytes + } + + // because we compress videos, we can't simple forward response from fbsimctl http request to /diagnose/video + // if we move compression to consumer side, this method can be simplified + private fun tryCompressVideo(srcVideo: File): File { + //FIXME: move this ["which", FFMPEG_PATH] logic to host checker. no need to do it every time when fetching a video. + if (!remote.execIgnoringErrors(listOf("which", FFMPEG_PATH)).isSuccess) { + return srcVideo + } + + val dstVideo = File(srcVideo.parent, "compressed.mp4") + val compressionResult = remote.execIgnoringErrors( + listOf( + FFMPEG_PATH, + "-loglevel", "panic", + "-i", srcVideo.absolutePath, + "-y", "-preset", "ultrafast", + dstVideo.absolutePath + ) + ) + + if (compressionResult.isSuccess) { + remote.execIgnoringErrors(listOf("rm", "-f", srcVideo.absolutePath)) + logger.debug(logMarker, "Successfully compressed video to $dstVideo") + return dstVideo + } else { + logger.error(logMarker, "Failed to compress video $srcVideo. $compressionResult") + remote.execIgnoringErrors(listOf("rm", "-f", dstVideo.absolutePath)) + return srcVideo + } + } + + private companion object { + const val FFMPEG_PATH = "/usr/local/bin/ffmpeg" + val RECORDER_STOP_TIMEOUT: Duration = Duration.ofSeconds(3) + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecordingException.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecordingException.kt new file mode 100644 index 00000000..454cb7b1 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecordingException.kt @@ -0,0 +1,3 @@ +package com.badoo.automation.deviceserver.ios.simulator.video + +class SimulatorVideoRecordingException(message: String, cause: Throwable? = null) : RuntimeException(message, cause) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClient.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClient.kt new file mode 100644 index 00000000..c08172e7 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClient.kt @@ -0,0 +1,46 @@ +package com.badoo.automation.deviceserver.util + +import com.badoo.automation.deviceserver.util.HttpCodes.* +import okhttp3.OkHttpClient +import okhttp3.Request +import java.net.ConnectException +import java.net.SocketTimeoutException +import java.net.URL +import java.net.UnknownHostException +import java.util.concurrent.TimeUnit + +class CustomHttpClient { + companion object { + private val client: OkHttpClient = OkHttpClient.Builder() + .connectTimeout(10, TimeUnit.SECONDS) + .writeTimeout(10, TimeUnit.SECONDS) + .readTimeout(10, TimeUnit.SECONDS) + .build() + } + + fun get(url: URL): HttpResult { + val request: Request = Request.Builder() + .get() + .url(url) + .build() + try { + val result = client.newCall(request).execute() + + return HttpResult( + responseBody = result.body()?.string() ?: "", + httpCode = result.code() + ) + } catch (e: SocketTimeoutException) { + return HttpResult(522) + } catch (e: UnknownHostException) { + return HttpResult(OriginIsUnreachable.code) + } catch (e: ConnectException) { + return HttpResult(WebServerIsDown.code) + } catch (e: java.io.EOFException) { + return HttpResult(NetworkReadTimeoutError.code) + } catch (e: RuntimeException) { + return HttpResult(UnknownError.code) + } + } + +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/HttpCodes.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/HttpCodes.kt new file mode 100644 index 00000000..38f82134 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/HttpCodes.kt @@ -0,0 +1,10 @@ +package com.badoo.automation.deviceserver.util + +enum class HttpCodes(val code: Int) { + OK(200), + UnknownError(520), + WebServerIsDown(521), + ConnectionTimedOut(522), + OriginIsUnreachable(523), + NetworkReadTimeoutError(598), +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/HttpResult.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/HttpResult.kt new file mode 100644 index 00000000..20ccf735 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/HttpResult.kt @@ -0,0 +1,9 @@ +package com.badoo.automation.deviceserver.util + +import com.badoo.automation.deviceserver.util.HttpCodes.* + +data class HttpResult( + val httpCode: Int, + val responseBody: String? = null, + val isSuccess: Boolean = httpCode == OK.code +) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/Support.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/Support.kt new file mode 100644 index 00000000..ee41d22a --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/Support.kt @@ -0,0 +1,77 @@ +package com.badoo.automation.deviceserver.util + +import com.badoo.automation.deviceserver.WaitTimeoutError +import org.slf4j.Logger +import org.slf4j.Marker +import java.net.URI +import java.time.Clock +import java.time.Duration +import java.time.LocalDateTime +import java.util.concurrent.ExecutionException +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +fun executeWithTimeout(timeout: Duration, name: String, action: () -> T): T { + val executor = Executors.newSingleThreadExecutor() + val future = executor.submit(action) + executor.shutdown() // does not cancel already scheduled task, prevents new tasks + + try { + return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS) + } catch (e: TimeoutException) { + future.cancel(true) + throw TimeoutException("$name timed out after ${timeout.seconds} seconds") + } catch (e: ExecutionException) { + when { + e.cause == null -> throw e + else -> throw e.cause!! + } + } +} + +/** + * [timeOut] — timeout seconds + */ +fun pollFor(timeOut: Duration, reasonName: String, shouldReturnOnTimeout: Boolean = false, + retryInterval: Duration = Duration.ofSeconds(2), logger: Logger, marker: Marker, action: () -> Boolean) { + var isSuccess: Boolean + val startMillis = System.currentTimeMillis() + val stopMillis = startMillis + timeOut.toMillis() + + logger.trace(marker, "Awaiting for: $reasonName...") + do { + isSuccess = action() // what if this hangs on IO ?? + + if (isSuccess) { + logger.trace(marker, "Awaited successfully for: $reasonName") + break + } else { + Thread.sleep(retryInterval.toMillis()) + } + } while (!isSuccess && stopMillis > System.currentTimeMillis()) + + if (!isSuccess && !shouldReturnOnTimeout) { + val message = "$reasonName failed after waiting ${timeOut.seconds} seconds" + logger.error(marker, message) + throw WaitTimeoutError(message) + } +} + +/** + * returns [LocalDateTime] object containing Time.now in UTC + */ +fun dateNowUTC(): LocalDateTime = LocalDateTime.now(Clock.systemUTC()) + +/** + * Ensures [condition] is true, otherwise throws error [exception] + */ +fun ensure(condition: Boolean, exception: () -> RuntimeException) { + if (!condition) { + throw exception() + } +} + +fun uriWithPath(uri: URI, path: String): URI { + return URI(listOf(uri.toString(), path).joinToString("/")).normalize() +} diff --git a/device-server/src/main/resources/application.conf b/device-server/src/main/resources/application.conf new file mode 100644 index 00000000..432e2040 --- /dev/null +++ b/device-server/src/main/resources/application.conf @@ -0,0 +1,15 @@ +ktor { + deployment { + port = 4567 + autoreload = false + watch = [] + parallelism = 8 + connectionGroupSize = 40 + workerGroupSize = 100 + callGroupSize = 200 + } + + application { + modules = [ com.badoo.automation.deviceserver.DeviceServerKt.module ] + } +} diff --git a/device-server/src/main/resources/logback-test.xml b/device-server/src/main/resources/logback-test.xml new file mode 100644 index 00000000..85cf0f3a --- /dev/null +++ b/device-server/src/main/resources/logback-test.xml @@ -0,0 +1,19 @@ + + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %logger{36} - %msg%n + + + + + + + + + + + + + + \ No newline at end of file diff --git a/device-server/src/main/resources/logback.xml b/device-server/src/main/resources/logback.xml new file mode 100644 index 00000000..116a8368 --- /dev/null +++ b/device-server/src/main/resources/logback.xml @@ -0,0 +1,19 @@ + + + + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %logger{36} - %msg%n + + + + + + + + + + + + + + \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/DeviceServerConfigTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/DeviceServerConfigTest.kt new file mode 100644 index 00000000..4bf3bb7e --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/DeviceServerConfigTest.kt @@ -0,0 +1,23 @@ +package com.badoo.automation.deviceserver + +import org.junit.Assert +import org.junit.Test + +class DeviceServerConfigTest { + + private val localhostConfig = """ +{ + "timeouts": { + "device": 600 + }, + "nodes": [{}] +} +""".trimMargin() + + @Test + fun shouldDeserialize() { + val config = JsonMapper().fromJson(localhostConfig) + Assert.assertEquals(600, config.timeouts["device"]?.toInt()) + Assert.assertEquals(NodeConfig(), config.nodes.first()) + } +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/NodeConfigTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/NodeConfigTest.kt new file mode 100644 index 00000000..36b5cb0a --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/NodeConfigTest.kt @@ -0,0 +1,47 @@ +package com.badoo.automation.deviceserver + +import com.badoo.automation.deviceserver.ios.device.KnownDevice +import org.junit.Assert +import org.junit.Test + +class NodeConfigTest { + private val config = """ + { + "type": "devices", + "user": "user", + "host": "host", + "simulator_limit": 1, + "concurrent_boots": 1, + "whitelist_apps": [ "bundle.id" ], + "devices": [ + { + "udid": "c865bdbe652d17cbe2c79566fb046b73fed66a38", + "ip": "127.0.0.1", + "wifi_address": "00:00:00:00:00:00" + } + ] + } + """ + + @Test + fun shouldDeserialize() { + val config = JsonMapper().fromJson(config) + + val expected = NodeConfig( + type = NodeConfig.NodeType.Devices, + user = "user", + host = "host", + simulatorLimit = 1, + concurrentBoots = 1, + whitelistApps = setOf("bundle.id"), + knownDevices = listOf( + KnownDevice( + "c865bdbe652d17cbe2c79566fb046b73fed66a38", + ipAddress = "127.0.0.1" + ) + ) + ) + + Assert.assertEquals(expected, config) + } +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/TestUtil.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/TestUtil.kt new file mode 100644 index 00000000..1a3c0d5a --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/TestUtil.kt @@ -0,0 +1,38 @@ +package com.badoo.automation.deviceserver + +import com.badoo.automation.deviceserver.data.* +import org.mockito.MockSettings +import org.mockito.Mockito +import java.net.URI + +/** + * Utility methods for tests. Syntax sugar or workarounds for Kotlin's keywords + */ + +/** + * Shortcut to '`Mockito.mock(MyClass::class.java)`' notation. + * Example: i.e. instead of this: + * `val myClassMock: MyClass = Mockito.mock(MyClass::class.java)` you can use this: + * `val myClassMock: MyClass = mockThis()` + * + * @return T instance of whatever type was on the left side of assignment operator. + */ +inline fun mockThis(): T = Mockito.mock(T::class.java) +inline fun mockThis(name: String): T = Mockito.mock(T::class.java, name) +inline fun mockThis(settings: MockSettings): T = Mockito.mock(T::class.java, settings) +fun anyType(): T = Mockito.any() as T +/** + * Parse a string of permissive JSON. + */ +fun json(json: String) = JsonMapper().readTree(json.byteInputStream()) + +fun deviceDTOStub(ref: DeviceRef): DeviceDTO { + return DeviceDTO( + ref, DeviceState.NONE, + URI("http://fbsimctl/endpoint/for/testing"), + URI("http://wda/endpoint/for/testing"), + 0, setOf(0), + DeviceInfo("", "", "", "", ""), + Exception().toDto(), + capabilities = null) +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommandTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommandTest.kt new file mode 100644 index 00000000..5155164f --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommandTest.kt @@ -0,0 +1,129 @@ +package com.badoo.automation.deviceserver.command + +import com.badoo.automation.deviceserver.mockThis +import com.nhaarman.mockito_kotlin.whenever +import com.zaxxer.nuprocess.NuProcess +import com.zaxxer.nuprocess.NuProcessBuilder +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import java.io.ByteArrayOutputStream +import java.io.PrintStream +import java.time.Duration + +class RemoteShellCommandTest { + private lateinit var systemErr: PrintStream + private lateinit var systemOut: PrintStream + private lateinit var spyProcessBuilder: TestProcessBuilder + private lateinit var remoteShell: IShellCommand + @Mock private lateinit var processListener: ShellCommandListener + + private val remoteHost = "node" + private val userName = "user" + private val userAtHost = "user@node" + + @Before fun setUp() { + hideTestOutput() // comment out to debug + + MockitoAnnotations.initMocks(this) + whenever(processListener.exitCode).thenReturn(0) + whenever(processListener.stdOut).thenReturn("") + whenever(processListener.stdErr).thenReturn("") + } + + private fun hideTestOutput() { + systemErr = System.err + systemOut = System.out + val testErr = PrintStream(ByteArrayOutputStream(10_000)) + val testOut = PrintStream(ByteArrayOutputStream(10_000)) + System.setErr(testErr) + System.setOut(testOut) + } + + @Test fun nonInteractiveSshCommand() { + remoteShell = RemoteShellCommand( + remoteHost = remoteHost, + userName = userName, + builderFactory = ::nuProcessBuilderForTesting, + connectionTimeout = 1 + ) + remoteShell.exec(listOf("fbsimctl"), timeOut = Duration.ofMillis(100)) + + val expectedCommand = listOf( + "/usr/bin/ssh", + "-o", "ConnectTimeout=1", + "-o", "PreferredAuthentications=publickey", + "-q", + "-T", + userAtHost, + "fbsimctl" + ) + + assertEquals(expectedCommand, spyProcessBuilder.command()) + } + + @Test fun interactiveSshCommand() { + remoteShell = RemoteShellCommand( + remoteHost = remoteHost, + userName = userName, + builderFactory = ::nuProcessBuilderForTesting, + isInteractiveShell = true, + connectionTimeout = 1 + ) + remoteShell.exec(listOf("fbsimctl"), timeOut = Duration.ofMillis(100)) + + val expectedCommand = listOf( + "/usr/bin/ssh", + "-o", "ConnectTimeout=1", + "-o", "PreferredAuthentications=publickey", + "-q", + "-t", "-t", + userAtHost, + "fbsimctl" + ) + + assertEquals(expectedCommand, spyProcessBuilder.command()) + } + + @Test fun sshCommandWithEnvironmentVariables() { + remoteShell = RemoteShellCommand( + remoteHost = remoteHost, + userName = userName, + builderFactory = ::nuProcessBuilderForTesting, + connectionTimeout = 1 + ) + remoteShell.exec(listOf("fbsimctl", "udid='UDID'", "\$PWD"), timeOut = Duration.ofMillis(100)) + + val expectedCommand = listOf( + "/usr/bin/ssh", + "-o", "ConnectTimeout=1", + "-o", "PreferredAuthentications=publickey", + "-q", + "-T", + userAtHost, + "fbsimctl", + "udid='UDID'", + "\$PWD" + ) + + assertEquals(expectedCommand, spyProcessBuilder.command()) + } + + private class TestProcessBuilder(cmd: List, env: Map) : NuProcessBuilder(cmd, env) { + val mockedProcess: NuProcess = mockThis() + init { + whenever(mockedProcess.pid).thenReturn(Int.MAX_VALUE) + } + + override fun start(): NuProcess { + return mockedProcess + } + } + + private fun nuProcessBuilderForTesting(cmd: List, env: Map): NuProcessBuilder { + spyProcessBuilder = TestProcessBuilder(cmd, env) + return spyProcessBuilder + } +} diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ShellCommandTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ShellCommandTest.kt new file mode 100644 index 00000000..d497908b --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ShellCommandTest.kt @@ -0,0 +1,58 @@ +package com.badoo.automation.deviceserver.command + +import org.hamcrest.Matchers.* +import org.junit.Assert.assertThat +import org.junit.Before +import org.junit.Ignore +import org.junit.Test +import java.io.ByteArrayOutputStream +import java.io.PrintStream +import java.time.Duration +import kotlin.test.assertFailsWith + +class ShellCommandTest { + private lateinit var systemErr: PrintStream + private lateinit var systemOut: PrintStream + + @Before fun setUp() { + hideTestOutput() // comment out to debug + } + + private fun hideTestOutput() { + systemErr = System.err + systemOut = System.out + val testErr = PrintStream(ByteArrayOutputStream(10_000)) + val testOut = PrintStream(ByteArrayOutputStream(10_000)) + System.setErr(testErr) + System.setOut(testOut) + } + + @Test fun testCommandWithRealProcess() { + val result = ShellCommand().exec(listOf("ls", "-lah")) + assertThat("Wrong exit code", result.exitCode, equalTo(0)) + assertThat("StdOut should not be empty", result.stdOut, not(emptyString())) + assertThat("StdErr should be empty", result.stdErr, emptyString()) + } + + @Test + fun testCommandThrowsErrorWithRealProcess() { + assertFailsWith { + ShellCommand().exec(listOf("/bin/cp"), returnFailure = false) + } + } + + @Test @Ignore("Flaky in docker container") + fun testCommandThrowsErrorWhenCommandNotFound() { + assertFailsWith { + ShellCommand().exec(listOf("/usr/bin/not_existing_command"), returnFailure = false) + } + } + + @Test(expected = ShellCommandException::class) + fun testTimeOutLongRunningCommand() { + val result = ShellCommand().exec(listOf("sleep", "600"), timeOut = Duration.ofMillis(100), returnFailure = false) + assertThat("Wrong exit code", result.exitCode, equalTo(0)) + assertThat("StdOut should not be empty", result.stdOut, not(emptyString())) + assertThat("StdErr should be empty", result.stdErr, emptyString()) + } +} diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ShellUtilsTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ShellUtilsTest.kt new file mode 100644 index 00000000..777c37f1 --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ShellUtilsTest.kt @@ -0,0 +1,12 @@ +package com.badoo.automation.deviceserver.command + +import org.junit.Assert.* +import org.junit.Test + +class ShellUtilsTest { + @Test + fun testEscape() { + val actual = ShellUtils.escape("""{"version":1,"created":"2018-02-27T11:56:19"}""") + assertEquals("""\{\"version\":1,\"created\":\"2018-02-27T11:56:19\"\}""", actual) + } +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/DevicesControllerTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/DevicesControllerTest.kt new file mode 100644 index 00000000..b669bde8 --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/DevicesControllerTest.kt @@ -0,0 +1,213 @@ +package com.badoo.automation.deviceserver.controllers + +import com.badoo.automation.deviceserver.data.* +import com.badoo.automation.deviceserver.deviceDTOStub +import com.badoo.automation.deviceserver.host.management.IDeviceManager +import com.badoo.automation.deviceserver.json +import com.badoo.automation.deviceserver.mockThis +import com.nhaarman.mockito_kotlin.whenever +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.CoreMatchers.sameInstance +import org.junit.Assert.assertThat +import org.junit.Test +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import java.net.URL + +private val happyEmpty: Map = mapOf() + +class DevicesControllerTest { + private var deviceManager: IDeviceManager = mockThis() + private var expectedArray = ByteArray(3) + + private var deviceServer = DevicesController(deviceManager) + private val deviceRef: DeviceRef = "foobar" + private val model = "MODEL" + private val udid = "UDID-1" + private val os = "OS" + private val headless = true + private val desiredCaps = DesiredCapabilities(udid, model, os, headless) + private val desiredCapsNoUdid = DesiredCapabilities(null, model, os) + private val expectedDeviceDTO = deviceDTOStub("hello") + + @Test + fun getStatus() { + val expectedDevices = listOf(deviceDTOStub("one"), deviceDTOStub("two")) + whenever(deviceManager.getDeviceRefs()).thenReturn(expectedDevices) + + val actualDevices = deviceServer.getDeviceRefs() + + verify(deviceManager).getDeviceRefs() + assertThat(actualDevices, equalTo(expectedDevices)) + } + + @Test + fun getTotalCapacity() { + val expectedCapacity = mapOf("total" to 42) + whenever(deviceManager.getTotalCapacity(desiredCaps)).thenReturn(expectedCapacity) + + val actualCapacity = deviceServer.getTotalCapacity(desiredCaps) + + verify(deviceManager).getTotalCapacity(desiredCaps) + assertThat(actualCapacity, equalTo(expectedCapacity)) + } + + @Test + fun createDevice() { + whenever(deviceManager.createDeviceAsync(desiredCaps, null)).thenReturn(expectedDeviceDTO) + + val actualDeviceDTO = deviceServer.createDevice(desiredCaps, null) + + verify(deviceManager, times(1)).createDeviceAsync(desiredCaps, null) + assertThat(actualDeviceDTO, equalTo(expectedDeviceDTO)) + } + + @Test + fun createDeviceNoUdid() { + val desiredCapsWithEmptyUdid = DesiredCapabilities(null, model, os, headless) + whenever(deviceManager.createDeviceAsync(desiredCapsWithEmptyUdid, null)).thenReturn(expectedDeviceDTO) + + val actualDeviceDTO = deviceServer.createDevice(desiredCapsNoUdid, null) + + verify(deviceManager, times(1)).createDeviceAsync(desiredCapsWithEmptyUdid, null) + assertThat(actualDeviceDTO, equalTo(expectedDeviceDTO)) + } + + @Test + fun deleteDevice() { + val actualResult = deviceServer.deleteReleaseDevice(deviceRef) + + verify(deviceManager, times(1)).deleteReleaseDevice(deviceRef, "httpRequest") + assertThat(actualResult, equalTo(happyEmpty)) + } + + @Test + fun getDeviceContactDetails() { + whenever(deviceManager.getGetDeviceDTO(deviceRef)).thenReturn(expectedDeviceDTO) + + val actualDto = deviceServer.getDeviceContactDetails(deviceRef) + + verify(deviceManager, times(1)).getGetDeviceDTO(deviceRef) + assertThat(actualDto, equalTo(expectedDeviceDTO)) + } + + @Test + fun controlDeviceReset() { + val reset = json("""{"action": "reset"}""") + + val actualResult = deviceServer.controlDevice(deviceRef, reset) + + verify(deviceManager, times(1)).resetAsyncDevice(deviceRef) + assertThat(actualResult, equalTo(happyEmpty)) + } + + @Test + fun controlDeviceClearCookies() { + val clearSafariCookies = json("""{"action":"clear_safari_cookies"}""") + + val actualResult = deviceServer.controlDevice(deviceRef, clearSafariCookies) + + verify(deviceManager, times(1)).clearSafariCookies(deviceRef) + assertThat(actualResult, equalTo(happyEmpty)) + } + + @Test + fun setAccessToCameraAndThings() { + val cameraAndThings = json( + """ + [ + {"bundle_id": "thingy_1"}, + {"bundle_id": "thingy_2"} + ]""" + ) + val actualResult = deviceServer.setAccessToCameraAndThings(deviceRef, cameraAndThings) + + verify(deviceManager).approveAccess(deviceRef, "thingy_1") + verify(deviceManager).approveAccess(deviceRef, "thingy_2") + assertThat(actualResult, equalTo(happyEmpty)) + } + + @Test + fun getEndpointFor() { + val port = 1234 + val expectedResult = URL("http://foo:$port") + whenever(deviceManager.getEndpointFor(deviceRef, port)).thenReturn(expectedResult) + val actualResult = deviceServer.getEndpointFor(deviceRef, port) + verify(deviceManager).getEndpointFor(deviceRef, port) + assertThat(actualResult, equalTo(mapOf("endpoint" to expectedResult.toString()))) + } + + @Test + fun getLastCrashLog() { + val expectedCrashLog = mapOf("filename" to "some_path_name", "content" to "cat of pathname") + whenever(deviceManager.getLastCrashLog(deviceRef)).thenReturn(CrashLog( + expectedCrashLog["filename"]!!, + expectedCrashLog["content"]!!)) + + val actualCrashLog = deviceServer.getLastCrashLog(deviceRef) + + verify(deviceManager).getLastCrashLog(deviceRef) + assertThat(actualCrashLog, equalTo(expectedCrashLog)) + } + + @Test + fun startStopVideoStarts() { + val startVideo = json( + "{\"start\": true}" + ) + + val actualResult = deviceServer.startStopVideo(deviceRef, startVideo) + + verify(deviceManager, times(1)).startVideo(deviceRef) + assertThat(actualResult, equalTo(happyEmpty)) + } + + @Test + fun startStopVideoStops() { + val startVideo = json( + "{\"start\": false}" + ) + + val actualResult = deviceServer.startStopVideo(deviceRef, startVideo) + + verify(deviceManager, times(1)).stopVideo(deviceRef) + assertThat(actualResult, equalTo(happyEmpty)) + } + + @Test(expected = IllegalArgumentException::class) + fun startStopVideoFails() { + val startVideo = json( + """{"start": "start"}""" + ) + + deviceServer.startStopVideo(deviceRef, startVideo) + verify(deviceManager, times(1)).stopVideo(deviceRef) + } + + @Test + fun getVideo() { + whenever(deviceManager.getVideo(deviceRef)).thenReturn(expectedArray) + + val actualResult = deviceServer.getVideo(deviceRef) + + assertThat(actualResult, sameInstance(expectedArray)) + } + + @Test + fun deleteVideo() { + val actualResult = deviceServer.deleteVideo(deviceRef) + + verify(deviceManager, times(1)).deleteVideo(deviceRef) + assertThat(actualResult, equalTo(happyEmpty)) + } + + @Test + fun getDeviceState() { + val expectedState = SimulatorStatusDTO( + true, true, true, DeviceState.NONE.value, null) + whenever(deviceManager.getDeviceState(deviceRef)).thenReturn(expectedState) + val actualResult = deviceServer.getDeviceState(deviceRef) + assertThat(actualResult, equalTo(expectedState)) + } +} + diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/StatusControllerTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/StatusControllerTest.kt new file mode 100644 index 00000000..f0abf246 --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/StatusControllerTest.kt @@ -0,0 +1,20 @@ +package com.badoo.automation.deviceserver.controllers + +import com.badoo.automation.deviceserver.host.management.IDeviceManager +import com.badoo.automation.deviceserver.mockThis +import org.hamcrest.CoreMatchers +import org.junit.Assert +import org.junit.Test + +class StatusControllerTest { + private var deviceManager: IDeviceManager = mockThis() + private var statusController = StatusController(deviceManager) + @Test + fun getServerStatus() { + Assert.assertThat( + statusController.getServerStatus(), + CoreMatchers.equalTo(mapOf("status" to "ok", "deviceManager" to emptyMap() + ))) + } + +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/DesiredCapabilitiesTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/DesiredCapabilitiesTest.kt new file mode 100644 index 00000000..6f261fb8 --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/DesiredCapabilitiesTest.kt @@ -0,0 +1,63 @@ +package com.badoo.automation.deviceserver.data + +import com.badoo.automation.deviceserver.JsonMapper +import org.junit.Test +import kotlin.test.assertEquals + + +class DesiredCapabilitiesTest { + private val headless = true + + private fun fromJson(json: String): DesiredCapabilities { + return JsonMapper().fromJson(json) + } + + @Test + fun fromJsonParsesEmptyCapabilities() { + val json = "{}" + val actual = fromJson(json) + + assertEquals(DesiredCapabilities(null, null, null, headless), actual) + } + + @Test + fun fromJsonParsesModelAndVersionCapabilities() { + val json = """{"model":"iPhone 6", "os": "iOS 11.0"}""" + val actual = fromJson(json) + + assertEquals(DesiredCapabilities(null, "iPhone 6", "iOS 11.0", headless), actual) + } + + @Test + fun fromJsonParsesUdidCapability() { + val udid = "CD391B89-64C6-4106-BE37-EC1956956D28" + val json = """{"udid":"$udid"}""" + val actual = fromJson(json) + + assertEquals(DesiredCapabilities(udid, null, null, headless), actual) + } + + @Test + fun fromJsonParsesHeadlessFalseCapability() { + val json = """{"headless": false}""" + val actual = fromJson(json) + + assertEquals(DesiredCapabilities(null, null, null, false), actual) + } + + @Test + fun fromJsonParsesHeadlessBoolAsTextCapability() { + val json = """{"headless": "false"}""" + val actual = fromJson(json) + + assertEquals(DesiredCapabilities(null, null, null, false), actual) + } + + @Test + fun fromJsonParsesHeadlessDefaultCapability() { + val json = """{}""" + val actual = fromJson(json) + + assertEquals(DesiredCapabilities(null, null, null, true, true), actual) + } +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/ErrorDtoKtTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/ErrorDtoKtTest.kt new file mode 100644 index 00000000..5f9e8785 --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/ErrorDtoKtTest.kt @@ -0,0 +1,26 @@ +package com.badoo.automation.deviceserver.data + +import com.badoo.automation.deviceserver.mockThis +import com.nhaarman.mockito_kotlin.whenever +import org.junit.Assert +import org.junit.Test + +class ErrorDtoKtTest { + private val exception: Exception = mockThis() + + @Test + fun toDto() { + val stackTrace: Array = listOf( + StackTraceElement("", "", "", "com.badoo.SomeClass", "someMethod", "SomeFile.kt", 2), + StackTraceElement("", "", "", "com.badoo.SomeClass", "someMethod", "SomeFile.kt", 1), + StackTraceElement("", "", "", "io.ktor.SomeClass", "someMethod", "SomeFile.kt", 2) + ).toTypedArray() + whenever(exception.stackTrace).thenReturn(stackTrace) + + val expectedStackTrace = listOf( + "com.badoo.SomeClass.someMethod(SomeFile.kt:2)", + "com.badoo.SomeClass.someMethod(SomeFile.kt:1)" + ) + Assert.assertEquals(expectedStackTrace, exception.toDto().stackTrace) + } +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeRegistryTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeRegistryTest.kt new file mode 100644 index 00000000..b72916d9 --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeRegistryTest.kt @@ -0,0 +1,88 @@ +package com.badoo.automation.deviceserver.host + +import com.badoo.automation.deviceserver.data.DesiredCapabilities +import com.badoo.automation.deviceserver.deviceDTOStub +import com.badoo.automation.deviceserver.host.management.NodeRegistry +import com.badoo.automation.deviceserver.host.management.NodeWrapper +import com.badoo.automation.deviceserver.ios.IActiveDevices +import com.badoo.automation.deviceserver.mockThis +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.atLeast +import com.nhaarman.mockito_kotlin.verify +import com.nhaarman.mockito_kotlin.whenever +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Before +import org.junit.Test +import java.time.Duration + +class NodeRegistryTest { + private val activeDevices: IActiveDevices = mockThis() + private val nodeRegistry: NodeRegistry = NodeRegistry(activeDevices) + private val headless = true + private val desiredCapabilities = DesiredCapabilities("udid", "model", "os", headless) + private val nodeWrapper1: NodeWrapper = mockThis("wrapper1") + private val nodeWrapper2: NodeWrapper = mockThis("wrapper2") + private val wrappedNode1: ISimulatorsNode = mockThis("node1") + private val wrappedNode2: ISimulatorsNode = mockThis("node2") + private val capacityNotBusy = 0.8F + private val capacityBusy = 0.3F + + @Before + fun setUp() { + whenever(wrappedNode1.remoteAddress).thenReturn("n1") + whenever(wrappedNode2.remoteAddress).thenReturn("n2") + + whenever(nodeWrapper1.node).thenReturn(wrappedNode1) + whenever(nodeWrapper2.node).thenReturn(wrappedNode2) + + nodeRegistry.add(nodeWrapper1) + nodeRegistry.add(nodeWrapper2) + } + + @Test + fun removeIfPresent() { + nodeRegistry.removeIfPresent(nodeWrapper1) + nodeRegistry.removeIfPresent(nodeWrapper1) + + assertThat(nodeRegistry.getAll().size, equalTo(1)) + + verify(activeDevices, atLeast(1)).unregisterNodeDevices(wrappedNode1) + } + + @Test + fun capacityIgnoresDeadNodes() { + // arrange + whenever(wrappedNode1.totalCapacity(any())).thenReturn(1) + whenever(wrappedNode2.totalCapacity(any())).thenReturn(1) + + whenever(nodeWrapper1.isAlive()).thenReturn(false) + whenever(nodeWrapper2.isAlive()).thenReturn(true) + + // act + val actual = nodeRegistry.capacitiesTotal(desiredCapabilities) + + // assert + val expected = mapOf("total" to 1) + assertThat(actual, equalTo(expected)) + } + + @Test + fun createSimulatorByCapacity() { + val deviceTimeout = Duration.ofSeconds(0) + whenever(nodeWrapper1.isAlive()).thenReturn(true) + whenever(nodeWrapper2.isAlive()).thenReturn(true) + whenever(wrappedNode1.createDeviceAsync(desiredCapabilities)).then { deviceDTOStub("") } + whenever(wrappedNode2.createDeviceAsync(desiredCapabilities)).then { deviceDTOStub("") } + + whenever(wrappedNode1.capacityRemaining(desiredCapabilities)).thenReturn(capacityBusy) + whenever(wrappedNode2.capacityRemaining(desiredCapabilities)).thenReturn(capacityNotBusy) + + // act + nodeRegistry.createDeviceAsync(desiredCapabilities, deviceTimeout, null) + + // assert + verify(activeDevices).registerDevice("", wrappedNode2, deviceTimeout, null) + } +} + diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeWrapperTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeWrapperTest.kt new file mode 100644 index 00000000..189ebbb4 --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeWrapperTest.kt @@ -0,0 +1,90 @@ +package com.badoo.automation.deviceserver.host + +import com.badoo.automation.deviceserver.NodeConfig +import com.badoo.automation.deviceserver.host.management.IHostFactory +import com.badoo.automation.deviceserver.host.management.NodeRegistry +import com.badoo.automation.deviceserver.host.management.NodeWrapper +import com.badoo.automation.deviceserver.mockThis +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.times +import com.nhaarman.mockito_kotlin.verify +import com.nhaarman.mockito_kotlin.whenever +import org.junit.Test +import org.mockito.Mockito +import java.time.Duration +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class NodeWrapperTest { + private val registry: NodeRegistry = mockThis() + private val hostFactory: IHostFactory = mockThis() + private val nodeMock: ISimulatorsNode = mockThis() + private val config = NodeConfig("user", "localhost") + + @Test + fun startStopPeriodicHealthChecks() { + val nodeCheckInterval = 100L + whenever(hostFactory.getHostFromConfig(any())).thenReturn(nodeMock) + whenever(nodeMock.isReachable()).thenReturn(true) + + val nodeWrapper = Mockito.spy(getWrapperWithMocks(nodeCheckInterval)) + nodeWrapper.start() + nodeWrapper.startPeriodicHealthCheck() + Thread.sleep(nodeCheckInterval + nodeCheckInterval/2) // to guarantee a call to isAlive + nodeWrapper.stop() + Thread.sleep(nodeCheckInterval * 2) // to ensure that healthChecking thread is not terminated by jvm, but by the nodeWrapper + + verify(nodeWrapper, times(1)).isAlive() + } + + @Test + fun failsToInitOnFactoryError() { + whenever(hostFactory.getHostFromConfig(any())).thenThrow(RuntimeException()) + assertFailsWith { + getWrapperWithMocks() + } + } + + @Test + fun failsToStartIfUnreachable() { + whenever(hostFactory.getHostFromConfig(any())).thenReturn(nodeMock) + whenever(nodeMock.isReachable()).thenReturn(false) + + val started = getWrapperWithMocks().start() + + assertFalse(started) + } + + @Test + fun failsToStartIfStarted() { + whenever(hostFactory.getHostFromConfig(any())).thenReturn(nodeMock) + whenever(nodeMock.isReachable()).thenReturn(true) + val nodeWrapper = getWrapperWithMocks() + + val startedFirst = nodeWrapper.start() + val startedSecond = nodeWrapper.start() + + assertTrue(startedFirst) + assertFalse(startedSecond) + } + + @Test + fun unregistersSelfIfUnreachableLongEnough() { + whenever(hostFactory.getHostFromConfig(any())).thenReturn(nodeMock) + whenever(nodeMock.isReachable()).thenReturn(true) + val nodeWrapper = getWrapperWithMocks() + nodeWrapper.start() + + whenever(nodeMock.isReachable()).thenReturn(false) + nodeWrapper.startPeriodicHealthCheck() + + Thread.sleep(100) + verify(registry).removeIfPresent(nodeWrapper) + } + + private fun getWrapperWithMocks() = getWrapperWithMocks(1) + + private fun getWrapperWithMocks(nodeCheckInterval: Long) = + NodeWrapper(config, hostFactory, registry, 2, Duration.ofMillis(nodeCheckInterval)) +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/RemoteTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/RemoteTest.kt new file mode 100644 index 00000000..b125c33d --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/RemoteTest.kt @@ -0,0 +1,40 @@ +package com.badoo.automation.deviceserver.host + +import com.badoo.automation.deviceserver.anyType +import com.badoo.automation.deviceserver.command.CommandResult +import com.badoo.automation.deviceserver.command.IShellCommand +import com.badoo.automation.deviceserver.command.SshConnectionException +import com.badoo.automation.deviceserver.mockThis +import com.nhaarman.mockito_kotlin.whenever +import org.junit.Test + +import org.junit.Assert.* +import org.junit.Before +import org.mockito.ArgumentMatchers.* + +class RemoteTest { + private val localExecutor: IShellCommand = mockThis() + private val remoteExecutor: IShellCommand = mockThis() + private lateinit var remote: Remote + + @Before fun setUp() { + remote = Remote("host", "user", localExecutor, remoteExecutor) + } + + @Test + fun isReachableSshError() { + whenever(remoteExecutor.exec(anyList(), anyMap(), anyType(), anyBoolean(), anyType(), anyType())) + .thenThrow(SshConnectionException::class.java) + + assertFalse(remote.isReachable()) + } + + @Test + fun isReachable() { + val successfulResult = CommandResult("", "", ByteArray(0), 0) + whenever(remoteExecutor.exec(anyList(), anyMap(), anyType(), anyBoolean(), anyType(), anyType())) + .thenReturn(successfulResult) + + assertTrue(remote.isReachable()) + } +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorProviderTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorProviderTest.kt new file mode 100644 index 00000000..6735e3ed --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorProviderTest.kt @@ -0,0 +1,105 @@ +package com.badoo.automation.deviceserver.host + +import com.badoo.automation.deviceserver.data.DesiredCapabilities +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDevice +import com.badoo.automation.deviceserver.mockThis +import com.nhaarman.mockito_kotlin.times +import com.nhaarman.mockito_kotlin.verify +import com.nhaarman.mockito_kotlin.whenever +import org.hamcrest.CoreMatchers.* +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Test + +class SimulatorProviderTest { + private val remote: IRemote = mockThis() + private val headless = true + private val fbSimctl: FBSimctl = mockThis() + + init { + whenever(remote.fbsimctl).thenReturn(fbSimctl) + } + + private val provider = SimulatorProvider(remote) + + private val fbSimctlDevice: FBSimctlDevice = mockThis() + + private val dev1 = FBSimctlDevice("arch", "State", "model", "name", "udid-B", "iOS 11") + private val dev2 = FBSimctlDevice("arch", "State", "model", "name", "udid-A", "os") + + @Test + fun findByReturnsDeviceIfFound() { + whenever(fbSimctl.listDevice("udid")).thenReturn(fbSimctlDevice) + + val actual = provider.findBy("udid") + + assertThat(actual, sameInstance(fbSimctlDevice)) + } + + @Test + fun findByReturnsNullIfNotFound() { + whenever(fbSimctl.listDevice("udid")).thenReturn(null) + + val actual = provider.findBy("udid") + + assertThat(actual, nullValue()) + } + + @Test + fun listCachesResultAndStripsMissingModelOrOs() { + val expected: List = listOf( + dev1, + FBSimctlDevice("arch", "State", "", "name", "udid", "os"), + FBSimctlDevice("arch", "State", "model", "name", "udid", ""), + dev2 + ) + whenever(fbSimctl.listSimulators()) + .thenReturn(expected) + .thenThrow(RuntimeException("Expected first invocation to be cached")) + val actual = provider.list() + val actual2 = provider.list() + + assertThat(actual, equalTo(listOf(dev1, dev2))) + assertThat(actual2, equalTo(listOf(dev1, dev2))) + } + + @Test + fun createClearsCache() { + whenever(fbSimctl.create("model1", "os", false)).thenReturn(dev1) + whenever(fbSimctl.create("model2", "os", false)).thenReturn(dev2) + whenever(fbSimctl.listSimulators()) + .thenReturn(listOf(dev1)) + .thenReturn(listOf(dev1, dev2)) + .thenThrow(RuntimeException("Expected first invocation to be cached")) + val actual1 = provider.create("model1", "os", false) + provider.list() + val actual2 = provider.create("model2", "os", false) + provider.list() + provider.list() + + assertThat(actual1, sameInstance(dev1)) + assertThat(actual2, sameInstance(dev2)) + verify(fbSimctl, times(2)).listSimulators() + } + + @Test + fun matchByUuid() { + whenever(fbSimctl.listDevice("udid")).thenReturn(dev1) + val actual = provider.match(DesiredCapabilities("udid", "model", "os", headless), emptySet()) + assertThat(actual, sameInstance(dev1)) + } + + @Test + fun matchByExistingDesiredCaps() { + whenever(fbSimctl.listSimulators()).thenReturn(listOf(dev1)) + val actual = provider.match(DesiredCapabilities(null, "model", "iOS 11", true), emptySet()) + assertThat(actual, sameInstance(dev1)) + } + + @Test + fun matchByCreating() { + whenever(fbSimctl.create("model", "os", true)).thenReturn(dev2) + val actual = provider.match(DesiredCapabilities(null, "model", "os", headless, existing = false), emptySet()) + assertThat(actual, sameInstance(dev2)) + } +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt new file mode 100644 index 00000000..35085090 --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt @@ -0,0 +1,411 @@ +package com.badoo.automation.deviceserver.host + +import com.badoo.automation.deviceserver.JsonMapper +import com.badoo.automation.deviceserver.command.CommandResult +import com.badoo.automation.deviceserver.data.* +import com.badoo.automation.deviceserver.host.management.ISimulatorHostChecker +import com.badoo.automation.deviceserver.host.management.PortAllocator +import com.badoo.automation.deviceserver.host.management.SimulatorHostChecker +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDevice +import com.badoo.automation.deviceserver.ios.simulator.ISimulator +import com.badoo.automation.deviceserver.ios.simulator.video.SimulatorVideoRecorder +import com.badoo.automation.deviceserver.mockThis +import com.nhaarman.mockito_kotlin.* +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.CoreMatchers.sameInstance +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Assert.* +import org.junit.Ignore +import org.junit.Test +import org.slf4j.Logger +import java.io.File +import java.net.URI +import java.time.Duration + +class SimulatorsNodeTest { + private val iRemote: IRemote = mockThis() + private val fbSimctl: FBSimctl = mockThis() + + init { + whenever(iRemote.fbsimctl).thenReturn(fbSimctl) + } + + private val hostChecker: ISimulatorHostChecker = mockThis() + + private val wdaPath = File("some/file/from/wdaPathProc") + + private val iSimulatorProvider: ISimulatorProvider = mockThis() + + private val ref1: DeviceRef = "Udid1-rem-ote-node" + + private val fbsimulatorDevice: FBSimctlDevice = FBSimctlDevice( + "Arch", + "State", + "Model", + "Name", + "Udid1", + "Os") + + private val fbsimulatorDevice2: FBSimctlDevice = FBSimctlDevice( + "Arch", + "State", + "Model", + "Name", + "Udid2", + "Os") + + private val portAllocator = PortAllocator(1, 10) + + private val configuredSimulatorLimit = 3 + + private val simulatorFactory: ISimulatorFactory = mockThis() + private val simulatorsNode1 = SimulatorsNode( + iRemote, + hostChecker, + configuredSimulatorLimit, + 2, + wdaPath, + iSimulatorProvider, + portAllocator, + simulatorFactory + ) + private val simulatorsNode = simulatorsNode1 + + private val desiredCapabilities: DesiredCapabilities = mockThis() + + private val simulatorMock: ISimulator = mockThis("sim1") + private val simulatorMock2: ISimulator = mockThis("sim2") + + private val expectedDeviceDTO = DeviceDTO( + "someref0", + DeviceState.CREATING, + URI("http://fbsimctl"), + URI("http://wda"), + 4444, + setOf(1, 2, 3, 37265), + DeviceInfo("", "", "", "", ""), + null, + ActualCapabilities(true, true) + ) + private val expectedDeviceDTOJson = JsonMapper().toJson(expectedDeviceDTO) + + @Test + fun shouldPrepareNodeOnlyOnce() { + simulatorsNode1.prepareNode() + + val inOrder = inOrder(hostChecker) + inOrder.verify(hostChecker).checkPrerequisites() + inOrder.verify(hostChecker).copyWdaBundleToHost() + inOrder.verify(hostChecker).cleanup() + inOrder.verify(hostChecker).setupHost() + inOrder.verifyNoMoreInteractions() + } + + @Test(expected = RuntimeException::class) + fun createDeviceAsyncFailsIfNoCapacity() { + simulatorsNode.createDeviceAsync(desiredCapabilities) + } + + @Test(expected = RuntimeException::class) + fun createDeviceAsyncFailsIfNoMatch() { + whenever(iSimulatorProvider.match(desiredCapabilities, emptySet())).thenReturn(null) + simulatorsNode.createDeviceAsync(desiredCapabilities) + } + + @Test + fun createDeviceAsyncSucceeds() { + createDeviceForTest() + + verify(simulatorFactory).newSimulator( + eq("Udid1-rem-ote-node"), + eq(iRemote), + eq(fbsimulatorDevice), + eq(DeviceAllocatedPorts(1,2, 3)), + eq("/node/specific/device/set"), + eq("some/file/from/wdaPathProc"), + any(), + eq(false), + eq("FBSimctlDevice(arch=Arch, state=State, model=Model, name=Name, udid=Udid1, os=Os)") + ) + verify(simulatorMock).prepareAsync() + + assertThat(simulatorsNode.count(), equalTo(1)) + } + + private fun createDeviceForTest(): DeviceDTO = + createDevicesForTest(simulatorMock to fbsimulatorDevice) + + private fun createTwoDevicesForTest(): DeviceDTO = + createDevicesForTest(simulatorMock to fbsimulatorDevice, simulatorMock2 to fbsimulatorDevice2) + + private fun createDevicesForTest(vararg simulatorMocks: Pair): DeviceDTO { + mockForSimulatorMocks(*simulatorMocks) + return simulatorMocks.map { + simulatorsNode.createDeviceAsync(desiredCapabilities) + }.last() + } + + private fun mockForSimulatorMocks(vararg simulatorMocks: Pair) { + whenever(iRemote.hostName).thenReturn("rem.ote.node") + whenever(iRemote.fbsimctl).thenReturn(fbSimctl) + whenever(fbSimctl.listDeviceSets()).thenReturn("/node/specific/device/set") + var fbsimmock = whenever(iSimulatorProvider.match(eq(desiredCapabilities), any())) + simulatorMocks.forEach { pair -> + fbsimmock = fbsimmock.thenReturn(pair.second) + } + + var simfac = whenever(simulatorFactory.newSimulator(any(), any(), any(), any(), any(), any(), any(), any(), any())) + simulatorMocks.forEach { pair -> + simfac = simfac.thenReturn(pair.first) + } + + simulatorMocks.forEachIndexed { index, pair -> + val it = pair.first + whenever(it.ref).thenReturn("someref$index") + whenever(it.state).thenReturn(DeviceState.CREATING) + whenever(it.info).thenReturn(DeviceInfo("","","","","")) + whenever(it.userPorts).thenReturn(DeviceAllocatedPorts(1,2,3)) + whenever(it.fbsimctlEndpoint).thenReturn(URI("http://fbsimctl")) + whenever(it.wdaEndpoint).thenReturn(URI("http://wda")) + whenever(it.calabashPort).thenReturn(4444 + index) + whenever(it.fbsimctlSubject).thenReturn("string representation of simulatorMock $index") + } + } + + + @Test + fun countStartsAtZero() { + assertThat(simulatorsNode.count(), equalTo(0)) + } + + @Test + fun disposeReportsNoErrorsOnSuccessfulDispose() { + createDeviceForTest() + + simulatorsNode.dispose() + + verify(hostChecker).killDiskCleanupThread() + verify(simulatorMock).release(any()) + } + + @Test + fun disposeReportsErrorsOnFailedDispose() { + createDeviceForTest() + + whenever(simulatorMock.release(any())).thenThrow(RuntimeException("Oh no! Actually, this is an expected test exception")) + + simulatorsNode.dispose() + + verify(hostChecker).killDiskCleanupThread() + } + + @Test + fun list() { + createTwoDevicesForTest() + + assertThat(simulatorsNode.list().count(), equalTo(2)) + } + + private val headless = true + @Test + fun supportsOnlyWhenArchIsx8664() { + assertTrue(simulatorsNode.supports(DesiredCapabilities("", "", "", headless, arch = "x86_64"))) + assertFalse(simulatorsNode.supports(DesiredCapabilities("", "", "", headless, arch ="notx86_64"))) + } + + @Test + fun totalCapacityReturnsNonzeroWhenSupported() { + assertThat(simulatorsNode.totalCapacity(DesiredCapabilities("not", "des", "", headless)), + equalTo(configuredSimulatorLimit)) + } + + @Test + fun totalCapacityReturnsZeroWhenUnsupported() { + assertThat(simulatorsNode.totalCapacity(DesiredCapabilities("not", "des", "ir", headless, arch = "ed")), equalTo(0)) + } + + @Test + fun capacityRemainingReturns1WhenNothingIsAllocated() { + assertThat(simulatorsNode.capacityRemaining(desiredCapabilities), equalTo(1F)) + + createDeviceForTest() + + assertThat(simulatorsNode.capacityRemaining(desiredCapabilities), equalTo(2F/3)) + } + + @Test + fun approveAccess() { + createDeviceForTest() + + val bundleId = "somebundle" + + simulatorsNode.approveAccess(ref1, bundleId) + + verify(simulatorMock).approveAccess(bundleId) + } + + @Test + fun clearSafariCookies() { + createDeviceForTest() + + simulatorsNode.clearSafariCookies(ref1) + + verify(simulatorMock).clearSafariCookies() + } + + @Test + fun endpointFor() { + createDeviceForTest() + + simulatorsNode.endpointFor(ref1, 3) + + verify(simulatorMock).endpointFor(3) + } + + @Test + fun getDeviceDTO() { + createDeviceForTest() + + val actual = simulatorsNode.getDeviceDTO(ref1) + + assertThat(actual, equalTo(DeviceDTO( + "someref0", + DeviceState.CREATING, + URI("http://fbsimctl"), + URI("http://wda"), + 4444, + setOf(1,2,3, 37265), + DeviceInfo("", "", "", "", ""), + null, + ActualCapabilities(true, true) + ))) + } + + @Test + fun getDeviceDTOJSON() { + createDeviceForTest() + + val actual = simulatorsNode.getDeviceDTO(ref1) + val actualJson = JsonMapper().toJson(actual) + + assertThat(actualJson, equalTo(expectedDeviceDTOJson)) + } + + @Test + fun lastCrashLog() { + createDeviceForTest() + + simulatorsNode.lastCrashLog(ref1) + + verify(simulatorMock).lastCrashLog() + } + + @Test + fun deleteReleaseIgnoresNonexistentRef() { + assertThat(simulatorsNode.deleteRelease(ref1, "anything"), equalTo(false)) + } + + @Test + fun deleteReleaseReleasesExistingRef() { + createTwoDevicesForTest() + assertThat(simulatorsNode.count(), equalTo(2)) + assertThat(portAllocator.available(), equalTo(4)) + + val actual = simulatorsNode.deleteRelease(ref1, "test") + assertThat(actual, equalTo(true)) + verify(simulatorMock).release(any()) + assertThat(simulatorsNode.count(), equalTo(1)) + assertThat(portAllocator.available(), equalTo(7)) + } + + @Test + fun resetAsync() { + createDeviceForTest() + + simulatorsNode.resetAsync(ref1) + + verify(simulatorMock).resetAsync() + } + + @Test + fun state() { + createDeviceForTest() + val expected = SimulatorStatusDTO(false, false, false, DeviceState.CREATING.value, null) + + whenever(simulatorMock.status()).thenReturn(expected) + + val actual = simulatorsNode.state(ref1) + + assertThat(actual, sameInstance(expected)) + + verify(simulatorMock).status() + } + + private val videoRecorderMock = mockThis() + + @Test + fun videoRecorderDelete() { + createDeviceForTest() + + whenever(simulatorMock.videoRecorder).thenReturn(videoRecorderMock) + + simulatorsNode.videoRecordingDelete(ref1) + + verify(videoRecorderMock).delete() + } + + @Test + fun videoRecorderGet() { + createDeviceForTest() + + whenever(simulatorMock.videoRecorder).thenReturn(videoRecorderMock) + val bytes = ByteArray(23) + + whenever(videoRecorderMock.getRecording()).thenReturn(bytes) + + val byteArray = simulatorsNode.videoRecordingGet(ref1) + + assertThat(byteArray, sameInstance(bytes)) + } + + @Test + fun videoRecorderStart() { + createDeviceForTest() + + whenever(simulatorMock.videoRecorder).thenReturn(videoRecorderMock) + + simulatorsNode.videoRecordingStart(ref1) + + verify(videoRecorderMock).start() + } + + @Test + fun videoRecorderStop() { + createDeviceForTest() + + whenever(simulatorMock.videoRecorder).thenReturn(videoRecorderMock) + + simulatorsNode.videoRecordingStop(ref1) + + verify(videoRecorderMock).stop() + + } + +} + +class SimulatorHostCheckerTest { + private val iRemote: IRemote = mockThis() + + private val hostChecker = SimulatorHostChecker(iRemote, Duration.ofMillis(1)) + + @Test(expected = RuntimeException::class) + fun checkPrerequisitesDislikesBadXcode() { + whenever(iRemote.execIgnoringErrors("xcodebuild -version".split(" "))) + .thenReturn(CommandResult( + "Xcode x.", "", ByteArray(0), + 0, true)) + + hostChecker.checkPrerequisites() + } +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/management/DesiredCapabilitiesMatcherTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/management/DesiredCapabilitiesMatcherTest.kt new file mode 100644 index 00000000..17e8c77a --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/management/DesiredCapabilitiesMatcherTest.kt @@ -0,0 +1,43 @@ +package com.badoo.automation.deviceserver.host.management + +import org.junit.Test +import kotlin.test.assertFalse +import kotlin.test.assertTrue + +class DesiredCapabilitiesMatcherTest { + @Test + fun isRuntimeMatchShouldMatchExact() { + val matcher = DesiredCapabilitiesMatcher() + + assertTrue { + matcher.isRuntimeMatch("iOS 11", "iOS 11.2") + } + } + + @Test + fun isRuntimeMatchShouldMatchPartial() { + val matcher = DesiredCapabilitiesMatcher() + + assertTrue { + matcher.isRuntimeMatch("iOS 11", "iOS 11.2") + } + } + + @Test + fun isRuntimeMatchShouldNotMatchDifferentNames() { + val matcher = DesiredCapabilitiesMatcher() + + assertFalse { + matcher.isRuntimeMatch("tvOS 11.2", "iOS 11.2") + } + } + + @Test + fun isRuntimeMatchShouldNotMatchDifferentVersions() { + val matcher = DesiredCapabilitiesMatcher() + + assertFalse { + matcher.isRuntimeMatch("iOS 11.3", "iOS 11.2") + } + } +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/DeviceManagerTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/DeviceManagerTest.kt new file mode 100644 index 00000000..59b06474 --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/DeviceManagerTest.kt @@ -0,0 +1,198 @@ +package com.badoo.automation.deviceserver.ios + +import com.badoo.automation.deviceserver.DeviceServerConfig +import com.badoo.automation.deviceserver.NodeConfig +import com.badoo.automation.deviceserver.data.* +import com.badoo.automation.deviceserver.deviceDTOStub +import com.badoo.automation.deviceserver.host.ISimulatorsNode +import com.badoo.automation.deviceserver.host.management.DeviceManager +import com.badoo.automation.deviceserver.host.management.IAutoreleaseLooper +import com.badoo.automation.deviceserver.host.management.IHostFactory +import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException +import com.badoo.automation.deviceserver.mockThis +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.whenever +import org.hamcrest.MatcherAssert.assertThat +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.sameInstance +import org.junit.Test +import org.mockito.Mockito +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import java.net.URL + +class DeviceManagerTest { + private var desiredCaps = DesiredCapabilities("udid", "model", "os", true) + + private var someUrl = URL("http://whatever") + + private val ref: DeviceRef = "just-a-device-ref" + private val expectedDto = deviceDTOStub("mockDTO") + private val bundleId = "some.bundle.id" + private val hostZero = mockHostWithTotalCapacity(0, true) + private val hostOne = mockHostWithTotalCapacity(1, true) + private val hostTwo = mockHostWithTotalCapacity(2, true) + + private val hostsMap = mapOf( + "zero" to hostZero, "one" to hostOne, "two" to hostTwo, + "unreachable" to mockHostWithTotalCapacity(4, false) + ) + private val hostFactory: IHostFactory = object : IHostFactory { + override fun getHostFromConfig(config: NodeConfig): ISimulatorsNode { + val nodeName = config.host + return hostsMap[nodeName]!! + } + } + + private fun mockHostWithTotalCapacity(total: Int, reachable: Boolean): ISimulatorsNode { + val m: ISimulatorsNode = Mockito.mock(ISimulatorsNode::class.java, "mockHost$total") + whenever(m.totalCapacity(desiredCaps)).thenReturn(total) + whenever(m.isReachable()).thenReturn(reachable) + return m + } + + private val activeDevices: IActiveDevices = mockThis() + + private val autoreleaseLooper: IAutoreleaseLooper = mockThis() + + private val deviceManager = DeviceManager( + DeviceServerConfig( + emptyMap(), + listOf() + ), + hostFactory, + activeDevices, + autoreleaseLooper + ) + + @Test + fun getDeviceRefsEmpty() { + hostsMap.forEach { _, mock -> whenever(mock.list()).thenReturn(emptyList()) } + + val actualRefs = deviceManager.getDeviceRefs() // .list() + + assertThat(actualRefs.size, equalTo(0)) + } + + @Test + fun deleteReleaseDeviceThatHasBeenReleased() { + val sessionId = "defaultSessionId" + whenever(activeDevices.getNodeFor(ref)).thenThrow( + DeviceNotFoundException("Device [$ref] not found in [$sessionId] activeDevices") + ) + deviceManager.deleteReleaseDevice(ref, "httpRequest") + verify(activeDevices, times(0)).unregisterDeleteDevice(any()) + } + + private fun withDeviceOnHost(host: ISimulatorsNode, block: () -> Unit) { + whenever(activeDevices.getNodeFor(ref)).thenReturn(host) + block() + } + + @Test + fun getMethodReturningDeviceDTO() { + withDeviceOnHost(hostTwo ) { + whenever(hostTwo.getDeviceDTO(ref)).thenReturn(expectedDto) + + val actualDto = deviceManager.getGetDeviceDTO(ref) + + assertThat(actualDto, equalTo(expectedDto)) + } + } + + @Test + fun clearSafariCookies() { + withDeviceOnHost(hostTwo ) { + deviceManager.clearSafariCookies(ref) + verify(hostTwo).clearSafariCookies(ref) + } + } + + @Test + fun resetAsyncDevice() { + withDeviceOnHost(hostTwo ) { + + deviceManager.resetAsyncDevice(ref) + + verify(hostTwo).resetAsync(ref) + } + } + + @Test + fun approveAccess() { + withDeviceOnHost(hostTwo ) { + deviceManager.approveAccess(ref, bundleId) + verify(hostTwo).approveAccess(ref, bundleId) + } + } + + @Test + fun getEndpointFor() { + withDeviceOnHost(hostTwo ) { + whenever(hostTwo.endpointFor(ref, 1234)).thenReturn(someUrl) + val actual = deviceManager.getEndpointFor(ref, 1234) + assertThat(actual, equalTo(someUrl)) + } + } + + @Test + fun getLastCrashLog() { + withDeviceOnHost(hostTwo) { + val crashLog = CrashLog("some/path", "stdout from cat of filename") + whenever(hostTwo.lastCrashLog(ref)).thenReturn(crashLog) + val actual = deviceManager.getLastCrashLog(ref) + assertThat(actual, equalTo(crashLog)) + } + } + + @Test + fun startVideo() { + withDeviceOnHost(hostTwo) { + deviceManager.startVideo(ref) + verify(hostTwo).videoRecordingStart(ref) + } + } + + @Test + fun stopVideo() { + withDeviceOnHost(hostTwo) { + deviceManager.stopVideo(ref) + verify(hostTwo).videoRecordingStop(ref) + } + } + + @Test + fun getVideo() { + withDeviceOnHost(hostTwo) { + val bytes = ByteArray(3) + whenever(hostTwo.videoRecordingGet(ref)).thenReturn(bytes) + val actual = deviceManager.getVideo(ref) + assertThat(actual, sameInstance(bytes)) + } + } + + @Test + fun deleteVideo() { + withDeviceOnHost(hostTwo) { + deviceManager.deleteVideo(ref) + verify(hostTwo).videoRecordingDelete(ref) + } + } + + @Test + fun getDeviceState() { // deviceStateDTO + withDeviceOnHost(hostTwo ) { + val deviceOrSimulatorStatusBloodyContradictoryNonsense = SimulatorStatusDTO( + false, false, false, DeviceState.NONE.value, null) + whenever(hostTwo.state(ref)).thenReturn(deviceOrSimulatorStatusBloodyContradictoryNonsense) + val actual = deviceManager.getDeviceState(ref) + assertThat(actual, equalTo(deviceOrSimulatorStatusBloodyContradictoryNonsense)) + } + } + + @Test + fun autoReleaseLoopIsCalledByConstructor() { + deviceManager.launchAutoReleaseLoop() + verify(autoreleaseLooper).autoreleaseLoop(deviceManager) + } +} diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/HostFactoryTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/HostFactoryTest.kt new file mode 100644 index 00000000..549c6789 --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/HostFactoryTest.kt @@ -0,0 +1,62 @@ +package com.badoo.automation.deviceserver.ios + +import com.badoo.automation.deviceserver.DeviceServerConfig +import com.badoo.automation.deviceserver.NodeConfig +import com.badoo.automation.deviceserver.host.HostFactory +import com.badoo.automation.deviceserver.host.IRemote +import com.badoo.automation.deviceserver.host.ISimulatorHostProvider +import com.badoo.automation.deviceserver.host.ISimulatorsNode +import com.badoo.automation.deviceserver.mockThis +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.eq +import com.nhaarman.mockito_kotlin.whenever +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.CoreMatchers.sameInstance +import org.hamcrest.MatcherAssert.assertThat +import org.junit.Test + +class HostFactoryTest { + companion object { + const val WDA_BUNDLE_PATH_STRING = "/ABSOLUTE/PATH/ios/facebook/simulators/WebDriverAgentRunner-Runner.app" + const val WDA_DEVICE_BUNDLE_PATH_STRING = "/ABSOLUTE/PATH/ios/facebook/devices/WebDriverAgentRunner-Runner.app" + } + init { + System.setProperty("wda.bundle.path", WDA_BUNDLE_PATH_STRING) + System.setProperty("wda.device.bundle.path", WDA_DEVICE_BUNDLE_PATH_STRING) + System.setProperty("device.server.config.path", "/some/dir/some/file") + } + + private val hostName: String = "some.node.name" + + private val deviceServerConfig = DeviceServerConfig( + mapOf("" to "1"), + listOf(NodeConfig(host = hostName, simulatorLimit = 99, concurrentBoots = 9)) + ) + + private var remoteMock: IRemote = mockThis() + private var remoteMockProvider: (String, String) -> IRemote = { host, _ -> + assertThat(host, equalTo(hostName)) + remoteMock + } + + private var simulatorHostProvider: ISimulatorHostProvider = mockThis() + + private var simNodeMock: ISimulatorsNode = mockThis() + + private var factory = HostFactory(remoteMockProvider, simulatorHostProvider) + + @Test + fun getHostFromConfigHandlesReachableLocalDevice() { + whenever(remoteMock.isReachable()).thenReturn(true) + whenever(remoteMock.isLocalhost()).thenReturn(true) + whenever(simulatorHostProvider.simulatorsNode( + eq(remoteMock), eq(99), eq(9), + any() + )).thenReturn(simNodeMock) + + val nodeConfig = deviceServerConfig.nodes.first() + val actual = factory.getHostFromConfig(nodeConfig) + + assertThat(actual, sameInstance(simNodeMock)) + } +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/SessionTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/SessionTest.kt new file mode 100644 index 00000000..1af6313a --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/SessionTest.kt @@ -0,0 +1,99 @@ +package com.badoo.automation.deviceserver.ios + +import com.badoo.automation.deviceserver.data.DeviceRef +import com.badoo.automation.deviceserver.host.ISimulatorsNode +import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException +import com.badoo.automation.deviceserver.mockThis +import org.hamcrest.Matchers.* +import org.junit.Assert.assertThat +import org.junit.Test +import java.time.Duration + +class SessionTest { + private var host1: ISimulatorsNode = mockThis() + private var host2: ISimulatorsNode = mockThis() + + private var sillySeconds: Long = 42L + private val session = ActiveDevices(currentTimeSeconds = { sillySeconds++ }) + + private val releaseAfterSecs = Duration.ofSeconds(5) + + private val deviceRef1: DeviceRef = "hello-1" + private val deviceRef2: DeviceRef = "hello-2" + + @Test + fun deviceRefsAreEmptyInitially() { + assertThat(session.deviceRefs(), empty()) + } + + @Test + fun registerDevice() { + session.registerDevice(deviceRef1, host1, releaseAfterSecs, null) + assertThat(session.deviceRefs().size, equalTo(1)) + } + + @Test + fun unregisterNodeDevices() { + session.registerDevice(deviceRef1, host1, releaseAfterSecs, null) + session.registerDevice(deviceRef2, host2, releaseAfterSecs, null) + + session.unregisterNodeDevices(host1) + + assertThat(session.deviceRefs(), equalTo(setOf(deviceRef2))) + } + + @Test + fun tryGetNodeForReturnsDeviceIfPresent() { + session.registerDevice(deviceRef1, host1, releaseAfterSecs, null) + session.registerDevice(deviceRef2, host2, releaseAfterSecs, null) + assertThat(session.getNodeFor(deviceRef1), equalTo(host1)) + assertThat(session.getNodeFor(deviceRef2), equalTo(host2)) + } + + @Test(expected = DeviceNotFoundException::class) + fun getNodeForThrowsIfAbsent() { + session.getNodeFor(deviceRef1) + } + + @Test + fun getNodeForReturnsDeviceIfPresent() { + session.registerDevice(deviceRef1, host1, releaseAfterSecs, null) + session.registerDevice(deviceRef2, host2, releaseAfterSecs, null) + assertThat(session.getNodeFor(deviceRef1), equalTo(host1)) + assertThat(session.getNodeFor(deviceRef2), equalTo(host2)) + } + + @Test + fun refreshDevice() { + session.registerDevice(deviceRef1, host1, releaseAfterSecs, null) + val preReleaseSeconds = session.nextReleaseAtSeconds() + + session.getNodeFor(deviceRef1) + + assertThat(session.nextReleaseAtSeconds(), greaterThan(preReleaseSeconds)) + } + + @Test + fun unregisterDeleteDevice() { + session.registerDevice(deviceRef1, host1, releaseAfterSecs, null) + session.registerDevice(deviceRef2, host2, releaseAfterSecs, null) + session.unregisterDeleteDevice(deviceRef2) + assertThat(session.deviceRefs().size, equalTo(1)) + assertThat(session.deviceRefs().first(), equalTo(deviceRef1)) + } + + @Test + fun readyForRelease() { + session.registerDevice(deviceRef1, host1, Duration.ofSeconds(1), null) + // Implementation detail: secondsSinceEpoch always increments once per call. + session.registerDevice(deviceRef2, host2, Duration.ofSeconds(1), null) + val deviceList = session.readyForRelease() + assertThat(deviceList.size, equalTo(2)) + } + + @Test + fun nextReleaseAtSeconds() { + session.registerDevice(deviceRef1, host1, Duration.ofSeconds(10), null) + assertThat(session.nextReleaseAtSeconds(), equalTo(10L + 42L)) + } +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlTest.kt new file mode 100644 index 00000000..0fc6d22f --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlTest.kt @@ -0,0 +1,36 @@ +package com.badoo.automation.deviceserver.ios.fbsimctl + +import com.badoo.automation.deviceserver.anyType +import com.badoo.automation.deviceserver.command.CommandResult +import com.badoo.automation.deviceserver.command.IShellCommand +import com.nhaarman.mockito_kotlin.whenever +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentMatchers.* +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import org.slf4j.Marker + +class FBSimctlTest { + @Mock private lateinit var executor: IShellCommand + @Mock private lateinit var parser: IFBSimctlResponseParser + + @Before fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @Test fun mustTrimLastNewLine() { + whenever(executor.exec(anyList(), anyMap(), anyType(), anyBoolean(), any(), anyType())).thenReturn(CommandResult("/a\n", "", ByteArray(0), 0)) + val fbSimctl = FBSimctl(executor, parser) + val deviceSets = fbSimctl.listDeviceSets() + Assert.assertEquals("/a", deviceSets) + } + + @Test(expected = FBSimctlError::class) + fun shouldThrowWhenNoDeviceSets() { + whenever(executor.exec(anyList(), anyMap(), anyType(), anyBoolean(), any(), anyType())).thenReturn(CommandResult("asdfa\n", "", ByteArray(0), 0)) + val fbSimctl = FBSimctl(executor, parser) + fbSimctl.listDeviceSets() + } +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FbSimctlResponseParserTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FbSimctlResponseParserTest.kt new file mode 100644 index 00000000..d9629e75 --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FbSimctlResponseParserTest.kt @@ -0,0 +1,109 @@ +package com.badoo.automation.deviceserver.ios.fbsimctl + +import org.hamcrest.Matchers.equalTo +import org.hamcrest.Matchers.instanceOf +import org.junit.Assert.* +import org.junit.Test + +class FbSimctlResponseParserTest { + private val simulatorsListString = """ + {"event_name":"list","timestamp":1516204149,"subject":{"arch":"x86_64","state":"Shutdown","model":"iPhone X","name":"iPhone X","udid":"54BC5B1F-7144-450C-8459-D61C2206D1F4","os":"iOS 11.2"},"event_type":"discrete"} + {"event_name":"list","timestamp":1516204149,"subject":{"arch":"x86_64","state":"Booted","model":"iPhone 8","name":"iPhone 8","udid":"4B740F75-D83E-4DBA-8BA1-1A82A68FA27E","os":"iOS 11.2"},"event_type":"discrete"} + {"event_name":"list","timestamp":1516204149,"subject":{"arch":"x86_64","state":"Shutdown","model":"iPad Pro (12.9-inch) (2nd generation)","name":"iPad Pro (12.9-inch) (2nd generation)","udid":"82D60C16-CBF7-4AB0-85EA-4FB778C0D7CD","os":"iOS 11.2"},"event_type":"discrete"} + """.trimIndent() + + private val listAppsString = """ + {"event_name":"list_apps","timestamp":1516247902,"subject":[{"data_container":null,"bundle":{"path":"\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/Applications\/MobileSlideShow.app","bundle_id":"com.apple.mobileslideshow","binary":{"name":"MobileSlideShow","path":"\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/Applications\/MobileSlideShow.app\/MobileSlideShow","architectures":["x86_64"]},"name":"MobileSlideShow"},"install_type":"system"},{"data_container":"\/Users\/}|{eka\/Library\/Developer\/CoreSimulator\/Devices\/4B740F75-D83E-4DBA-8BA1-1A82A68FA27E\/data\/Containers\/Data\/Application\/B6B0251A-282C-422B-B379-3D6A1052B620","bundle":{"path":"\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/Applications\/Maps.app","bundle_id":"com.apple.Maps","binary":{"name":"Maps","path":"\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/Applications\/Maps.app\/Maps","architectures":["x86_64"]},"name":"Maps"},"install_type":"system"},{"data_container":null,"bundle":{"path":"\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/Applications\/MobileSMS.app","bundle_id":"com.apple.MobileSMS","binary":{"name":"MobileSMS","path":"\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/Applications\/MobileSMS.app\/MobileSMS","architectures":["x86_64"]},"name":"MobileSMS"},"install_type":"system"},{"data_container":"\/Users\/}|{eka\/Library\/Developer\/CoreSimulator\/Devices\/4B740F75-D83E-4DBA-8BA1-1A82A68FA27E\/data\/Containers\/Data\/Application\/1F486790-36FA-4DA1-8BC4-EC347466C06A","bundle":{"path":"\/Users\/}|{eka\/Library\/Developer\/CoreSimulator\/Devices\/4B740F75-D83E-4DBA-8BA1-1A82A68FA27E\/data\/Containers\/Bundle\/Application\/DE199219-B5B3-4DAA-92E4-37A9476B4885\/Bumble.app","bundle_id":"com.moxco.bumble","binary":{"name":"Bumble","path":"\/Users\/}|{eka\/Library\/Developer\/CoreSimulator\/Devices\/4B740F75-D83E-4DBA-8BA1-1A82A68FA27E\/data\/Containers\/Bundle\/Application\/DE199219-B5B3-4DAA-92E4-37A9476B4885\/Bumble.app\/Bumble","architectures":["x86_64"]},"name":"Bumble"},"install_type":"user"},{"data_container":"\/Users\/}|{eka\/Library\/Developer\/CoreSimulator\/Devices\/4B740F75-D83E-4DBA-8BA1-1A82A68FA27E\/data\/Containers\/Data\/Application\/48CF1917-F972-464E-9EA1-EF9C4140FEBB","bundle":{"path":"\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/Applications\/MobileSafari.app","bundle_id":"com.apple.mobilesafari","binary":{"name":"MobileSafari","path":"\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/Applications\/MobileSafari.app\/MobileSafari","architectures":["x86_64"]},"name":"MobileSafari"},"install_type":"system"}],"event_type":"discrete"} + {"event_type":"started","subject":{},"timestamp":1516247902,"target":{"arch":"x86_64","state":"Booted","model":"iPhone 8","name":"iPhone 8","udid":"4B740F75-D83E-4DBA-8BA1-1A82A68FA27E","os":"iOS 11.2"},"event_name":"list_apps"} + {"event_type":"ended","subject":{},"timestamp":1516247902,"target":{"arch":"x86_64","state":"Booted","model":"iPhone 8","name":"iPhone 8","udid":"4B740F75-D83E-4DBA-8BA1-1A82A68FA27E","os":"iOS 11.2"},"event_name":"list_apps"} + """.trimIndent() + + // fbsimctl --json FA2EC53F-E71A-4BAF-8686-840813C5348F diagnose + private val diagnoseRunningStrings = """ + {"event_type":"started","subject":{"type":"all"},"timestamp":1516634884,"target":{"arch":"x86_64","state":"Booted","model":"iPhone 6","name":"iPhone 6","udid":"FA2EC53F-E71A-4BAF-8686-840813C5348F","os":"iOS 11.0"},"event_name":"diagnose"} + {"event_type":"ended","subject":{"type":"all"},"timestamp":1516634884,"target":{"arch":"x86_64","state":"Booted","model":"iPhone 6","name":"iPhone 6","udid":"FA2EC53F-E71A-4BAF-8686-840813C5348F","os":"iOS 11.0"},"event_name":"diagnose"} + {"event_name":"diagnostic","timestamp":1516634884,"subject":{"short_name":"system_log","human_name":"System Log","file_type":"log","location":"\/Users\/}|{eka\/Library\/Logs\/CoreSimulator\/FA2EC53F-E71A-4BAF-8686-840813C5348F\/system.log"},"event_type":"discrete"} + {"event_name":"diagnostic","timestamp":1516634884,"subject":{"short_name":"coresimulator","human_name":"Core Simulator Log","file_type":"log","location":"\/Users\/}|{eka\/Library\/Logs\/CoreSimulator\/CoreSimulator.log"},"event_type":"discrete"} + {"event_name":"diagnostic","timestamp":1516634884,"subject":{"short_name":"launchd_bootstrap","human_name":"Launchd Bootstrap","file_type":"plist","location":"\/Users\/}|{eka\/Library\/Developer\/CoreSimulator\/Devices\/FA2EC53F-E71A-4BAF-8686-840813C5348F\/data\/var\/run\/launchd_bootstrap.plist"},"event_type":"discrete"} + """.trimIndent() + + private val diagnoseShutdownStrings = """ + {"event_type":"started","subject":{"type":"all"},"timestamp":1516635037,"target":{"arch":"x86_64","state":"Shutdown","model":"iPhone X","name":"iPhone X","udid":"54BC5B1F-7144-450C-8459-D61C2206D1F4","os":"iOS 11.2"},"event_name":"diagnose"} + {"event_type":"ended","subject":{"type":"all"},"timestamp":1516635037,"target":{"arch":"x86_64","state":"Shutdown","model":"iPhone X","name":"iPhone X","udid":"54BC5B1F-7144-450C-8459-D61C2206D1F4","os":"iOS 11.2"},"event_name":"diagnose"} + {"event_name":"diagnostic","timestamp":1516635037,"subject":{"short_name":"coresimulator","human_name":"Core Simulator Log","file_type":"log","location":"\/Users\/}|{eka\/Library\/Logs\/CoreSimulator\/CoreSimulator.log"},"event_type":"discrete"} + """.trimIndent() + + private val simulatorCreateStrings = """ + {"event_name":"create","timestamp":1521028581,"subject":{"device":"iPhone 6","os":"iOS 11.2","aux_directory":null,"architecture":"x86_64"},"event_type":"started"} + {"event_name":"log","timestamp":1521028581,"level":"info","subject":"Did Change State => Booting","event_type":"discrete"} + {"event_name":"log","timestamp":1521028581,"level":"info","subject":"Did Change State => Booted","event_type":"discrete"} + {"event_name":"log","timestamp":1521028581,"level":"info","subject":"Simulator Did launch => Process launchd_sim | PID 88778","event_type":"discrete"} + {"event_name":"create","timestamp":1521028581,"subject":{"pid":0,"arch":"x86_64","os":"iOS 11.2","container-pid":0,"model":"iPhone 6","udid":"7CA9DCE7-22A2-434B-A9EE-3E2A497E3881","name":"iPhone 6","state":"Shutdown"},"event_type":"ended"} + """.trimIndent() + + private val simulatorListDeviceHttp = """ + {"status":"success","subject":[{"event_name":"list","timestamp":1518060205,"subject":{"arch":"x86_64","state":"Booted","model":"iPhone 6","name":"iPhone 6","os":"iOS 11.0","udid":"5913965A-EF5A-488B-91E0-1FDFF93086FD"},"event_type":"discrete"}],"events":[]} + """.trimIndent() + + @Test fun parseList() { + val parsedValue = FBSimctlResponseParser().parse(simulatorsListString) + assertEquals("Wrong element count", 3, parsedValue.size) + val deviceAsMap = parsedValue[0]["subject"] as Map<*, *> + assertEquals("54BC5B1F-7144-450C-8459-D61C2206D1F4", deviceAsMap["udid"]) + } + + @Test fun parseCreateDevice() { + val parsedValue = FBSimctlResponseParser().parseDeviceCreation(simulatorCreateStrings, false) + assertEquals("7CA9DCE7-22A2-434B-A9EE-3E2A497E3881", parsedValue.udid) + } + + @Test fun parseDeviceList() { + val parsedValue = FBSimctlResponseParser().parseDeviceList(simulatorsListString) + val fbSimctlDevice = parsedValue[0] + + assertEquals("Wrong element count", 3, parsedValue.size) + assertThat(fbSimctlDevice, instanceOf(FBSimctlDevice::class.java)) + assertThat(fbSimctlDevice.udid, equalTo("54BC5B1F-7144-450C-8459-D61C2206D1F4")) + } + + @Test fun parseDeviceListWithUntrustedDevice() { + val list = """{"event_name":"list","timestamp":1527060706,"subject":{"state":"Booted","udid":"1aa0a00a0a00aaa000a000a00a0a000a0000a000"},"event_type":"discrete"}""" + + val parsedValue = FBSimctlResponseParser().parseDeviceList(list) + val fbSimctlDevice = parsedValue[0] + + assertEquals("Wrong element count", 1, parsedValue.size) + assertThat(fbSimctlDevice, instanceOf(FBSimctlDevice::class.java)) + assertThat(fbSimctlDevice.udid, equalTo("1aa0a00a0a00aaa000a000a00a0a000a0000a000")) + } + + @Test fun parseDeviceListHttp() { + val fbSimctlDevice = FBSimctlResponseParser().parseDeviceListHttp(simulatorListDeviceHttp) + + assertThat(fbSimctlDevice, instanceOf(FBSimctlDevice::class.java)) + assertThat(fbSimctlDevice.udid, equalTo("5913965A-EF5A-488B-91E0-1FDFF93086FD")) + } + + @Test fun parseAppsList() { + val bumbleAppIndex = 3 + val parsedValue = FBSimctlResponseParser().parseApplicationsList(listAppsString) + val fbSimctlAppInfo = parsedValue[bumbleAppIndex] + + assertEquals("Wrong element count", 5, parsedValue.size) + assertThat(fbSimctlAppInfo, instanceOf(FBSimctlAppInfo::class.java)) + assertThat(fbSimctlAppInfo.bundle.bundle_id, equalTo("com.moxco.bumble")) + } + + @Test fun diagnoseSyslogLocationRunning() { + val expectedSyslogLocation = "/Users/}|{eka/Library/Logs/CoreSimulator/FA2EC53F-E71A-4BAF-8686-840813C5348F/system.log" + val parsedValue = FBSimctlResponseParser().parseDiagnosticInfo(diagnoseRunningStrings) + assertEquals("Wrong syslog location", expectedSyslogLocation, parsedValue.sysLogLocation) + } + + @Test fun diagnoseSyslogLocationShutdown() { + val expectedCoreSimulatorLogLocation = "/Users/}|{eka/Library/Logs/CoreSimulator/CoreSimulator.log" + val parsedValue = FBSimctlResponseParser().parseDiagnosticInfo(diagnoseShutdownStrings) + assertNull("Syslog should be null", parsedValue.sysLogLocation) + assertEquals("Wrong syslog location", expectedCoreSimulatorLogLocation, parsedValue.coreSimulatorLogLocation) + } +} diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/DeviceFbsimctlProcTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/DeviceFbsimctlProcTest.kt new file mode 100644 index 00000000..503f2869 --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/DeviceFbsimctlProcTest.kt @@ -0,0 +1,66 @@ +package com.badoo.automation.deviceserver.ios.proc + +import com.badoo.automation.deviceserver.command.ChildProcess +import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.host.IRemote +import com.badoo.automation.deviceserver.ios.device.DeviceFbsimctlProc +import com.nhaarman.mockito_kotlin.whenever +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import java.net.URI +import kotlin.test.assertEquals + +class DeviceFbsimctlProcTest { + @Mock + private lateinit var remote: IRemote + private val udid: UDID = "UDID" + @Mock + private lateinit var endpoint: URI + @Mock + private lateinit var childProcess: ChildProcess + private lateinit var actualCommand: List + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(remote.hostName).thenReturn("hostName") + whenever(remote.userName).thenReturn("userName") + whenever(endpoint.port).thenReturn(1) + } + + @Test + fun start() { + DeviceFbsimctlProc( + remote, + udid, + endpoint, + headless = false, + childFactory = this::childFactory + ).start() + val expectedCommand = listOf( + "/usr/local/bin/fbsimctl", + "--json", + "UDID", + "listen", + "--http", + "1" + ) + + assertEquals(expectedCommand, actualCommand, "Wrong command") + } + + @Suppress("UNUSED_PARAMETER") + private fun childFactory( + remoteHost: String, + username: String, + cmd: List, + isInteractiveShell: Boolean, + out_reader: (line: String) -> Unit, + err_reader: (line: String) -> Unit + ): ChildProcess { + actualCommand = cmd + return childProcess + } +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProcTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProcTest.kt new file mode 100644 index 00000000..73a17e43 --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProcTest.kt @@ -0,0 +1,77 @@ +package com.badoo.automation.deviceserver.ios.proc + +import com.badoo.automation.deviceserver.command.ChildProcess +import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.host.IRemote +import com.nhaarman.mockito_kotlin.whenever +import org.junit.Before +import org.junit.Test +import org.mockito.Mock +import org.mockito.MockitoAnnotations +import java.net.URI +import kotlin.test.assertEquals + +class FbsimctlProcTest { + @Mock private lateinit var remote: IRemote + private val udid: UDID = "UDID" + @Mock private lateinit var endpoint: URI + @Mock private lateinit var childProcess: ChildProcess + private lateinit var actualCommand: List + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + whenever(remote.hostName).thenReturn("hostName") + whenever(remote.userName).thenReturn("userName") + whenever(endpoint.port).thenReturn(1) + } + + @Test + fun startHeadless() { + val headless = true + FbsimctlProc(remote, udid, endpoint, headless, this::childFactory).start() + val expectedCommand = listOf( + "/usr/local/bin/fbsimctl", + "--json", + "UDID", + "boot", + "--direct-launch", + "--", + "listen", + "--http", + "1" + ) + + assertEquals(expectedCommand, actualCommand, "Wrong command") + } + @Test + fun startHeaded() { + val headless = false + FbsimctlProc(remote, udid, endpoint, headless, this::childFactory).start() + val expectedCommand = listOf( + "/usr/local/bin/fbsimctl", + "--json", + "UDID", + "boot", + "--", + "listen", + "--http", + "1" + ) + + assertEquals(expectedCommand, actualCommand, "Wrong command") + } + + @Suppress("UNUSED_PARAMETER") + private fun childFactory( + remoteHost: String, + username: String, + cmd: List, + isInteractiveShell: Boolean, + out_reader: (line: String) -> Unit, + err_reader: (line: String) -> Unit + ): ChildProcess { + actualCommand = cmd + return childProcess + } +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorBackupTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorBackupTest.kt new file mode 100644 index 00000000..b13283e2 --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorBackupTest.kt @@ -0,0 +1,92 @@ +package com.badoo.automation.deviceserver.ios.simulator + +import com.badoo.automation.deviceserver.command.CommandResult +import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.host.IRemote +import com.badoo.automation.deviceserver.ios.simulator.backup.ISimulatorBackup +import com.badoo.automation.deviceserver.ios.simulator.backup.SimulatorBackup +import com.badoo.automation.deviceserver.ios.simulator.backup.SimulatorBackupError +import com.nhaarman.mockito_kotlin.firstValue +import com.nhaarman.mockito_kotlin.whenever +import org.hamcrest.Matchers.matchesPattern +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.Mock +import org.mockito.Mockito.* +import org.mockito.MockitoAnnotations + +class SimulatorBackupTest { + private val metaJson = """ + {"version":1,"created":"2018-01-12 01:46:48 +0000"} + """.trimIndent() + + @Mock private lateinit var remote: IRemote + private val udid: UDID = "M-Y-P-H-O-N-E" + private val deviceSetPath = "/home/user/backup" + private val captor = ArgumentCaptor.forClass(listOf("").javaClass) + private val resultStub = CommandResult("", "", ByteArray(0), 0) + private val resultFailureStub = CommandResult("", "", ByteArray(0), 1) + + @Before fun setUp() { + MockitoAnnotations.initMocks(this) + } + + @Test fun shouldExistBackup() { + val resultWithMeta = CommandResult(metaJson, "", ByteArray(0), 0) + + whenever(remote.isDirectory(anyString())).thenReturn(true) + whenever(remote.execIgnoringErrors(anyList(), anyMap(), anyLong())).thenReturn(resultWithMeta) + + val backup: ISimulatorBackup = SimulatorBackup(remote, udid, deviceSetPath) + + assertTrue("Backup should exist", backup.isExist()) + } + + @Test fun shouldNotExistBackup() { + whenever(remote.isDirectory(anyString())).thenReturn(true) + whenever(remote.execIgnoringErrors(anyList(), anyMap(), anyLong())).thenReturn(resultStub) + + val backup: ISimulatorBackup = SimulatorBackup(remote, udid, deviceSetPath) + + assertFalse("Backup should not exist", backup.isExist()) + } + + @Test fun shouldCreateBackup() { + whenever(remote.execIgnoringErrors(anyList(), anyMap(), anyLong())).thenReturn(resultStub) + whenever(remote.shell(anyString(), anyBoolean())).thenReturn(resultStub) + + SimulatorBackup(remote, udid, deviceSetPath).create() + + verify(remote, times(3)).execIgnoringErrors(captor.capture() ?: emptyList(), anyMap(), anyLong()) + + assertEquals("rm -rf $deviceSetPath/${udid}_BACKUP", captor.allValues[0].joinToString(" ")) + assertEquals("cp -R $deviceSetPath/$udid /home/user/backup/${udid}_BACKUP", captor.allValues[1].joinToString(" ")) + assertEquals("mkdir -p $deviceSetPath/${udid}_BACKUP/data/device_server", captor.allValues[2].joinToString(" ")) + + val cmdCaptor = ArgumentCaptor.forClass("".javaClass) + verify(remote, times(1)).shell(cmdCaptor.capture() ?: "", anyBoolean()) + val redirectPath = "$deviceSetPath/${udid}_BACKUP/data/device_server/meta.json" + val regex = """echo \\\{\\"version\\":1,\\"created\\":\\"[0-9T:-]+\\"\\} > $redirectPath""".toRegex() + assertThat(cmdCaptor.firstValue, matchesPattern(regex.pattern)) + } + + @Test(expected = SimulatorBackupError::class) + fun shouldDeleteThrow() { + whenever(remote.execIgnoringErrors(anyList(), anyMap(), anyLong())).thenReturn(resultFailureStub) + SimulatorBackup(remote, udid, deviceSetPath).delete() + } + + @Test(expected = SimulatorBackupError::class) + fun shouldCreateThrow() { + whenever(remote.execIgnoringErrors(anyList(), anyMap(), anyLong())).thenReturn(resultFailureStub) + SimulatorBackup(remote, udid, deviceSetPath).create() + } + + @Test(expected = SimulatorBackupError::class) + fun shouldRestoreThrow() { + whenever(remote.execIgnoringErrors(anyList(), anyMap(), anyLong())).thenReturn(resultFailureStub) + SimulatorBackup(remote, udid, deviceSetPath).restore() + } +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorderTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorderTest.kt new file mode 100644 index 00000000..1485e1f5 --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorderTest.kt @@ -0,0 +1,59 @@ +package com.badoo.automation.deviceserver.ios.simulator.video + +import com.badoo.automation.deviceserver.command.ChildProcess +import com.badoo.automation.deviceserver.host.IRemote +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDeviceDiagnosticInfo +import com.badoo.automation.deviceserver.mockThis +import com.nhaarman.mockito_kotlin.any +import com.nhaarman.mockito_kotlin.whenever +import org.junit.Before +import org.junit.Test +import java.time.Duration +import kotlin.test.assertFailsWith +import kotlin.test.assertFalse + +class SimulatorVideoRecorderTest { + private val childProcess: ChildProcess = mockThis() + private val remote = mockThis() + private val udid = "udid" + @Suppress("UNUSED_PARAMETER") + private fun childFactory( + remoteHost: String, username: String, cmd: List, isInteractiveShell: Boolean, + out_reader: (line: String) -> Unit, err_reader: (line: String) -> Unit + ): ChildProcess? { + return childProcess + } + + private val fbsimctl: FBSimctl = mockThis() + private val info: FBSimctlDeviceDiagnosticInfo = FBSimctlDeviceDiagnosticInfo("", "", "") + + @Before + fun setUp() { + whenever(remote.hostName).thenReturn("hostname") + whenever(remote.userName).thenReturn("username") + whenever(remote.fbsimctl).thenReturn(fbsimctl) + whenever(fbsimctl.diagnose(any())).thenReturn(info) + } + + @Test + fun stopFails() { + val recording = SimulatorVideoRecorder(udid, remote, ::childFactory, Duration.ofMillis(10)) + assertFailsWith { recording.stop() } + } + + @Test + fun doubleStartFails() { + val recording = SimulatorVideoRecorder(udid, remote, ::childFactory, Duration.ofMillis(10)) + recording.start() + assertFailsWith { recording.start() } + } + + @Test + fun stopSuccess() { + val recording = SimulatorVideoRecorder(udid, remote, ::childFactory, Duration.ofMillis(10)) + recording.start() + recording.stop() + assertFalse(recording.isProcessAlive, "Child process should not be alive") + } +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClientTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClientTest.kt new file mode 100644 index 00000000..34a24d57 --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClientTest.kt @@ -0,0 +1,20 @@ +package com.badoo.automation.deviceserver.util + +import com.badoo.automation.deviceserver.util.HttpCodes.* +import org.junit.Assert.assertEquals +import org.junit.Test +import java.net.URL + +class CustomHttpClientTest { + @Test fun unknownHost() { + val client = CustomHttpClient() + val result = client.get(URL("http://1922.168.1.6")) + assertEquals("Wrong code", OriginIsUnreachable.code, result.httpCode) + } + + @Test fun connectionRefused() { + val client = CustomHttpClient() + val result = client.get(URL("http://localhost:1")) + assertEquals("Wrong code", WebServerIsDown.code, result.httpCode) + } +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/util/SupportKtTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/util/SupportKtTest.kt new file mode 100644 index 00000000..2596a660 --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/util/SupportKtTest.kt @@ -0,0 +1,43 @@ +package com.badoo.automation.deviceserver.util + +import org.junit.Assert.assertEquals +import org.junit.Test +import java.net.URI +import java.time.Duration +import java.util.concurrent.TimeoutException +import kotlin.test.assertFailsWith + +class SupportKtTest { + @Test + fun testUriWithPath() { + val uri = URI("http://localhost:41798/FA2EC53F-E71A-4BAF-8686-840813C5348F/") + val newUri = uriWithPath(uri, "/list") + assertEquals("http://localhost:41798/FA2EC53F-E71A-4BAF-8686-840813C5348F/list", newUri.toString()) + } + + @Test + fun testExecuteWithTimeoutShouldThrowOnTimeout() { + assertFailsWith { + executeWithTimeout(Duration.ofMillis(500), "Slow") { + Thread.sleep(1000) + } + } + } + + @Test + fun testExecuteWithTimeoutShouldReturnReturnValueOfUnderlyingCallable() { + val actual = executeWithTimeout(Duration.ofSeconds(1), "Preparing simulator") { + return@executeWithTimeout 1 + } + assertEquals(1, actual) + } + + @Test + fun testExecuteWithTimeoutShouldRethrowOriginalException() { + assertFailsWith { + executeWithTimeout(Duration.ofSeconds(1), "Preparing simulator") { + throw RuntimeException("original error") + } + } + } +} \ No newline at end of file diff --git a/device-server/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/device-server/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 00000000..ca6ee9ce --- /dev/null +++ b/device-server/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-inline \ No newline at end of file From 1c4dd9f81230eb38c13e975a94ab3f4430dab07f Mon Sep 17 00:00:00 2001 From: Nick Abalov Date: Mon, 28 May 2018 08:56:02 +0100 Subject: [PATCH 003/131] Enable Travis CI --- .travis.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..1cb3a590 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,18 @@ +language: java +jdk: + - oraclejdk9 + +# http://docs.travis-ci.com/user/migrating-from-legacy +sudo: false + +before_install: + - cd device-server + +# configure caching (https://docs.travis-ci.com/user/languages/java/#Caching) +before_cache: + - rm -f $HOME/.gradle/caches/modules-2/modules-2.lock + - rm -fr $HOME/.gradle/caches/*/plugin-resolution/ +cache: + directories: + - $HOME/.gradle/caches/ + - $HOME/.gradle/wrapper/ \ No newline at end of file From 0fd0c03b26ab76bd31665b7bd0ce2238234a55b4 Mon Sep 17 00:00:00 2001 From: Nick Abalov Date: Mon, 28 May 2018 09:19:23 +0100 Subject: [PATCH 004/131] Add build badge to README --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cfae55bd..98621f66 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# iOS Remote Device Server +# iOS Remote Device Server [![Build Status](https://travis-ci.org/badoo/ios-device-server.svg?branch=master)](https://travis-ci.org/badoo/ios-device-server) A server for managing, booting, and controlling simulators and devices on remote host machines. From 441b5caaf55354f321aad74ae2c1caeba4f6d3bb Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Tue, 29 May 2018 13:52:33 +0100 Subject: [PATCH 005/131] Add JSON schema for config files --- device-server/config/config.schema.json | 109 ++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 device-server/config/config.schema.json diff --git a/device-server/config/config.schema.json b/device-server/config/config.schema.json new file mode 100644 index 00000000..3bfe9883 --- /dev/null +++ b/device-server/config/config.schema.json @@ -0,0 +1,109 @@ +{ + "$schema": "http://json-schema.org/draft-04/schema#", + "definitions": { + "node": { + "type": "object", + "properties": { + "user": { + "type": "string" + }, + "host": { + "type": "string" + } + } + }, + "devices_node": { + "type": "object", + "properties": { + "type": { + "enum": [ + "devices" + ] + }, + "whitelist_apps": { + "type": "array", + "items": { + "type": "string" + } + }, + "devices": { + "type": "array", + "items": { + "$ref": "#/definitions/devices" + } + } + }, + "required": [ + "type" + ] + }, + "simulators_node": { + "type": "object", + "properties": { + "type": { + "enum": [ + "simulators" + ] + }, + "concurrent_boots": { + "type": "integer" + }, + "simulator_limit": { + "type": "integer" + } + } + }, + "device": { + "type": "object", + "properties": { + "udid": { + "type": "string" + }, + "ip": { + "type": "string" + } + }, + "required": [ + "udid", + "ip" + ] + } + }, + "type": "object", + "properties": { + "timeouts": { + "description": "", + "type": "object", + "properties": { + "device": { + "type": [ + "integer", + "null" + ] + } + } + }, + "nodes": { + "type": "array", + "items": { + "allOf": [ + { + "$ref": "#/definitions/node" + }, + { + "anyOf": [ + { + "additionalProperties": false, + "$ref": "#/definitions/simulators_node" + }, + { + "additionalProperties": false, + "$ref": "#/definitions/devices_node" + } + ] + } + ] + } + } + } +} From b9495366d376b249ba738c72a5c7dea3b552dcd1 Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Tue, 5 Jun 2018 15:01:54 +0100 Subject: [PATCH 006/131] Return 503 code from status until initial registration is complete Device Server cannot create devices until initial registration of nodes is complete. Return 503 from /status endpoint until completed, return 200 after. --- .../kotlin/com/badoo/automation/deviceserver/DeviceServer.kt | 3 ++- .../automation/deviceserver/host/management/DeviceManager.kt | 4 ++++ .../automation/deviceserver/host/management/IDeviceManager.kt | 1 + 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt index 9238c577..c4257e46 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt @@ -132,7 +132,8 @@ fun Application.module() { call.respond(statusController.welcomeMessage(routes)) } get("status") { - call.respond(statusController.getServerStatus()) + val code = if (deviceManager.isReady()) HttpStatusCode.OK else HttpStatusCode.ServiceUnavailable + call.respond(code, statusController.getServerStatus()) } route("devices") { get { diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt index 2bdc721c..4204fada 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt @@ -131,4 +131,8 @@ class DeviceManager( val devices = nodeRegistry.activeDevices.getUserDeviceRefs(userId) nodeRegistry.activeDevices.releaseDevices(devices, reason) } + + override fun isReady(): Boolean { + return nodeRegistry.getInitialRegistrationComplete() + } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt index f736fa5b..f9e3d668 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt @@ -23,4 +23,5 @@ interface IDeviceManager { fun readyForRelease(): List fun getStatus(): Map fun releaseUserDevices(userId: String, reason: String) + fun isReady(): Boolean } \ No newline at end of file From 83d39b5fe6f347fc8fed87ca90b533889606ed9d Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Wed, 6 Jun 2018 11:16:51 +0100 Subject: [PATCH 007/131] Fix list method crashing on device disconnection --- .../deviceserver/ios/ActiveDevices.kt | 17 ++++++++-- .../deviceserver/ios/ActiveDevicesTest.kt | 32 +++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevicesTest.kt diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt index 24f39a4f..bf4bb24c 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt @@ -33,9 +33,20 @@ class ActiveDevices( } override fun deviceList(): List { - return devices.map { - it.value.node.getDeviceDTO(it.value.ref) - } + val disconnected = mutableListOf() + + val list = devices.map { + try { + it.value.node.getDeviceDTO(it.value.ref) + } catch (ex: DeviceNotFoundException) { + disconnected.add(it.key) + null + } + }.filterNotNull() + + disconnected.forEach { unregisterDeleteDevice(it) } + + return list } override fun registerDevice(ref: DeviceRef, node: ISimulatorsNode, releaseTimeout: Duration, userId: String?) { diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevicesTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevicesTest.kt new file mode 100644 index 00000000..d2daaf0d --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevicesTest.kt @@ -0,0 +1,32 @@ +package com.badoo.automation.deviceserver.ios + +import com.badoo.automation.deviceserver.deviceDTOStub +import com.badoo.automation.deviceserver.host.ISimulatorsNode +import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException +import com.badoo.automation.deviceserver.mockThis +import com.nhaarman.mockito_kotlin.whenever +import org.junit.Assert.assertEquals +import org.junit.Test +import java.time.Duration + +class ActiveDevicesTest { + + @Test + fun deviceListShouldIgnoreDisconnectedDevices() { + val activeDevices = ActiveDevices() + val node = mockThis() + + whenever(node.getDeviceDTO("ref1")).thenReturn(deviceDTOStub("ref1")) + whenever(node.getDeviceDTO("ref2")).thenThrow(DeviceNotFoundException("")) + whenever(node.getDeviceDTO("ref3")).thenReturn(deviceDTOStub("ref3")) + + activeDevices.registerDevice("ref1", node, Duration.ZERO, null) + activeDevices.registerDevice("ref2", node, Duration.ZERO, null) + activeDevices.registerDevice("ref3", node, Duration.ZERO, null) + + val list = activeDevices.deviceList() + + assertEquals(listOf("ref1", "ref3"), list.map { it.ref }.sorted()) + assertEquals(2, activeDevices.deviceRefs().size) + } +} \ No newline at end of file From 9adae7f11c9aec057bc065320d1e664b245349d7 Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Fri, 29 Jun 2018 14:01:07 +0100 Subject: [PATCH 008/131] Relax Java version requirements --- device-server/jar_launcher.sh | 4 ++-- device-server/run_device_server.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/device-server/jar_launcher.sh b/device-server/jar_launcher.sh index e565d927..568512cc 100755 --- a/device-server/jar_launcher.sh +++ b/device-server/jar_launcher.sh @@ -6,10 +6,10 @@ declare -r DEVICE_SERVER_CONFIG_PATH="${DEVICE_SERVER_CONFIG_PATH}" declare -r DEVICE_SERVER_JAR="${DEVICE_SERVER_JAR}" declare -r WDA_RUNNER='../ios/facebook/simulators/WebDriverAgentRunner-Runner.app' -declare -r WDA_DEVICE_RUNNER='../ios/facebook/devices/WebDriverAgentRunner-Runner.app' +declare -r WDA_DEVICE_RUNNER=${DEVICE_SERVER_WDA_DEVICE_RUNNER:-'../ios/facebook/devices/WebDriverAgentRunner-Runner.app'} declare -r LOG_CONFIG='logback-test.xml' -export JAVA_HOME=$(/usr/libexec/java_home -v 10) +export JAVA_HOME=$(/usr/libexec/java_home -v 10 -F || /usr/libexec/java_home -v 9 -F) pushd "$( dirname "${BASH_SOURCE[0]}" )" diff --git a/device-server/run_device_server.sh b/device-server/run_device_server.sh index e9893e7b..5d181601 100755 --- a/device-server/run_device_server.sh +++ b/device-server/run_device_server.sh @@ -3,7 +3,7 @@ set -xe export DEVICE_SERVER_JAR='build/libs/device-server-1.0-SNAPSHOT.jar' -export JAVA_HOME=$(/usr/libexec/java_home -v 10) +export JAVA_HOME=$(/usr/libexec/java_home -v 10 -F || /usr/libexec/java_home -v 9 -F) pushd "$( dirname "${BASH_SOURCE[0]}" )" From e9fb7554caf29f95ff8fa99d2687b59d4b701da0 Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Fri, 29 Jun 2018 14:14:50 +0100 Subject: [PATCH 009/131] Use default config when run from gradle task --- device-server/build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/device-server/build.gradle b/device-server/build.gradle index 297f21ca..b49816df 100644 --- a/device-server/build.gradle +++ b/device-server/build.gradle @@ -118,7 +118,7 @@ jar { run { systemProperty 'wda.bundle.path', '../ios/facebook/simulators/WebDriverAgentRunner-Runner.app' systemProperty 'wda.device.bundle.path', '../ios/facebook/devices/WebDriverAgentRunner-Runner.app' - systemProperty 'device.server.config.path', './config/.device_config' + systemProperty 'device.server.config.path', '' systemProperty 'logback.configurationFile', 'logback.xml' } From 8548bd4c168aaff1b35bce8946151301be84d8ba Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Fri, 29 Jun 2018 14:25:45 +0100 Subject: [PATCH 010/131] =?UTF-8?q?WDA=20host=20app=20=E2=80=94=20com.appl?= =?UTF-8?q?e.MobileAddressBook?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt index 4dae6c78..a0dfdc00 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt @@ -9,7 +9,7 @@ class SimulatorWebDriverAgent( wdaPath: String, udid: UDID, wdaEndpoint: URI, - hostApp: String = "com.apple.Preferences" + hostApp: String = "com.apple.MobileAddressBook" // seems like AddressBook has the least memory usage and no alerts ) : WebDriverAgent( remote = remote, wdaPath = wdaPath, From 9fdc929421d3b34cb48678947f472cb219980be0 Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Tue, 12 Jun 2018 15:45:40 +0100 Subject: [PATCH 011/131] Loosen Xcode version checking Signed-off-by: Nikolai Abalov --- .../deviceserver/host/DevicesNode.kt | 10 +++++----- .../host/management/ISimulatorHostChecker.kt | 7 ++++--- .../host/management/XcodeVersion.kt | 18 ++++++++++++++++++ .../deviceserver/host/SimulatorsNodeTest.kt | 16 ---------------- .../host/management/XcodeVersionTest.kt | 16 ++++++++++++++++ 5 files changed, 43 insertions(+), 24 deletions(-) create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersion.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersionTest.kt diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt index 7c94f2e4..1e7c5df1 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt @@ -3,6 +3,7 @@ package com.badoo.automation.deviceserver.host import com.badoo.automation.deviceserver.LogMarkers import com.badoo.automation.deviceserver.data.* import com.badoo.automation.deviceserver.host.management.PortAllocator +import com.badoo.automation.deviceserver.host.management.XcodeVersion import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException import com.badoo.automation.deviceserver.ios.device.* import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl @@ -254,12 +255,11 @@ class DevicesNode( } private fun checkPrerequisites() { - val expectedXcodeVersions = setOf("Xcode 9.2", "Xcode 9.3") + val xcodeOutput = remote.execIgnoringErrors(listOf("xcodebuild", "-version")) + val xcodeVersion = XcodeVersion.fromXcodeBuildOutput(xcodeOutput.stdOut) - val xcodeVersion = remote.execIgnoringErrors(listOf("xcodebuild", "-version")) - - if (!expectedXcodeVersions.any { xcodeVersion.stdOut.contains(it) }) { - throw RuntimeException("Expecting ${expectedXcodeVersions.joinToString(", ")}, but it is $xcodeVersion") + if (!(xcodeVersion.major == 9 && xcodeVersion.minor >= 2)) { + throw RuntimeException("Expecting Xcode 9.2 or higher up to 10, but it is $xcodeVersion") } val expectedFbsimctlVersion = "HEAD-292a1bd" diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt index 770623e8..412c92fb 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt @@ -48,10 +48,11 @@ class SimulatorHostChecker( } override fun checkPrerequisites() { - val xcodeVersion = remote.execIgnoringErrors(listOf("xcodebuild", "-version")) + val xcodeOutput = remote.execIgnoringErrors(listOf("xcodebuild", "-version")) + val xcodeVersion = XcodeVersion.fromXcodeBuildOutput(xcodeOutput.stdOut) - if (!xcodeVersion.stdOut.contains(EXPECTED_XCODE_VERSION)) { - throw RuntimeException("Expecting $EXPECTED_XCODE_VERSION, but received $xcodeVersion") + if (xcodeVersion.major != 9) { + throw RuntimeException("Expecting Xcode 9, but received $xcodeOutput") } // temp solution, prereq should be satisfied without having to switch anything diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersion.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersion.kt new file mode 100644 index 00000000..08c7aa0c --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersion.kt @@ -0,0 +1,18 @@ +package com.badoo.automation.deviceserver.host.management; + +class XcodeVersion(val major: Int, val minor: Int) { + companion object { + fun fromXcodeBuildOutput(output: String): XcodeVersion { + val regex = Regex("Xcode (\\d+)\\.(\\d+)") + val versionLine = output.lines().first() + val match = regex.matchEntire(versionLine) + + match?.destructured?.let { + val major = match.groups[1]!!.value.toInt() + val minor = match.groups[2]!!.value.toInt() + + return XcodeVersion(major, minor) + } ?: throw IllegalArgumentException("Could not parse Xcode version $versionLine") + } + } +} diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt index 35085090..5d11d1fd 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt @@ -393,19 +393,3 @@ class SimulatorsNodeTest { } } - -class SimulatorHostCheckerTest { - private val iRemote: IRemote = mockThis() - - private val hostChecker = SimulatorHostChecker(iRemote, Duration.ofMillis(1)) - - @Test(expected = RuntimeException::class) - fun checkPrerequisitesDislikesBadXcode() { - whenever(iRemote.execIgnoringErrors("xcodebuild -version".split(" "))) - .thenReturn(CommandResult( - "Xcode x.", "", ByteArray(0), - 0, true)) - - hostChecker.checkPrerequisites() - } -} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersionTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersionTest.kt new file mode 100644 index 00000000..bf3bc2f0 --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersionTest.kt @@ -0,0 +1,16 @@ +package com.badoo.automation.deviceserver.host.management + +import org.junit.Assert.* +import org.junit.Test + +class XcodeVersionTest { + @Test + fun shouldParseXcodeBuildOutput() { + val out = "Xcode 9.2\nBuild version 9C40b" + + val version = XcodeVersion.fromXcodeBuildOutput(out) + + assertEquals(9, version.major) + assertEquals(2, version.minor) + } +} From a5bf0152bfb4975433ff55af0f3dd4079d65804d Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Wed, 13 Jun 2018 16:23:25 +0100 Subject: [PATCH 012/131] Fix Xcode patch version parsing Signed-off-by: Nikolai Abalov --- .../deviceserver/host/management/XcodeVersion.kt | 2 +- .../deviceserver/host/management/XcodeVersionTest.kt | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersion.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersion.kt index 08c7aa0c..b427f005 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersion.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersion.kt @@ -3,7 +3,7 @@ package com.badoo.automation.deviceserver.host.management; class XcodeVersion(val major: Int, val minor: Int) { companion object { fun fromXcodeBuildOutput(output: String): XcodeVersion { - val regex = Regex("Xcode (\\d+)\\.(\\d+)") + val regex = Regex("Xcode (\\d+)\\.(\\d+)(\\.(\\d+))?") val versionLine = output.lines().first() val match = regex.matchEntire(versionLine) diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersionTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersionTest.kt index bf3bc2f0..1b08b976 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersionTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersionTest.kt @@ -13,4 +13,14 @@ class XcodeVersionTest { assertEquals(9, version.major) assertEquals(2, version.minor) } + + @Test + fun shouldParseXcodeBuildOutputWithPatchVersion() { + val out = "Xcode 9.2.1\nBuild version 9C40b" + + val version = XcodeVersion.fromXcodeBuildOutput(out) + + assertEquals(9, version.major) + assertEquals(2, version.minor) + } } From b838f417c7f7da3d8ba26eaa2915b0b6254c350f Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Fri, 15 Jun 2018 16:33:01 +0100 Subject: [PATCH 013/131] Skip registering empty list of nodes --- .../automation/deviceserver/host/management/NodeRegistrar.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt index fc68f653..90da7121 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt @@ -41,6 +41,11 @@ class NodeRegistrar( private fun autoRegister() { val unregistered = nodeWrappers - nodeRegistry.getAll() + + if (unregistered.isEmpty()) { + return + } + logger.debug("Going to auto register ${unregistered.map(NodeWrapper::toString)}") val executor = Executors.newFixedThreadPool(unregistered.size) val results: List> = unregistered.map { nodeWrapper -> From 142358b2714ca4abaf9c4df13eb8f9b1ed7647ee Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Fri, 15 Jun 2018 16:34:20 +0100 Subject: [PATCH 014/131] Remove IActiveDevices interface --- .../host/management/DeviceManager.kt | 3 +-- .../host/management/NodeRegistry.kt | 3 +-- .../deviceserver/ios/ActiveDevices.kt | 26 +++++++++---------- .../deviceserver/ios/IActiveDevices.kt | 21 --------------- .../deviceserver/host/NodeRegistryTest.kt | 4 +-- .../deviceserver/ios/DeviceManagerTest.kt | 2 +- 6 files changed, 18 insertions(+), 41 deletions(-) delete mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/IActiveDevices.kt diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt index 4204fada..0eaf1ace 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt @@ -6,7 +6,6 @@ import com.badoo.automation.deviceserver.host.HostFactory import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException import com.badoo.automation.deviceserver.host.management.util.AutoreleaseLooper import com.badoo.automation.deviceserver.ios.ActiveDevices -import com.badoo.automation.deviceserver.ios.IActiveDevices import org.slf4j.LoggerFactory import java.net.URL import java.time.Duration @@ -16,7 +15,7 @@ private val INFINITE_DEVICE_TIMEOUT: Duration = Duration.ofSeconds(Integer.MAX_V class DeviceManager( config: DeviceServerConfig, nodeFactory: IHostFactory = HostFactory(), - activeDevices: IActiveDevices = ActiveDevices(), + activeDevices: ActiveDevices = ActiveDevices(), private val autoreleaseLooper: IAutoreleaseLooper = AutoreleaseLooper() ) : IDeviceManager { private val logger = LoggerFactory.getLogger(javaClass.simpleName) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt index 9077a6bb..f03dfa85 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt @@ -6,7 +6,6 @@ import com.badoo.automation.deviceserver.data.DeviceDTO import com.badoo.automation.deviceserver.host.ISimulatorsNode import com.badoo.automation.deviceserver.host.management.errors.NoAliveNodesException import com.badoo.automation.deviceserver.ios.ActiveDevices -import com.badoo.automation.deviceserver.ios.IActiveDevices import com.badoo.automation.deviceserver.ios.simulator.simulatorsThreadPool import kotlinx.coroutines.experimental.Job import kotlinx.coroutines.experimental.launch @@ -17,7 +16,7 @@ import org.slf4j.Marker import java.time.Duration import java.util.concurrent.ConcurrentHashMap -class NodeRegistry(val activeDevices: IActiveDevices = ActiveDevices()) { +class NodeRegistry(val activeDevices: ActiveDevices = ActiveDevices()) { private var initialRegistrationComplete: Boolean = false private val nodeWrappers = ConcurrentHashMap.newKeySet() private val logger = LoggerFactory.getLogger(javaClass.simpleName) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt index bf4bb24c..0cad0390 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt @@ -19,7 +19,7 @@ data class SessionEntry( class ActiveDevices( private val sessionId: String = "defaultSessionId", private val currentTimeSeconds: ()->Long = ::currentTimeSecondsProvider -) : IActiveDevices { +) { private val devices: MutableMap = ConcurrentHashMap() private val logger = LoggerFactory.getLogger(javaClass.simpleName) @@ -28,11 +28,11 @@ class ActiveDevices( fun currentTimeSecondsProvider(): Long = System.currentTimeMillis() / 1000 } - override fun deviceRefs(): Set { + fun deviceRefs(): Set { return devices.keys } - override fun deviceList(): List { + fun deviceList(): List { val disconnected = mutableListOf() val list = devices.map { @@ -49,11 +49,11 @@ class ActiveDevices( return list } - override fun registerDevice(ref: DeviceRef, node: ISimulatorsNode, releaseTimeout: Duration, userId: String?) { + fun registerDevice(ref: DeviceRef, node: ISimulatorsNode, releaseTimeout: Duration, userId: String?) { devices[ref] = SessionEntry(ref, node, currentTimeSeconds(), releaseTimeout, userId) } - override fun unregisterNodeDevices(node: ISimulatorsNode) { + fun unregisterNodeDevices(node: ISimulatorsNode) { devices.entries .filter { it.value.node == node } .forEach { unregisterDeleteDevice(it.key) } @@ -73,7 +73,7 @@ class ActiveDevices( } } - override fun getNodeFor(ref: DeviceRef): ISimulatorsNode { + fun getNodeFor(ref: DeviceRef): ISimulatorsNode { val node = tryGetNodeFor(ref) if (node == null) { throw DeviceNotFoundException("Device [$ref] not found in [$sessionId] activeDevices") @@ -82,24 +82,24 @@ class ActiveDevices( } } - override fun getStatus(): String { + fun getStatus(): String { return devices .map { "\n" + it.key to it.value } .toString() } - override fun unregisterDeleteDevice(ref: DeviceRef) { + fun unregisterDeleteDevice(ref: DeviceRef) { devices.remove(ref) } - override fun readyForRelease(): List { + fun readyForRelease(): List { val secondsNow = currentTimeSeconds() return devices.filter { with(it.value) { releaseTimeout.seconds + updatedAtSeconds <= secondsNow } } .map { it.key } .also { logger.info("Ready to release $it"); } } - override fun nextReleaseAtSeconds(): Long { + fun nextReleaseAtSeconds(): Long { val sessionEntry = devices.minBy { it.value.updatedAtSeconds + it.value.releaseTimeout.seconds } @@ -115,13 +115,13 @@ class ActiveDevices( return nextReleaseAtSeconds } - override fun releaseDevice(ref: DeviceRef, reason: String) { + fun releaseDevice(ref: DeviceRef, reason: String) { val session = sessionByRef(ref) session.node.deleteRelease(session.ref, reason) unregisterDeleteDevice(session.ref) } - override fun releaseDevices(entries: List, reason: String) { + fun releaseDevices(entries: List, reason: String) { entries.parallelStream().forEach { try { releaseDevice(it, reason) @@ -131,7 +131,7 @@ class ActiveDevices( } } - override fun getUserDeviceRefs(userId: String): List { + fun getUserDeviceRefs(userId: String): List { return devices.filter { it.value.userId == userId }.map { it.key } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/IActiveDevices.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/IActiveDevices.kt deleted file mode 100644 index f8a51616..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/IActiveDevices.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.badoo.automation.deviceserver.ios - -import com.badoo.automation.deviceserver.data.DeviceDTO -import com.badoo.automation.deviceserver.data.DeviceRef -import com.badoo.automation.deviceserver.host.ISimulatorsNode -import java.time.Duration - -interface IActiveDevices { - fun deviceRefs(): Set - fun registerDevice(ref: DeviceRef, node: ISimulatorsNode, releaseTimeout: Duration, userId: String?) - fun getNodeFor(ref: DeviceRef): ISimulatorsNode - fun unregisterDeleteDevice(ref: DeviceRef) - fun readyForRelease(): List - fun nextReleaseAtSeconds(): Long - fun unregisterNodeDevices(node: ISimulatorsNode) - fun getStatus(): String - fun deviceList(): List - fun releaseDevice(ref: DeviceRef, reason: String) - fun getUserDeviceRefs(userId: String) : List - fun releaseDevices(entries: List, reason: String) -} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeRegistryTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeRegistryTest.kt index b72916d9..f5e59b5c 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeRegistryTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeRegistryTest.kt @@ -4,7 +4,7 @@ import com.badoo.automation.deviceserver.data.DesiredCapabilities import com.badoo.automation.deviceserver.deviceDTOStub import com.badoo.automation.deviceserver.host.management.NodeRegistry import com.badoo.automation.deviceserver.host.management.NodeWrapper -import com.badoo.automation.deviceserver.ios.IActiveDevices +import com.badoo.automation.deviceserver.ios.ActiveDevices import com.badoo.automation.deviceserver.mockThis import com.nhaarman.mockito_kotlin.any import com.nhaarman.mockito_kotlin.atLeast @@ -17,7 +17,7 @@ import org.junit.Test import java.time.Duration class NodeRegistryTest { - private val activeDevices: IActiveDevices = mockThis() + private val activeDevices: ActiveDevices = mockThis() private val nodeRegistry: NodeRegistry = NodeRegistry(activeDevices) private val headless = true private val desiredCapabilities = DesiredCapabilities("udid", "model", "os", headless) diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/DeviceManagerTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/DeviceManagerTest.kt index 59b06474..df6a03ae 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/DeviceManagerTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/DeviceManagerTest.kt @@ -51,7 +51,7 @@ class DeviceManagerTest { return m } - private val activeDevices: IActiveDevices = mockThis() + private val activeDevices: ActiveDevices = mockThis() private val autoreleaseLooper: IAutoreleaseLooper = mockThis() From 1d6ab0ab72a568ac45073bcdf0b1fe98fbbe5031 Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Fri, 15 Jun 2018 17:53:51 +0100 Subject: [PATCH 015/131] Add exception to logs if node fails to start --- .../automation/deviceserver/host/management/NodeRegistrar.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt index 90da7121..7a7fb980 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt @@ -62,7 +62,7 @@ class NodeRegistrar( try { result.get() } catch (e: Throwable) { - logger.debug("Error while starting node ") + logger.debug("Error while starting node", e) } } nodeRegistry.setInitialRegistrationComplete() From 594c6f60dff3e092226a0650368ec8b95d3205c0 Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Fri, 15 Jun 2018 17:54:24 +0100 Subject: [PATCH 016/131] Add ssh profiling marker to timing logs --- .../com/badoo/automation/deviceserver/LogMarkers.kt | 2 +- .../deviceserver/command/RemoteShellCommand.kt | 13 ++++++++----- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/LogMarkers.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/LogMarkers.kt index 71c97074..4bdeab2b 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/LogMarkers.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/LogMarkers.kt @@ -11,6 +11,6 @@ class LogMarkers { const val DEVICE_REF = "deviceRef" const val UDID = "udid" const val HOSTNAME = "hostname" - const val SSH_PROFILING = "sshProfiling" + const val SSH_PROFILING_MS = "sshTimeMs" } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt index 9bd2f7ff..742fc5be 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt @@ -18,10 +18,6 @@ class RemoteShellCommand( ) : ShellCommand(builderFactory, commonEnvironment) { private val userAtHost: String = if (userName.isBlank()) { remoteHost } else { "$userName@$remoteHost" } override val logMarker: Marker get() = MapEntriesAppendingMarker(mapOf(LogMarkers.HOSTNAME to remoteHost)) - private val sshProfilingLogMarker = MapEntriesAppendingMarker(mapOf( - LogMarkers.HOSTNAME to remoteHost, - LogMarkers.SSH_PROFILING to "true" - )) private val sshEnv: Map private val sshCommandPrefix: List init { @@ -65,7 +61,14 @@ class RemoteShellCommand( val start = System.currentTimeMillis() val result = super.exec(cmd, getEnvironmentForSSH(), timeOut, returnFailure, logMarker, processListener) val elapsed = System.currentTimeMillis() - start - logger.debug(sshProfilingLogMarker, "Execution of SSH command took $elapsed ms. Command: $cmd") + val marker = MapEntriesAppendingMarker( + mapOf( + LogMarkers.HOSTNAME to remoteHost, + LogMarkers.SSH_PROFILING_MS to elapsed + ) + ) + logger.debug( + marker, "Execution of SSH command took $elapsed ms. Command: $cmd") if (result.exitCode == SSH_ERROR) { // FIXME: Check stdout and stderr, if they are empty – ssh timeout, otherwise, it is likely to be command error From 400230b2e27289b22779c307efc2d00382814fde Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Mon, 18 Jun 2018 17:45:34 +0100 Subject: [PATCH 017/131] Uninstall proper bundle of WebDriverAgent --- .../automation/deviceserver/ios/device/DeviceWebDriverAgent.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceWebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceWebDriverAgent.kt index f1f349b7..640afbfc 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceWebDriverAgent.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceWebDriverAgent.kt @@ -20,7 +20,7 @@ class DeviceWebDriverAgent( wdaEndpoint = wdaEndpoint ) { override fun terminateHostApp() { - remote.fbsimctl.uninstallApp(udid, "com.apple.test.WebDriverAgentRunner-Runner") + remote.fbsimctl.uninstallApp(udid, hostApp) Thread.sleep(1000) } } \ No newline at end of file From d0346c3f83ae268d0f9221cdfae2ffbb5a2a7de0 Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Mon, 18 Jun 2018 16:42:59 +0100 Subject: [PATCH 018/131] Make user apps uninstall optional --- device-server/config/config.schema.json | 3 +++ .../automation/deviceserver/NodeConfig.kt | 3 +++ .../deviceserver/host/DevicesNode.kt | 5 +++-- .../deviceserver/host/HostFactory.kt | 3 ++- .../deviceserver/ios/device/Device.kt | 6 ++++-- .../automation/deviceserver/NodeConfigTest.kt | 20 +++++++++++++++++++ 6 files changed, 35 insertions(+), 5 deletions(-) diff --git a/device-server/config/config.schema.json b/device-server/config/config.schema.json index 3bfe9883..2fdc9bf2 100644 --- a/device-server/config/config.schema.json +++ b/device-server/config/config.schema.json @@ -20,6 +20,9 @@ "devices" ] }, + "uninstall_apps": { + "type": "boolean" + }, "whitelist_apps": { "type": "array", "items": { diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/NodeConfig.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/NodeConfig.kt index dba0e708..4d169086 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/NodeConfig.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/NodeConfig.kt @@ -24,6 +24,9 @@ data class NodeConfig( @JsonProperty("whitelist_apps") val whitelistApps: Set = emptySet(), + @JsonProperty("uninstall_apps") + val uninstallApps: Boolean = false, + @JsonProperty("devices") val knownDevices: List = emptyList() ) { diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt index 1e7c5df1..b260072d 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt @@ -22,7 +22,8 @@ class DevicesNode( portAllocator: PortAllocator = PortAllocator(), wdaPath: File, knownDevices: List, - private val whitelistedApps: Set + private val whitelistedApps: Set, + private val uninstallApps: Boolean ) : ISimulatorsNode { private val logger = LoggerFactory.getLogger(javaClass.simpleName) private val logMarker = MapEntriesAppendingMarker( @@ -144,7 +145,7 @@ class DevicesNode( activeRefs[ref!!] = slot!!.udid } - slot!!.device.renewAsync(whitelistedApps = whitelistedApps) + slot!!.device.renewAsync(whitelistedApps = whitelistedApps, uninstallApps = uninstallApps) return deviceToDto(ref!!, device = slot!!.device) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt index c04810cd..1aa7ca96 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt @@ -43,7 +43,8 @@ class HostFactory( remote, whitelistedApps = config.whitelistApps, wdaPath = getWdaDevicesPath(remote.isLocalhost()), - knownDevices = config.knownDevices + knownDevices = config.knownDevices, + uninstallApps = config.uninstallApps ) } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt index b7812e51..0c4623ba 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt @@ -189,7 +189,7 @@ class Device( } } - fun renewAsync(whitelistedApps: Set) { + fun renewAsync(whitelistedApps: Set, uninstallApps: Boolean) { val currentStatus = status() var prepareRequired = false @@ -209,7 +209,9 @@ class Device( } } - uninstallUserApps(whitelistedApps = whitelistedApps) + if (uninstallApps) { + uninstallUserApps(whitelistedApps = whitelistedApps) + } if (prepareRequired) { preparePromise?.cancel(true) diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/NodeConfigTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/NodeConfigTest.kt index 36b5cb0a..a9f96560 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/NodeConfigTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/NodeConfigTest.kt @@ -13,6 +13,7 @@ class NodeConfigTest { "simulator_limit": 1, "concurrent_boots": 1, "whitelist_apps": [ "bundle.id" ], + "uninstall_apps": true, "devices": [ { "udid": "c865bdbe652d17cbe2c79566fb046b73fed66a38", @@ -34,6 +35,7 @@ class NodeConfigTest { simulatorLimit = 1, concurrentBoots = 1, whitelistApps = setOf("bundle.id"), + uninstallApps = true, knownDevices = listOf( KnownDevice( "c865bdbe652d17cbe2c79566fb046b73fed66a38", @@ -44,4 +46,22 @@ class NodeConfigTest { Assert.assertEquals(expected, config) } + + @Test + fun shouldHaveDefaults() { + val config = JsonMapper().fromJson("{}") + + val expected = NodeConfig( + type = NodeConfig.NodeType.Simulators, + user = "", + host = "localhost", + simulatorLimit = 6, + concurrentBoots = 3, + whitelistApps = emptySet(), + uninstallApps = false, + knownDevices = emptyList() + ) + + Assert.assertEquals(expected, config) + } } \ No newline at end of file From 902b51837fc210693963ff0a88d6822926adedba Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Wed, 20 Jun 2018 10:28:40 +0100 Subject: [PATCH 019/131] Use TCP over USB instead of Wi-Fi - Use iproxy to proxy tcp to localohst for WebDriver and Calabash - Remove ip and wifi_address from configuration - Make whitelisting devices optional - Update config file in accordance --- device-server/config/config.schema.json | 6 +- device-server/config/physical-sample.json | 4 +- .../deviceserver/host/DevicesNode.kt | 5 ++ .../deviceserver/ios/device/Device.kt | 44 ++++++++++--- .../ios/device/DeviceException.kt | 3 + .../deviceserver/ios/device/DeviceSlots.kt | 9 ++- .../ios/device/DeviceWebDriverAgent.kt | 4 +- .../deviceserver/ios/device/KnownDevice.kt | 6 +- .../deviceserver/ios/device/UsbProxy.kt | 61 +++++++++++++++++++ .../ios/device/UsbProxyFactory.kt | 12 ++++ .../deviceserver/ios/proc/WebDriverAgent.kt | 3 +- .../automation/deviceserver/NodeConfigTest.kt | 7 +-- 12 files changed, 134 insertions(+), 30 deletions(-) create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceException.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxy.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxyFactory.kt diff --git a/device-server/config/config.schema.json b/device-server/config/config.schema.json index 2fdc9bf2..8bd4031b 100644 --- a/device-server/config/config.schema.json +++ b/device-server/config/config.schema.json @@ -61,14 +61,10 @@ "properties": { "udid": { "type": "string" - }, - "ip": { - "type": "string" } }, "required": [ - "udid", - "ip" + "udid" ] } }, diff --git a/device-server/config/physical-sample.json b/device-server/config/physical-sample.json index 31b59c7a..fcbe935e 100644 --- a/device-server/config/physical-sample.json +++ b/device-server/config/physical-sample.json @@ -12,9 +12,7 @@ ], "devices": [ { - "udid": "a000aaaa000a00aaa0a00000aa000a00aaa00a00", - "ip": "192.168.18.219", - "wifi_address": "00:00:00:00:00:00" + "udid": "a000aaaa000a00aaa0a00000aa000a00aaa00a00" } ] } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt index b260072d..e85c1437 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt @@ -283,6 +283,11 @@ class DevicesNode( if (actualFbsimctlVersion != expectedFbsimctlVersion) { throw RuntimeException("Expecting fbsimctl $expectedFbsimctlVersion, but it was $actualFbsimctlVersion ${match.groupValues}") } + + val iproxy = remote.execIgnoringErrors((listOf("iproxy"))) + if (iproxy.exitCode != 0) { + throw RuntimeException("Expecting iproxy to be installed") + } } private fun copyWdaBundleToHost() { diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt index 0c4623ba..d5a73a32 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt @@ -19,17 +19,31 @@ import java.time.Duration import java.util.concurrent.Executors import java.util.concurrent.Future + class Device( private val remote: IRemote, val deviceInfo: DeviceInfo, val allocatedPorts: DeviceAllocatedPorts, wdaPath: String, - private val ipAddress: String + usbProxy: UsbProxyFactory = UsbProxyFactory(remote) ) { val udid: String = deviceInfo.udid + + private val calabashProxy = usbProxy.create( + udid = deviceInfo.udid, + localPort = allocatedPorts.calabashPort, + devicePort = CALABASH_PORT + ) + + private val wdaProxy = usbProxy.create( + udid = deviceInfo.udid, + localPort = allocatedPorts.wdaPort, + devicePort = WDA_PORT + ) + val fbsimctlEndpoint = URI("http://${remote.hostName}:${allocatedPorts.fbsimctlPort}/$udid/") - val wdaEndpoint = URI("http://$ipAddress:$WDA_PORT") - val calabashPort = CALABASH_PORT + val wdaEndpoint = URI("http://${remote.hostName}:${wdaProxy.localPort}") + val calabashPort = calabashProxy.localPort @Volatile var lastException: Exception? = null @@ -44,7 +58,7 @@ class Device( } private val fbsimctlProc: DeviceFbsimctlProc = DeviceFbsimctlProc(remote, deviceInfo.udid, fbsimctlEndpoint, false) - private val wdaProc = DeviceWebDriverAgent(remote, wdaPath, deviceInfo.udid, wdaEndpoint) + private val wdaProc = DeviceWebDriverAgent(remote, wdaPath, deviceInfo.udid, wdaEndpoint, wdaProxy.devicePort) private val status = SimulatorStatus() @@ -130,11 +144,10 @@ class Device( } fun endpointFor(port: Int): URL { - val ports = setOf(WDA_PORT, CALABASH_PORT) - + val ports = allocatedPorts.toSet() require(ports.contains(port)) { "Port $port is not in user ports range $ports" } - return URL("http://$ipAddress:$port/") + return URL("http://${remote.hostName}:$port/") } fun dispose() { @@ -148,6 +161,8 @@ class Device( private fun disposeResources() { ignoringDisposeErrors { fbsimctlProc.kill() } ignoringDisposeErrors { wdaProc.kill() } + ignoringDisposeErrors { calabashProxy.stop() } + ignoringDisposeErrors { wdaProxy.stop() } } private fun ignoringDisposeErrors(action: () -> Unit?) { @@ -264,7 +279,22 @@ class Device( fbsimctlProc.kill() wdaProc.kill() + wdaProxy.stop() + calabashProxy.stop() + executeWithTimeout(timeout, name = "Preparing devices") { + wdaProxy.start() + + if (!wdaProxy.isHealthy()) { + throw DeviceException("Failed to start $wdaProxy") + } + + calabashProxy.start() + + if (!calabashProxy.isHealthy()) { + throw DeviceException("Failed to start $calabashProxy") + } + startFbsimctl() startWdaWithRetry() diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceException.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceException.kt new file mode 100644 index 00000000..2af9a9d2 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceException.kt @@ -0,0 +1,3 @@ +package com.badoo.automation.deviceserver.ios.device + +class DeviceException(message: String) : RuntimeException(message) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlots.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlots.kt index 65d30017..cfdb3c6c 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlots.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlots.kt @@ -38,7 +38,7 @@ class DeviceSlots( fun registerDevices() { val connectedDevices = deviceInfoProvider.list() - val knownConnectedDevices = connectedDevices.filter { knownDevices.containsKey(it.udid) } + val knownConnectedDevices = connectedDevices.filter { isWhitelisted(it.udid) } val diff = diff(knownConnectedDevices) @@ -107,6 +107,10 @@ class DeviceSlots( activeSlots.clear() } + private fun isWhitelisted(udid: UDID): Boolean { + return knownDevices.isEmpty() || knownDevices.containsKey((udid)) + } + private fun availableSlots(desiredCapabilities: DesiredCapabilities): List { return activeSlots.filter { !it.isReserved() && dcMatcher.isMatch(it.device.deviceInfo, desiredCapabilities) @@ -138,8 +142,7 @@ class DeviceSlots( remote =remote, deviceInfo = deviceInfo, allocatedPorts = allocatedPorts, - wdaPath = wdaPath, - ipAddress = knownDevices.getValue(udid).ipAddress + wdaPath = wdaPath ) device.prepareAsync() diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceWebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceWebDriverAgent.kt index 640afbfc..f6f1de0d 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceWebDriverAgent.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceWebDriverAgent.kt @@ -11,13 +11,15 @@ class DeviceWebDriverAgent( wdaPath: String, udid: UDID, wdaEndpoint: URI, + port: Int, hostApp: String = Paths.get(wdaPath).parent.parent.toString() ) : WebDriverAgent( remote = remote, wdaPath = wdaPath, hostApp = hostApp, udid = udid, - wdaEndpoint = wdaEndpoint + wdaEndpoint = wdaEndpoint, + port = port ) { override fun terminateHostApp() { remote.fbsimctl.uninstallApp(udid, hostApp) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/KnownDevice.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/KnownDevice.kt index 08a01029..3b9c7da7 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/KnownDevice.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/KnownDevice.kt @@ -4,11 +4,7 @@ import com.badoo.automation.deviceserver.data.UDID import com.fasterxml.jackson.annotation.JsonIgnoreProperties import com.fasterxml.jackson.annotation.JsonProperty -@JsonIgnoreProperties(value = ["wifi_address"]) data class KnownDevice( @JsonProperty("udid") - val udid: UDID, - - @JsonProperty("ip") - val ipAddress: String + val udid: UDID ) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxy.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxy.kt new file mode 100644 index 00000000..c549a629 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxy.kt @@ -0,0 +1,61 @@ +package com.badoo.automation.deviceserver.ios.device + +import com.badoo.automation.deviceserver.LogMarkers +import com.badoo.automation.deviceserver.command.ChildProcess +import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.host.IRemote +import net.logstash.logback.marker.MapEntriesAppendingMarker +import org.slf4j.Logger +import org.slf4j.LoggerFactory + +class UsbProxy( + private val udid: UDID, + private val remote: IRemote, + val localPort: Int, + val devicePort: Int, + private val childFactory: ( + remoteHost: String, + userName: String, + cmd: List, + isInteractiveShell: Boolean, + out_reader: (line: String) -> Unit, + err_reader: (line: String) -> Unit + ) -> ChildProcess = ChildProcess.Companion::fromCommand +) { + private val logger: Logger = LoggerFactory.getLogger(javaClass.simpleName) + private val logMarker = MapEntriesAppendingMarker( + mapOf( + LogMarkers.HOSTNAME to remote.hostName, + LogMarkers.UDID to udid + ) + ) + + override fun toString(): String = "" + + private var childProcess: ChildProcess? = null + + fun start() { + childProcess = childFactory( + remote.hostName, + remote.userName, + listOf("iproxy", localPort.toString(), devicePort.toString(), udid), + false, + { message -> logger.info(logMarker, "${this}: iproxy : ${message.trim()}") }, + { message -> logger.warn(logMarker, "${this}: iproxy : ${message.trim()}") } + ) + } + + fun isHealthy(): Boolean { + return childProcess?.isAlive() ?: false + } + + fun stop() { + if (childProcess == null) { + return + } + + logger.debug(logMarker, "$this — Killing child process $childProcess") + childProcess!!.kill() + childProcess = null + } +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxyFactory.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxyFactory.kt new file mode 100644 index 00000000..45cc012f --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxyFactory.kt @@ -0,0 +1,12 @@ +package com.badoo.automation.deviceserver.ios.device + +import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.host.IRemote + +class UsbProxyFactory( + private val remote: IRemote +) { + fun create(udid: UDID, localPort: Int, devicePort: Int): UsbProxy { + return UsbProxy(udid, remote, localPort, devicePort) + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt index f5b25fbe..12b89526 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt @@ -14,6 +14,7 @@ open class WebDriverAgent( protected val hostApp: String, protected val udid: UDID, private val wdaEndpoint: URI, + port: Int = wdaEndpoint.port, private val childFactory: ( remoteHost: String, userName: String, @@ -31,7 +32,7 @@ open class WebDriverAgent( wdaPath, hostApp, "--port", - wdaEndpoint.port.toString(), + port.toString(), "--", "listen" ) diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/NodeConfigTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/NodeConfigTest.kt index a9f96560..1e3f1051 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/NodeConfigTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/NodeConfigTest.kt @@ -16,9 +16,7 @@ class NodeConfigTest { "uninstall_apps": true, "devices": [ { - "udid": "c865bdbe652d17cbe2c79566fb046b73fed66a38", - "ip": "127.0.0.1", - "wifi_address": "00:00:00:00:00:00" + "udid": "c865bdbe652d17cbe2c79566fb046b73fed66a38" } ] } @@ -38,8 +36,7 @@ class NodeConfigTest { uninstallApps = true, knownDevices = listOf( KnownDevice( - "c865bdbe652d17cbe2c79566fb046b73fed66a38", - ipAddress = "127.0.0.1" + "c865bdbe652d17cbe2c79566fb046b73fed66a38" ) ) ) From e4304e247e8cd270ca13eb67997ce718ebcca1cc Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Wed, 20 Jun 2018 16:48:48 +0100 Subject: [PATCH 020/131] Add public host and expose device ports --- README.md | 8 ++++ .../automation/deviceserver/NodeConfig.kt | 3 ++ .../deviceserver/host/DevicesNode.kt | 9 +++- .../deviceserver/host/HostFactory.kt | 5 ++- .../automation/deviceserver/host/IRemote.kt | 1 + .../automation/deviceserver/host/Remote.kt | 1 + .../deviceserver/host/SimulatorsNode.kt | 2 +- .../deviceserver/ios/device/Device.kt | 6 +-- .../deviceserver/ios/device/UsbProxy.kt | 41 ++++++++++++++----- .../deviceserver/ios/simulator/Simulator.kt | 6 +-- .../deviceserver/util/CustomHttpClient.kt | 9 ++-- .../deviceserver/host/RemoteTest.kt | 2 +- .../deviceserver/host/SimulatorsNodeTest.kt | 1 + .../deviceserver/ios/HostFactoryTest.kt | 2 +- 14 files changed, 67 insertions(+), 29 deletions(-) diff --git a/README.md b/README.md index 98621f66..2a18d24c 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,14 @@ A server for managing, booting, and controlling simulators and devices on remote export JAVA_HOME=$(/usr/libexec/java_home -v 10 -F) ``` +### Phyiscal Devices + +To use phyiscal devices extra dependencies are required: +``` +brew install libimobiledevice --HEAD +brew install socat +``` + ## Usage Build and run Device Server diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/NodeConfig.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/NodeConfig.kt index 4d169086..5f9c7df0 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/NodeConfig.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/NodeConfig.kt @@ -12,6 +12,9 @@ data class NodeConfig( @JsonProperty("host") val host: String = "localhost", + @JsonProperty("public_host") + val publicHost: String = host, + @JsonProperty("simulator_limit") val simulatorLimit: Int = 6, diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt index e85c1437..97ad3a12 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt @@ -246,7 +246,7 @@ class DevicesNode( private fun newRef(udid: UDID): DeviceRef { val unsafe = Regex("[^\\-_a-zA-Z\\d]") // TODO: Replace with UUID 4 - return "$udid-${remote.hostName}".replace(unsafe, "-") + return "$udid-${remote.publicHostName}".replace(unsafe, "-") } private fun slotByExternalRef(deviceRef: DeviceRef): DeviceSlot { @@ -284,10 +284,15 @@ class DevicesNode( throw RuntimeException("Expecting fbsimctl $expectedFbsimctlVersion, but it was $actualFbsimctlVersion ${match.groupValues}") } - val iproxy = remote.execIgnoringErrors((listOf("iproxy"))) + val iproxy = remote.execIgnoringErrors((listOf(UsbProxy.IPROXY_BIN))) if (iproxy.exitCode != 0) { throw RuntimeException("Expecting iproxy to be installed") } + + val socat = remote.execIgnoringErrors((listOf(UsbProxy.SOCAT_BIN, "-V"))) + if (socat.exitCode != 0) { + throw RuntimeException("Expecting socat to be installed") + } } private fun copyWdaBundleToHost() { diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt index 1aa7ca96..85399ad6 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt @@ -7,7 +7,7 @@ import org.slf4j.LoggerFactory import java.io.File class HostFactory( - private val remoteProvider: (hostName: String, userName: String) -> IRemote = { hostName, userName -> Remote(hostName, userName) }, + private val remoteProvider: (hostName: String, userName: String, publicHost: String) -> IRemote = { hostName, userName, publicHostName -> Remote(hostName, userName, publicHostName) }, private val simulatorHostProvider: ISimulatorHostProvider = DefaultSimulatorHostProvider ) : IHostFactory { companion object { @@ -25,7 +25,8 @@ class HostFactory( val hostName = config.host val userName = config.user - val remote: IRemote = remoteProvider(hostName, userName) + val publicHostName = config.publicHost + val remote: IRemote = remoteProvider(hostName, userName, publicHostName) if (userName.isBlank() && !remote.isLocalhost()) { throw RuntimeException("Config for non-localhost nodes must have non-empty 'user'. Current config: $config") diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt index 8ea19171..a71ee594 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt @@ -19,6 +19,7 @@ interface IRemote { val hostName: String val userName: String + val publicHostName: String fun isReachable(): Boolean fun isLocalhost(): Boolean = isLocalhost(hostName, userName) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt index 818dbec1..c8284b65 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt @@ -14,6 +14,7 @@ import java.time.Duration class Remote( override val hostName: String, override val userName: String, + override val publicHostName: String, private val localExecutor: IShellCommand = ShellCommand(), private val remoteExecutor: IShellCommand = getRemoteCommandExecutor(hostName, userName), override val fbsimctl: FBSimctl = FBSimctl(remoteExecutor, FBSimctlResponseParser()) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt index cf904261..520e11a3 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt @@ -117,7 +117,7 @@ class SimulatorsNode( } } - private fun newRef(udid: String): DeviceRef = "$udid-${remote.hostName}".replace(Regex("[^-\\w]"), "-") + private fun newRef(udid: String): DeviceRef = "$udid-${remote.publicHostName}".replace(Regex("[^-\\w]"), "-") override fun approveAccess(deviceRef: DeviceRef, bundleId: String) { getDeviceFor(deviceRef).approveAccess(bundleId) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt index d5a73a32..fec034e5 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt @@ -41,8 +41,8 @@ class Device( devicePort = WDA_PORT ) - val fbsimctlEndpoint = URI("http://${remote.hostName}:${allocatedPorts.fbsimctlPort}/$udid/") - val wdaEndpoint = URI("http://${remote.hostName}:${wdaProxy.localPort}") + val fbsimctlEndpoint = URI("http://${remote.publicHostName}:${allocatedPorts.fbsimctlPort}/$udid/") + val wdaEndpoint = URI("http://${remote.publicHostName}:${wdaProxy.localPort}") val calabashPort = calabashProxy.localPort @Volatile @@ -147,7 +147,7 @@ class Device( val ports = allocatedPorts.toSet() require(ports.contains(port)) { "Port $port is not in user ports range $ports" } - return URL("http://${remote.hostName}:$port/") + return URL("http://${remote.publicHostName}:$port/") } fun dispose() { diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxy.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxy.kt index c549a629..d901e5f8 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxy.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxy.kt @@ -32,30 +32,49 @@ class UsbProxy( override fun toString(): String = "" - private var childProcess: ChildProcess? = null + private var iproxy: ChildProcess? = null + private var socat: ChildProcess? = null fun start() { - childProcess = childFactory( + iproxy = childFactory( remote.hostName, remote.userName, - listOf("iproxy", localPort.toString(), devicePort.toString(), udid), + listOf(IPROXY_BIN, localPort.toString(), devicePort.toString(), udid), false, - { message -> logger.info(logMarker, "${this}: iproxy : ${message.trim()}") }, - { message -> logger.warn(logMarker, "${this}: iproxy : ${message.trim()}") } + { message -> logger.debug(logMarker, "${this}: iproxy : ${message.trim()}") }, + { message -> logger.debug(logMarker, "${this}: iproxy : ${message.trim()}") } + ) + + socat = childFactory( + remote.hostName, + remote.userName, + listOf(SOCAT_BIN, "tcp-listen:$localPort,reuseaddr,fork", "tcp:0.0.0.0:$localPort"), + false, + { message -> logger.debug(logMarker, "${this}: socat : ${message.trim()}") }, + { message -> logger.debug(logMarker, "${this}: socat : ${message.trim()}") } ) } fun isHealthy(): Boolean { - return childProcess?.isAlive() ?: false + return (iproxy?.isAlive() ?: false) && (socat?.isAlive() ?: false) } fun stop() { - if (childProcess == null) { - return + if (iproxy != null) { + logger.debug(logMarker, "$this — Killing child process $iproxy") + iproxy!!.kill() + iproxy = null + } + + if (socat !=null) { + logger.debug(logMarker, "$this — Killing child process $socat") + socat!!.kill() + socat = null } + } - logger.debug(logMarker, "$this — Killing child process $childProcess") - childProcess!!.kill() - childProcess = null + companion object { + const val IPROXY_BIN = "/usr/local/bin/iproxy" + const val SOCAT_BIN = "/usr/local/bin/socat" } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt index e0a9c699..a0c971e7 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt @@ -48,8 +48,8 @@ class Simulator ( //region public properties copied from Ruby override val ref = deviceRef override val udid = deviceInfo.udid - override val fbsimctlEndpoint = URI("http://${remote.hostName}:${allocatedPorts.fbsimctlPort}/$udid/") - override val wdaEndpoint= URI("http://${remote.hostName}:${allocatedPorts.wdaPort}/") + override val fbsimctlEndpoint = URI("http://${remote.publicHostName}:${allocatedPorts.fbsimctlPort}/$udid/") + override val wdaEndpoint= URI("http://${remote.publicHostName}:${allocatedPorts.wdaPort}/") override val userPorts = allocatedPorts override val info = deviceInfo override val calabashPort: Int = allocatedPorts.calabashPort @@ -449,7 +449,7 @@ class Simulator ( val ports = allocatedPorts.toSet() require(ports.contains(port)) { "Port $port is not in user ports range $ports" } - return URL("http://${remote.hostName}:$port/") + return URL("http://${remote.publicHostName}:$port/") } //region approveAccess diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClient.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClient.kt index c08172e7..748fc2f3 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClient.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClient.kt @@ -3,10 +3,7 @@ package com.badoo.automation.deviceserver.util import com.badoo.automation.deviceserver.util.HttpCodes.* import okhttp3.OkHttpClient import okhttp3.Request -import java.net.ConnectException -import java.net.SocketTimeoutException -import java.net.URL -import java.net.UnknownHostException +import java.net.* import java.util.concurrent.TimeUnit class CustomHttpClient { @@ -31,13 +28,15 @@ class CustomHttpClient { httpCode = result.code() ) } catch (e: SocketTimeoutException) { - return HttpResult(522) + return HttpResult(ConnectionTimedOut.code) } catch (e: UnknownHostException) { return HttpResult(OriginIsUnreachable.code) } catch (e: ConnectException) { return HttpResult(WebServerIsDown.code) } catch (e: java.io.EOFException) { return HttpResult(NetworkReadTimeoutError.code) + } catch (e: java.io.IOException) { // we can get connection reset or stream end from iproxy or socat + return HttpResult(WebServerIsDown.code) } catch (e: RuntimeException) { return HttpResult(UnknownError.code) } diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/RemoteTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/RemoteTest.kt index b125c33d..3450ab54 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/RemoteTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/RemoteTest.kt @@ -18,7 +18,7 @@ class RemoteTest { private lateinit var remote: Remote @Before fun setUp() { - remote = Remote("host", "user", localExecutor, remoteExecutor) + remote = Remote("host", "user", "", localExecutor, remoteExecutor) } @Test diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt index 5d11d1fd..21a53e58 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt @@ -148,6 +148,7 @@ class SimulatorsNodeTest { private fun mockForSimulatorMocks(vararg simulatorMocks: Pair) { whenever(iRemote.hostName).thenReturn("rem.ote.node") + whenever(iRemote.publicHostName).thenReturn("rem.ote.node") whenever(iRemote.fbsimctl).thenReturn(fbSimctl) whenever(fbSimctl.listDeviceSets()).thenReturn("/node/specific/device/set") var fbsimmock = whenever(iSimulatorProvider.match(eq(desiredCapabilities), any())) diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/HostFactoryTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/HostFactoryTest.kt index 549c6789..b76fb4a8 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/HostFactoryTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/HostFactoryTest.kt @@ -34,7 +34,7 @@ class HostFactoryTest { ) private var remoteMock: IRemote = mockThis() - private var remoteMockProvider: (String, String) -> IRemote = { host, _ -> + private var remoteMockProvider: (String, String, String) -> IRemote = { host, _, _ -> assertThat(host, equalTo(hostName)) remoteMock } From 4bfc3044436ee4de613c4302f75963eff972b207 Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Mon, 25 Jun 2018 16:40:28 +0100 Subject: [PATCH 021/131] Wrap clean caches into try-catch to prevent background task crash --- .../host/management/ISimulatorHostChecker.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt index 412c92fb..65fef491 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt @@ -93,10 +93,13 @@ class SimulatorHostChecker( val cleanUpRunnable: Runnable = object : Runnable { override fun run() { caches.forEach { - val command = "find $it -maxdepth 0 -mmin +60 -exec rm -rf {} \\;" - val r = remote.shell(command, returnOnFailure = true) // find returns non zero if nothing found - if (!r.isSuccess || r.stdErr.isNotEmpty() || r.stdOut.isNotEmpty()) { - logger.debug(logMarker, "[disc cleaner] $this returned non-empty $r") + try { + val r = remote.shell("find $it -maxdepth 0 -mmin +60 -exec rm -rf {} \\;", returnOnFailure = true) // find returns non zero if nothing found + if (!r.isSuccess || r.stdErr.isNotEmpty() || r.stdOut.isNotEmpty()) { + logger.debug(logMarker, "[disc cleaner] $this returned non-empty. ERR: [${r.stdErr}], OUT: [${r.stdOut}]") + } + } catch (e: RuntimeException) { + logger.debug(logMarker, "[disc cleaner] $this got exception while cleaning caches: ${e.message}", e) } } } From 789ca664e64f2af5ce14d33509a1914898384e1f Mon Sep 17 00:00:00 2001 From: Tim Baverstock Date: Fri, 29 Jun 2018 15:50:56 +0100 Subject: [PATCH 022/131] Info endpoints improvements - Add /status/config endpoint - Make / endpoint return html --- .../automation/deviceserver/DeviceServer.kt | 15 +++++++--- .../controllers/StatusController.kt | 29 ++++++++++++++----- 2 files changed, 32 insertions(+), 12 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt index c4257e46..869a6425 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt @@ -24,10 +24,12 @@ import io.ktor.features.CallLogging import io.ktor.features.ContentNegotiation import io.ktor.features.DefaultHeaders import io.ktor.features.StatusPages +import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode import io.ktor.jackson.jackson import io.ktor.request.uri import io.ktor.response.respond +import io.ktor.response.respondText import io.ktor.routing.* import io.ktor.server.engine.ApplicationEngineEnvironmentReloading import io.ktor.server.engine.ShutDownUrl @@ -129,11 +131,16 @@ fun Application.module() { logger.info("Server: Installing routing...") routes = install(Routing) { get { - call.respond(statusController.welcomeMessage(routes)) + call.respondText(statusController.welcomeMessage(routes), ContentType.Text.Html) } - get("status") { - val code = if (deviceManager.isReady()) HttpStatusCode.OK else HttpStatusCode.ServiceUnavailable - call.respond(code, statusController.getServerStatus()) + route("status") { + get { + val code = if (deviceManager.isReady()) HttpStatusCode.OK else HttpStatusCode.ServiceUnavailable + call.respond(code, statusController.getServerStatus()) + } + get("config") { + call.respond(config) + } } route("devices") { get { diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/StatusController.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/StatusController.kt index 53c534fa..4b5af1c1 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/StatusController.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/StatusController.kt @@ -4,10 +4,10 @@ import com.badoo.automation.deviceserver.host.management.IDeviceManager import io.ktor.routing.Route class StatusController(private val deviceManager: IDeviceManager) { - fun welcomeMessage(routes: Route?): String { - return "Welcome to the device server.\n" + - childHierarchy(routes?.children ?: emptyList()) + "\n" + - "Minimal /status, but /quitquitquit works\n" + fun welcomeMessage(route: Route?): String { + return "Welcome to the device server.

\n" +
+                (if (route == null) "No routes set yet!?" else childHierarchy(route)) + "\n" +
+                "
Minimal /status, but /quitquitquit works\n" } fun getServerStatus(): Map { @@ -19,9 +19,22 @@ class StatusController(private val deviceManager: IDeviceManager) { ) } - private fun childHierarchy(children: List, margin: String = "\n. "): Map { - return children.sortedBy { it.toString() } - .map { margin + it to childHierarchy(it.children, "$margin. ").toString() } - .toMap() + private fun childHierarchy(route: Route, path: String = "", margin: String = "\n. "): String { + val selector = route.selector.toString() + val maybe_real_endpoint = + if (!selector.startsWith("(method:")) + "" + else + { + val path_or_link = + if (selector == "(method:GET)" && !path.contains('{')) + "$path" // Safe to provide as a link: idempotent. + else + path + "$margin $path_or_link ${route.selector}" + } + return maybe_real_endpoint + + route.children.sortedBy { it.toString() } + .joinToString("" ) { childHierarchy(it, route.toString(), "$margin. ") } } } From 9dc9489d958f994a49ecbce5f2bd6210577012da Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Wed, 27 Jun 2018 09:50:40 +0100 Subject: [PATCH 023/131] Fix default device set path --- .../badoo/automation/deviceserver/host/SimulatorsNode.kt | 2 +- .../deviceserver/host/management/ISimulatorHostChecker.kt | 4 ++-- .../automation/deviceserver/ios/fbsimctl/FBSimctl.kt | 8 +++----- .../automation/deviceserver/ios/fbsimctl/IFBSimctl.kt | 2 +- .../automation/deviceserver/host/SimulatorsNodeTest.kt | 7 +------ .../automation/deviceserver/ios/fbsimctl/FBSimctlTest.kt | 6 +++--- 6 files changed, 11 insertions(+), 18 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt index 520e11a3..ee9fd651 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt @@ -50,7 +50,7 @@ class SimulatorsNode( } private val supportedArchitectures = listOf("x86_64") - private val deviceSetPath: String by lazy { remote.fbsimctl.listDeviceSets() } + private val deviceSetPath: String by lazy { remote.fbsimctl.defaultDeviceSet() } private val concurrentBoot = newFixedThreadPoolContext(concurrentBoots, "sim_boot_${remote.hostName}") private fun getDeviceFor(ref: DeviceRef): ISimulator { diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt index 65fef491..326a5bf6 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt @@ -81,8 +81,8 @@ class SimulatorHostChecker( logger.warn(logMarker, "Failed to shutdown simulator because: ${e.javaClass}: message: [${e.message}]") } - val deviceSetsPath = remote.fbsimctl.listDeviceSets() - check(!deviceSetsPath.isBlank()) { "Device sets must not be blank" } // fbsimctl.listDeviceSets will throw if empty. but paranoid mode on. + val deviceSetsPath = remote.fbsimctl.defaultDeviceSet() + check(!deviceSetsPath.isBlank()) { "Device sets must not be blank" } // fbsimctl.defaultDeviceSet will throw if empty. but paranoid mode on. val caches = listOf( "/private/var/folders/*/*/*/*-*-*/*.app", diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt index d2856891..f498ecc1 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt @@ -48,13 +48,11 @@ class FBSimctl( * returns path to device sets * E.g. "/Users/qa/Library/Developer/CoreSimulator/Devices" */ - override fun listDeviceSets(): String { + override fun defaultDeviceSet(): String { val result = fbsimctl("list_device_sets", jsonFormat = false) - val deviceSets = result - .split(NEW_LINE) - .findLast { it.startsWith("/") } + val deviceSets = result.lines().first() - if (deviceSets == null || deviceSets.isBlank()) { + if (deviceSets.isBlank()) { throw FBSimctlError("No device_sets returned by fbsimctl") } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctl.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctl.kt index e6bc02d1..3a7ca6b0 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctl.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctl.kt @@ -22,7 +22,7 @@ interface IFBSimctl { * returns path to device sets * E.g. "/Users/qa/Library/Developer/CoreSimulator/Devices" */ - fun listDeviceSets(): String + fun defaultDeviceSet(): String fun eraseSimulator(udid: UDID): String fun create(model: String?, os: String?, transitional: Boolean): FBSimctlDevice diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt index 21a53e58..680fe9a8 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt @@ -1,11 +1,9 @@ package com.badoo.automation.deviceserver.host import com.badoo.automation.deviceserver.JsonMapper -import com.badoo.automation.deviceserver.command.CommandResult import com.badoo.automation.deviceserver.data.* import com.badoo.automation.deviceserver.host.management.ISimulatorHostChecker import com.badoo.automation.deviceserver.host.management.PortAllocator -import com.badoo.automation.deviceserver.host.management.SimulatorHostChecker import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDevice import com.badoo.automation.deviceserver.ios.simulator.ISimulator @@ -16,12 +14,9 @@ import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.sameInstance import org.hamcrest.MatcherAssert.assertThat import org.junit.Assert.* -import org.junit.Ignore import org.junit.Test -import org.slf4j.Logger import java.io.File import java.net.URI -import java.time.Duration class SimulatorsNodeTest { private val iRemote: IRemote = mockThis() @@ -150,7 +145,7 @@ class SimulatorsNodeTest { whenever(iRemote.hostName).thenReturn("rem.ote.node") whenever(iRemote.publicHostName).thenReturn("rem.ote.node") whenever(iRemote.fbsimctl).thenReturn(fbSimctl) - whenever(fbSimctl.listDeviceSets()).thenReturn("/node/specific/device/set") + whenever(fbSimctl.defaultDeviceSet()).thenReturn("/node/specific/device/set") var fbsimmock = whenever(iSimulatorProvider.match(eq(desiredCapabilities), any())) simulatorMocks.forEach { pair -> fbsimmock = fbsimmock.thenReturn(pair.second) diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlTest.kt index 0fc6d22f..1063e080 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlTest.kt @@ -23,14 +23,14 @@ class FBSimctlTest { @Test fun mustTrimLastNewLine() { whenever(executor.exec(anyList(), anyMap(), anyType(), anyBoolean(), any(), anyType())).thenReturn(CommandResult("/a\n", "", ByteArray(0), 0)) val fbSimctl = FBSimctl(executor, parser) - val deviceSets = fbSimctl.listDeviceSets() + val deviceSets = fbSimctl.defaultDeviceSet() Assert.assertEquals("/a", deviceSets) } @Test(expected = FBSimctlError::class) fun shouldThrowWhenNoDeviceSets() { - whenever(executor.exec(anyList(), anyMap(), anyType(), anyBoolean(), any(), anyType())).thenReturn(CommandResult("asdfa\n", "", ByteArray(0), 0)) + whenever(executor.exec(anyList(), anyMap(), anyType(), anyBoolean(), any(), anyType())).thenReturn(CommandResult("\n", "", ByteArray(0), 0)) val fbSimctl = FBSimctl(executor, parser) - fbSimctl.listDeviceSets() + fbSimctl.defaultDeviceSet() } } \ No newline at end of file From a073f5a0b4b9033692d081867fd0fdb1be5819cf Mon Sep 17 00:00:00 2001 From: Nick Abalov Date: Fri, 29 Jun 2018 19:26:09 +0100 Subject: [PATCH 024/131] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2a18d24c..3bc129d0 100644 --- a/README.md +++ b/README.md @@ -16,9 +16,9 @@ A server for managing, booting, and controlling simulators and devices on remote export JAVA_HOME=$(/usr/libexec/java_home -v 10 -F) ``` -### Phyiscal Devices +### Physical Devices -To use phyiscal devices extra dependencies are required: +To use physical devices extra dependencies are required: ``` brew install libimobiledevice --HEAD brew install socat From 8840ba2329f6c6f6b08ba6e739204ca4588ede4d Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Tue, 17 Jul 2018 12:11:05 +0100 Subject: [PATCH 025/131] iOS Add methods to read files from data containers on server --- .../automation/deviceserver/DeviceServer.kt | 13 +++ .../controllers/DevicesController.kt | 13 ++- .../automation/deviceserver/data/DataPath.kt | 11 ++ .../deviceserver/host/DevicesNode.kt | 4 + .../deviceserver/host/ISimulatorsNode.kt | 3 + .../deviceserver/host/SimulatorsNode.kt | 8 ++ .../host/management/DeviceManager.kt | 8 ++ .../host/management/IDeviceManager.kt | 2 + .../deviceserver/ios/simulator/ISimulator.kt | 2 + .../deviceserver/ios/simulator/Simulator.kt | 8 ++ .../ios/simulator/data/DataContainer.kt | 51 ++++++++ .../simulator/data/DataContainerException.kt | 3 + .../ios/simulator/data/FileSystem.kt | 24 ++++ .../ios/simulator/data/DataContainerTest.kt | 109 ++++++++++++++++++ .../ios/simulator/data/FileSystemTest.kt | 53 +++++++++ 15 files changed, 308 insertions(+), 4 deletions(-) create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DataPath.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainer.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainerException.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/FileSystem.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainerTest.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/FileSystemTest.kt diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt index 869a6425..5ce60377 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt @@ -2,6 +2,7 @@ package com.badoo.automation.deviceserver import com.badoo.automation.deviceserver.controllers.DevicesController import com.badoo.automation.deviceserver.controllers.StatusController +import com.badoo.automation.deviceserver.data.DataPath import com.badoo.automation.deviceserver.data.DesiredCapabilities import com.badoo.automation.deviceserver.data.ErrorDto import com.badoo.automation.deviceserver.data.toDto @@ -180,6 +181,18 @@ fun Application.module() { get("crashes/last") { call.respond(devicesController.getLastCrashLog(param(call, "ref"))) } + route("data") { + post("pull_file") { + val ref = param(call, "ref") + val dataPath = jsonContent(call) + call.respond(devicesController.pullFile(ref, dataPath)) + } + post("list_files") { + val ref = param(call, "ref") + val dataPath = jsonContent(call) + call.respond(devicesController.listFiles(ref, dataPath)) + } + } route("video") { get { //FIXME: should be a better way of streaming a file over HTTP. without caching bytes in server's memory. Investigating ByteReadChannel diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt index 3bfe8ac8..ec8ceb4b 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt @@ -1,10 +1,7 @@ package com.badoo.automation.deviceserver.controllers import com.badoo.automation.deviceserver.EmptyMap -import com.badoo.automation.deviceserver.data.DesiredCapabilities -import com.badoo.automation.deviceserver.data.DeviceDTO -import com.badoo.automation.deviceserver.data.DeviceRef -import com.badoo.automation.deviceserver.data.SimulatorStatusDTO +import com.badoo.automation.deviceserver.data.* import com.badoo.automation.deviceserver.host.management.IDeviceManager import com.fasterxml.jackson.databind.JsonNode import io.ktor.auth.UserIdPrincipal @@ -86,4 +83,12 @@ class DevicesController(private val deviceManager: IDeviceManager) { fun getTotalCapacity(desiredCapabilities: DesiredCapabilities): Map { return deviceManager.getTotalCapacity(desiredCapabilities) } + + fun listFiles(ref: DeviceRef, dataPath: DataPath): List { + return deviceManager.listFiles(ref, dataPath) + } + + fun pullFile(ref: DeviceRef, dataPath: DataPath): ByteArray { + return deviceManager.pullFile(ref, dataPath) + } } \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DataPath.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DataPath.kt new file mode 100644 index 00000000..6e9fddf4 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DataPath.kt @@ -0,0 +1,11 @@ +package com.badoo.automation.deviceserver.data + +import com.fasterxml.jackson.annotation.JsonProperty +import java.nio.file.Path + +class DataPath( + @JsonProperty("bundle_id") + val bundleId: String, + + @JsonProperty("path") + val path: Path) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt index 97ad3a12..0d202782 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt @@ -225,6 +225,10 @@ class DevicesNode( override fun videoRecordingStart(deviceRef: DeviceRef): Unit = throw(NotImplementedError()) override fun videoRecordingStop(deviceRef: DeviceRef): Unit = throw(NotImplementedError()) + + override fun listFiles(deviceRef: DeviceRef, dataPath: DataPath): List = throw(NotImplementedError()) + + override fun pullFile(deviceRef: DeviceRef, dataPath: DataPath): ByteArray = throw(NotImplementedError()) // endregion private fun deviceToDto(deviceRef: DeviceRef, device: Device): DeviceDTO { diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt index ce0571dd..d6f6e6eb 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt @@ -19,6 +19,9 @@ interface ISimulatorsNode { fun videoRecordingStart(deviceRef: DeviceRef) fun videoRecordingStop(deviceRef: DeviceRef) + fun listFiles(deviceRef: DeviceRef, dataPath: DataPath): List + fun pullFile(deviceRef: DeviceRef, dataPath: DataPath): ByteArray + val remoteAddress: String fun isReachable(): Boolean fun prepareNode() diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt index ee9fd651..89622217 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt @@ -214,6 +214,14 @@ class SimulatorsNode( getDeviceFor(deviceRef).videoRecorder.stop() } + override fun listFiles(deviceRef: DeviceRef, dataPath: DataPath): List { + return getDeviceFor(deviceRef).dataContainer(dataPath.bundleId).listFiles(dataPath.path) + } + + override fun pullFile(deviceRef: DeviceRef, dataPath: DataPath): ByteArray { + return getDeviceFor(deviceRef).dataContainer(dataPath.bundleId).readFile(dataPath.path) + } + override fun toString(): String { return "${javaClass.simpleName} at $remoteAddress" } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt index 0eaf1ace..03a77199 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt @@ -134,4 +134,12 @@ class DeviceManager( override fun isReady(): Boolean { return nodeRegistry.getInitialRegistrationComplete() } + + override fun listFiles(ref: DeviceRef, dataPath: DataPath): List { + return nodeRegistry.activeDevices.getNodeFor(ref).listFiles(ref, dataPath) + } + + override fun pullFile(ref: DeviceRef, dataPath: DataPath): ByteArray { + return nodeRegistry.activeDevices.getNodeFor(ref).pullFile(ref, dataPath) + } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt index f9e3d668..7a3f6b5e 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt @@ -24,4 +24,6 @@ interface IDeviceManager { fun getStatus(): Map fun releaseUserDevices(userId: String, reason: String) fun isReady(): Boolean + fun listFiles(ref: DeviceRef, dataPath: DataPath): List + fun pullFile(ref: DeviceRef, dataPath: DataPath): ByteArray } \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt index 31081731..5716bbc2 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt @@ -1,6 +1,7 @@ package com.badoo.automation.deviceserver.ios.simulator import com.badoo.automation.deviceserver.data.* +import com.badoo.automation.deviceserver.ios.simulator.data.DataContainer import com.badoo.automation.deviceserver.ios.simulator.video.SimulatorVideoRecorder import java.net.URI import java.net.URL @@ -27,4 +28,5 @@ interface ISimulator { fun release(reason: String) fun clearSafariCookies(): Map fun lastCrashLog(): CrashLog + fun dataContainer(bundleId: String): DataContainer } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt index a0c971e7..a8514109 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt @@ -9,6 +9,8 @@ import com.badoo.automation.deviceserver.ios.proc.FbsimctlProc import com.badoo.automation.deviceserver.ios.proc.SimulatorWebDriverAgent import com.badoo.automation.deviceserver.ios.simulator.backup.ISimulatorBackup import com.badoo.automation.deviceserver.ios.simulator.backup.SimulatorBackup +import com.badoo.automation.deviceserver.ios.simulator.data.DataContainer +import com.badoo.automation.deviceserver.ios.simulator.data.FileSystem import com.badoo.automation.deviceserver.ios.simulator.video.SimulatorVideoRecorder import com.badoo.automation.deviceserver.util.executeWithTimeout import com.badoo.automation.deviceserver.util.pollFor @@ -72,6 +74,7 @@ class Simulator ( LogMarkers.UDID to udid, LogMarkers.HOSTNAME to remote.hostName )) + private val fileSystem = FileSystem(remote, udid) //endregion //region properties from ruby with backing mutable field @@ -577,5 +580,10 @@ class Simulator ( .map { it.trim() } .filter { it.isNotBlank() } } + + override fun dataContainer(bundleId: String): DataContainer { + return fileSystem.dataContainer(bundleId) + } + //endregion } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainer.kt new file mode 100644 index 00000000..37882754 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainer.kt @@ -0,0 +1,51 @@ +package com.badoo.automation.deviceserver.ios.simulator.data + +import com.badoo.automation.deviceserver.command.ShellUtils +import com.badoo.automation.deviceserver.host.IRemote +import java.io.File +import java.nio.file.Path + +class DataContainer( + private val remote: IRemote, + internal val basePath: Path, + private val bundleId: String + ) { + + fun listFiles(path: Path): List { + val expandedPath = sshNoEscapingWorkaround(expandPath(path).toString()) + + val result = remote.execIgnoringErrors(listOf("ls", "-1", "-p", expandedPath)) + if (!result.isSuccess) { + throw(DataContainerException("Could not list $path for $bundleId: $result")) + } + + return result.stdOut.lines().filter { it.isNotEmpty() } + } + + fun readFile(path: Path): ByteArray { + val expandedPath = sshNoEscapingWorkaround(expandPath(path).toString()) + + val result = remote.captureFile(File(expandedPath)) + if (!result.isSuccess) { + throw DataContainerException("Could not read file $path for $bundleId: $result") + } + return result.stdOutBytes + } + + private fun sshNoEscapingWorkaround(path: String): String { + // FIXME: fix escaping on ssh side and remove workarounds + return when { + remote.isLocalhost() -> path + else -> ShellUtils.escape(path) + } + } + + private fun expandPath(path: Path): Path { + val expanded = basePath.resolve(path).normalize() + if (!expanded.startsWith(basePath)) { + throw DataContainerException("$path points outside the container of $bundleId") + } + + return expanded + } +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainerException.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainerException.kt new file mode 100644 index 00000000..a7440327 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainerException.kt @@ -0,0 +1,3 @@ +package com.badoo.automation.deviceserver.ios.simulator.data + +class DataContainerException(message: String): RuntimeException(message) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/FileSystem.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/FileSystem.kt new file mode 100644 index 00000000..d73312a8 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/FileSystem.kt @@ -0,0 +1,24 @@ +package com.badoo.automation.deviceserver.ios.simulator.data + +import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.host.IRemote +import java.nio.file.Paths + +class FileSystem( + private val remote: IRemote, + private val udid: UDID +) { + fun dataContainer(bundleId: String): DataContainer { + val app = remote.fbsimctl.listApps(udid).find { it.bundle.bundle_id == bundleId } + + if (app?.data_container == null) { + throw(IllegalArgumentException("No data container for $bundleId")) + } + + return DataContainer( + remote, + Paths.get(app.data_container), + bundleId + ) + } +} diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainerTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainerTest.kt new file mode 100644 index 00000000..6b69bb74 --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainerTest.kt @@ -0,0 +1,109 @@ +package com.badoo.automation.deviceserver.ios.simulator.data + +import com.badoo.automation.deviceserver.command.CommandResult +import com.badoo.automation.deviceserver.host.IRemote +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl +import com.badoo.automation.deviceserver.mockThis +import com.nhaarman.mockito_kotlin.whenever +import org.junit.Before +import org.junit.Test +import org.mockito.Mockito +import java.nio.file.Paths +import kotlin.test.assertEquals +import kotlin.test.assertFails + +class DataContainerTest { + + private val remote: IRemote = mockThis() + private val fbsimctl: FBSimctl = mockThis() + + private val containerPathStub = + "/Users/qa/Library/Developer/CoreSimulator/Devices/UDID/data/Containers/Data/Application/A2C79BEC-FD2C-4676-BA9B-B6A62AFE193A" + + @Before + fun setUp() { + whenever(remote.fbsimctl).thenReturn(fbsimctl) + } + + @Test + fun shouldListFiles() { + val cmdResult = CommandResult("Caches/\nImage Cache/\nfile.ext\n", "", ByteArray(0), 0) + + whenever( + remote.execIgnoringErrors( + Mockito.anyList(), + Mockito.anyMap(), + Mockito.anyLong() + ) + ).thenReturn(cmdResult) + + val container = DataContainer( + remote = remote, + basePath = Paths.get(containerPathStub), + bundleId = "test.bundle" + ) + val actual = container.listFiles(Paths.get("Library/Caches")) + + val expected = listOf( + "Caches/", + "Image Cache/", + "file.ext" + ) + + assertEquals(expected, actual) + } + + @Test + fun shouldReturnEmptyListForEmptyDirectory() { + val cmdResult = CommandResult("", "", ByteArray(0), 0) + + whenever( + remote.execIgnoringErrors( + Mockito.anyList(), + Mockito.anyMap(), + Mockito.anyLong() + ) + ).thenReturn(cmdResult) + + val container = DataContainer( + remote = remote, + basePath = Paths.get(containerPathStub), + bundleId = "test.bundle" + ) + val actual = container.listFiles(Paths.get("Library/Caches")) + + assertEquals(emptyList(), actual) + } + + @Test + fun shouldReadFileAsByteArray() { + val expected = "123".toByteArray() + val cmdResult = CommandResult("", "", expected, 0) + + whenever(remote.captureFile(Paths.get(containerPathStub, "Library/Caches/file.txt").toFile())).thenReturn( + cmdResult + ) + + val container = DataContainer( + remote = remote, + basePath = Paths.get(containerPathStub), + bundleId = "test.bundle" + ) + val actual = container.readFile(Paths.get("Library/Caches/file.txt")) + + assertEquals(expected, actual) + } + + @Test + fun shouldRejectPathOutsideContainer() { + val container = DataContainer( + remote = remote, + basePath = Paths.get(containerPathStub), + bundleId = "test.bundle" + ) + + assertFails { + container.readFile(Paths.get("../file")) + } + } +} diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/FileSystemTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/FileSystemTest.kt new file mode 100644 index 00000000..1716d9ef --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/FileSystemTest.kt @@ -0,0 +1,53 @@ +package com.badoo.automation.deviceserver.ios.simulator.data + +import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.host.IRemote +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlAppInfo +import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlAppInfoBundle +import com.badoo.automation.deviceserver.mockThis +import com.nhaarman.mockito_kotlin.whenever +import org.junit.Before +import org.junit.Test +import java.nio.file.Paths +import kotlin.test.assertEquals +import kotlin.test.assertFails + +class FileSystemTest { + + private val udid: UDID = "udid" + private val remote: IRemote = mockThis() + private val fbsimctl: FBSimctl = mockThis() + + private val containerPathStub = + "/Users/qa/Library/Developer/CoreSimulator/Devices/UDID/data/Containers/Data/Application/A2C79BEC-FD2C-4676-BA9B-B6A62AFE193A/" + private val bundleInfoStub = FBSimctlAppInfo( + containerPathStub, + FBSimctlAppInfoBundle(null, "test.bundle", null, null), + null + ) + + @Before + fun setUp() { + whenever(remote.fbsimctl).thenReturn(fbsimctl) + } + + @Test + fun shouldCreateDataContainer() { + whenever(fbsimctl.listApps(udid)).thenReturn(listOf(bundleInfoStub)) + + val container = FileSystem(remote, udid).dataContainer("test.bundle") + + assertEquals(Paths.get(containerPathStub), container.basePath) + } + + @Test + fun shouldFailOnNonExistingBundleId() { + whenever(fbsimctl.listApps(udid)).thenReturn(listOf(bundleInfoStub)) + + + assertFails { + FileSystem(remote, udid).dataContainer("non-existing.bundle.id") + } + } +} From 4d2aaec78652b8bc1db8687f4f502a2b01dd6487 Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Thu, 19 Jul 2018 14:36:33 +0100 Subject: [PATCH 026/131] Add stats fields for Kibana --- .../deviceserver/ios/simulator/Simulator.kt | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt index a8514109..a0dca0d7 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt @@ -69,11 +69,12 @@ class Simulator ( private val backup: ISimulatorBackup = SimulatorBackup(remote, udid, deviceSetPath) private val simulatorStatus = SimulatorStatus() private val logger = LoggerFactory.getLogger(javaClass.simpleName) - private val logMarker: Marker = MapEntriesAppendingMarker(mapOf( + private val commonLogMarkerDetails = mapOf( LogMarkers.DEVICE_REF to deviceRef, LogMarkers.UDID to udid, LogMarkers.HOSTNAME to remote.hostName - )) + ) + private val logMarker: Marker = MapEntriesAppendingMarker(commonLogMarkerDetails) private val fileSystem = FileSystem(remote, udid) //endregion @@ -278,10 +279,16 @@ class Simulator ( } } - private fun logTiming(actionName: String, function: () -> Unit) { + private fun logTiming(actionName: String, action: () -> Unit) { logger.info(logMarker, "Device ${Simulator@this} starting action <$actionName>") - val elapsed = measureTimeMillis(function) - logger.info(logMarker, "Device ${Simulator@this} action <$actionName> took ${elapsed / 1000} seconds") + val millis = measureTimeMillis(action) + val seconds = millis / 1000 + val measurement = mutableMapOf( + "action_name" to actionName, + "duration" to seconds + ) + measurement.putAll(commonLogMarkerDetails) + logger.info(MapEntriesAppendingMarker(measurement), "Device ${Simulator@this} action <$actionName> took $seconds seconds") } //endregion From 7728c42adf1d2385d6c715b90b1e8df0d73d6a0e Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Tue, 31 Jul 2018 17:40:25 +0100 Subject: [PATCH 027/131] Ignore fbsimctl log events --- .../host/management/ISimulatorHostChecker.kt | 1 - .../deviceserver/ios/fbsimctl/FBSimctl.kt | 10 +++--- .../ios/fbsimctl/FBSimctlResponseParser.kt | 33 +++++++++++++------ .../ios/fbsimctl/IFBSimctlResponseParser.kt | 2 +- .../deviceserver/ios/fbsimctl/FBSimctlTest.kt | 9 +++-- .../fbsimctl/FbSimctlResponseParserTest.kt | 11 ------- 6 files changed, 36 insertions(+), 30 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt index 326a5bf6..3f36014f 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt @@ -25,7 +25,6 @@ class SimulatorHostChecker( private val diskCleanupInterval: Duration = Duration.ofMinutes(15) ) : ISimulatorHostChecker { companion object { - private const val EXPECTED_XCODE_VERSION = "Xcode 9." private const val EXPECTED_FBSIMCTL = "HEAD-292a1bd" } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt index f498ecc1..df8149ae 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt @@ -49,14 +49,14 @@ class FBSimctl( * E.g. "/Users/qa/Library/Developer/CoreSimulator/Devices" */ override fun defaultDeviceSet(): String { - val result = fbsimctl("list_device_sets", jsonFormat = false) - val deviceSets = result.lines().first() + val response = fbsimctl("list_device_sets", jsonFormat = true) + val deviceSet = parser.parseDeviceSets(response).firstOrNull() - if (deviceSets.isBlank()) { + if (deviceSet == null) { throw FBSimctlError("No device_sets returned by fbsimctl") + } else { + return deviceSet } - - return deviceSets } override fun eraseSimulator(udid: UDID) = fbsimctl(cmd = "erase", udid = udid) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlResponseParser.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlResponseParser.kt index e5174705..5cd54dfd 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlResponseParser.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlResponseParser.kt @@ -9,8 +9,7 @@ class FBSimctlResponseParser : IFBSimctlResponseParser { */ override fun parse(response: String): List> { val mapper = JsonMapper() - return response.lines() - .filter { isDiscreteEvent(it) } + return filteredResponseLines(response) .map { mapper.fromJson>(it) } } @@ -18,22 +17,21 @@ class FBSimctlResponseParser : IFBSimctlResponseParser { * Parses device list */ override fun parseDeviceList(response: String): List - = response.lines() - .filter { isDiscreteEvent(it) } + = filteredResponseLines(response) .filter { isOfEventName(it, "list") } .map { fromJson(it, FBSimctlDeviceListResponse::class.java) } .map { it.subject } - override fun parseDeviceListHttp(response: String): FBSimctlDevice - = fromJson(response, FBSimctlDeviceListHttpResponse::class.java) - .subject.first().subject + override fun parseDeviceSets(response: String): List { + return parse(response) + .map { it["subject"] as String } + } /** * Parses applications list */ override fun parseApplicationsList(response: String): List - = response.lines() - .filter { isDiscreteEvent(it) } + = filteredResponseLines(response) .filter { isOfEventName(it, "list_apps") } .map { fromJson(it, FBSimctlAppListResponse::class.java) } .map { it.subject }.flatten() @@ -57,6 +55,7 @@ class FBSimctlResponseParser : IFBSimctlResponseParser { try { parsedResponse = response.lines() + .filter { !isLogEvent(it) } .filter { isOfEventName(it, "create") } .filter { isEnded(it) } .map { fromJson(it, FBSimctlCreateDeviceResponse::class.java) } @@ -89,7 +88,21 @@ class FBSimctlResponseParser : IFBSimctlResponseParser { */ private fun isDiscreteEvent(it: String) = it.contains(":\"discrete\"") - // + /** + * Filter log lines + */ + private fun isLogEvent(it: String) = it.contains(""""event_name":"log"""") + + + /** + * Sanitize fbsimctl response + */ + private fun filteredResponseLines(response: String): List { + return response.lines() + .filter { isDiscreteEvent(it) } + .filter { !isLogEvent(it) } + } + /** * Filter events by name * FIXME: quick workaround added to skip extraneous events and prevent parser from failing, diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctlResponseParser.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctlResponseParser.kt index 19d1247b..bb355ef7 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctlResponseParser.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctlResponseParser.kt @@ -6,5 +6,5 @@ interface IFBSimctlResponseParser { fun parseDiagnosticInfo(response: String): FBSimctlDeviceDiagnosticInfo fun parseDeviceCreation(response: String, isTransitional: Boolean): FBSimctlDevice fun parse(response: String): List> - fun parseDeviceListHttp(response: String): FBSimctlDevice + fun parseDeviceSets(response: String): List } \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlTest.kt index 1063e080..f8d3f93e 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlTest.kt @@ -15,14 +15,18 @@ import org.slf4j.Marker class FBSimctlTest { @Mock private lateinit var executor: IShellCommand @Mock private lateinit var parser: IFBSimctlResponseParser + private val fbsimctlResponse = """ + {"event_name":"log","timestamp":1533056871,"level":"info","subject":"Running \/usr\/bin\/xcode-select --print-path with environment {\n HOME = \"\/Users\/vfrolov\";\n}","event_type":"discrete"} + {"event_name":"list_device_sets","timestamp":1533056871,"subject":"\/a","event_type":"discrete"} + """.trimIndent() @Before fun setUp() { MockitoAnnotations.initMocks(this) } @Test fun mustTrimLastNewLine() { - whenever(executor.exec(anyList(), anyMap(), anyType(), anyBoolean(), any(), anyType())).thenReturn(CommandResult("/a\n", "", ByteArray(0), 0)) - val fbSimctl = FBSimctl(executor, parser) + whenever(executor.exec(anyList(), anyMap(), anyType(), anyBoolean(), any(), anyType())).thenReturn(CommandResult(fbsimctlResponse, "", ByteArray(0), 0)) + val fbSimctl = FBSimctl(executor, FBSimctlResponseParser()) val deviceSets = fbSimctl.defaultDeviceSet() Assert.assertEquals("/a", deviceSets) } @@ -30,6 +34,7 @@ class FBSimctlTest { @Test(expected = FBSimctlError::class) fun shouldThrowWhenNoDeviceSets() { whenever(executor.exec(anyList(), anyMap(), anyType(), anyBoolean(), any(), anyType())).thenReturn(CommandResult("\n", "", ByteArray(0), 0)) + whenever(parser.parseDeviceSets(anyString())).thenReturn(emptyList()) val fbSimctl = FBSimctl(executor, parser) fbSimctl.defaultDeviceSet() } diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FbSimctlResponseParserTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FbSimctlResponseParserTest.kt index d9629e75..f7a20450 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FbSimctlResponseParserTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FbSimctlResponseParserTest.kt @@ -41,10 +41,6 @@ class FbSimctlResponseParserTest { {"event_name":"create","timestamp":1521028581,"subject":{"pid":0,"arch":"x86_64","os":"iOS 11.2","container-pid":0,"model":"iPhone 6","udid":"7CA9DCE7-22A2-434B-A9EE-3E2A497E3881","name":"iPhone 6","state":"Shutdown"},"event_type":"ended"} """.trimIndent() - private val simulatorListDeviceHttp = """ - {"status":"success","subject":[{"event_name":"list","timestamp":1518060205,"subject":{"arch":"x86_64","state":"Booted","model":"iPhone 6","name":"iPhone 6","os":"iOS 11.0","udid":"5913965A-EF5A-488B-91E0-1FDFF93086FD"},"event_type":"discrete"}],"events":[]} - """.trimIndent() - @Test fun parseList() { val parsedValue = FBSimctlResponseParser().parse(simulatorsListString) assertEquals("Wrong element count", 3, parsedValue.size) @@ -77,13 +73,6 @@ class FbSimctlResponseParserTest { assertThat(fbSimctlDevice.udid, equalTo("1aa0a00a0a00aaa000a000a00a0a000a0000a000")) } - @Test fun parseDeviceListHttp() { - val fbSimctlDevice = FBSimctlResponseParser().parseDeviceListHttp(simulatorListDeviceHttp) - - assertThat(fbSimctlDevice, instanceOf(FBSimctlDevice::class.java)) - assertThat(fbSimctlDevice.udid, equalTo("5913965A-EF5A-488B-91E0-1FDFF93086FD")) - } - @Test fun parseAppsList() { val bumbleAppIndex = 3 val parsedValue = FBSimctlResponseParser().parseApplicationsList(listAppsString) From 2b59c9636991562151054a51750dc0ab2fdf2bc6 Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Fri, 3 Aug 2018 13:23:44 +0100 Subject: [PATCH 028/131] Clean up some test strings --- .../ios/fbsimctl/FbSimctlResponseParserTest.kt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FbSimctlResponseParserTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FbSimctlResponseParserTest.kt index f7a20450..752e353e 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FbSimctlResponseParserTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FbSimctlResponseParserTest.kt @@ -13,7 +13,7 @@ class FbSimctlResponseParserTest { """.trimIndent() private val listAppsString = """ - {"event_name":"list_apps","timestamp":1516247902,"subject":[{"data_container":null,"bundle":{"path":"\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/Applications\/MobileSlideShow.app","bundle_id":"com.apple.mobileslideshow","binary":{"name":"MobileSlideShow","path":"\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/Applications\/MobileSlideShow.app\/MobileSlideShow","architectures":["x86_64"]},"name":"MobileSlideShow"},"install_type":"system"},{"data_container":"\/Users\/}|{eka\/Library\/Developer\/CoreSimulator\/Devices\/4B740F75-D83E-4DBA-8BA1-1A82A68FA27E\/data\/Containers\/Data\/Application\/B6B0251A-282C-422B-B379-3D6A1052B620","bundle":{"path":"\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/Applications\/Maps.app","bundle_id":"com.apple.Maps","binary":{"name":"Maps","path":"\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/Applications\/Maps.app\/Maps","architectures":["x86_64"]},"name":"Maps"},"install_type":"system"},{"data_container":null,"bundle":{"path":"\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/Applications\/MobileSMS.app","bundle_id":"com.apple.MobileSMS","binary":{"name":"MobileSMS","path":"\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/Applications\/MobileSMS.app\/MobileSMS","architectures":["x86_64"]},"name":"MobileSMS"},"install_type":"system"},{"data_container":"\/Users\/}|{eka\/Library\/Developer\/CoreSimulator\/Devices\/4B740F75-D83E-4DBA-8BA1-1A82A68FA27E\/data\/Containers\/Data\/Application\/1F486790-36FA-4DA1-8BC4-EC347466C06A","bundle":{"path":"\/Users\/}|{eka\/Library\/Developer\/CoreSimulator\/Devices\/4B740F75-D83E-4DBA-8BA1-1A82A68FA27E\/data\/Containers\/Bundle\/Application\/DE199219-B5B3-4DAA-92E4-37A9476B4885\/Bumble.app","bundle_id":"com.moxco.bumble","binary":{"name":"Bumble","path":"\/Users\/}|{eka\/Library\/Developer\/CoreSimulator\/Devices\/4B740F75-D83E-4DBA-8BA1-1A82A68FA27E\/data\/Containers\/Bundle\/Application\/DE199219-B5B3-4DAA-92E4-37A9476B4885\/Bumble.app\/Bumble","architectures":["x86_64"]},"name":"Bumble"},"install_type":"user"},{"data_container":"\/Users\/}|{eka\/Library\/Developer\/CoreSimulator\/Devices\/4B740F75-D83E-4DBA-8BA1-1A82A68FA27E\/data\/Containers\/Data\/Application\/48CF1917-F972-464E-9EA1-EF9C4140FEBB","bundle":{"path":"\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/Applications\/MobileSafari.app","bundle_id":"com.apple.mobilesafari","binary":{"name":"MobileSafari","path":"\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/Applications\/MobileSafari.app\/MobileSafari","architectures":["x86_64"]},"name":"MobileSafari"},"install_type":"system"}],"event_type":"discrete"} + {"event_name":"list_apps","timestamp":1516247902,"subject":[{"data_container":null,"bundle":{"path":"\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/Applications\/MobileSlideShow.app","bundle_id":"com.apple.mobileslideshow","binary":{"name":"MobileSlideShow","path":"\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/Applications\/MobileSlideShow.app\/MobileSlideShow","architectures":["x86_64"]},"name":"MobileSlideShow"},"install_type":"system"},{"data_container":"\/Users\/user\/Library\/Developer\/CoreSimulator\/Devices\/4B740F75-D83E-4DBA-8BA1-1A82A68FA27E\/data\/Containers\/Data\/Application\/B6B0251A-282C-422B-B379-3D6A1052B620","bundle":{"path":"\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/Applications\/Maps.app","bundle_id":"com.apple.Maps","binary":{"name":"Maps","path":"\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/Applications\/Maps.app\/Maps","architectures":["x86_64"]},"name":"Maps"},"install_type":"system"},{"data_container":null,"bundle":{"path":"\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/Applications\/MobileSMS.app","bundle_id":"com.apple.MobileSMS","binary":{"name":"MobileSMS","path":"\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/Applications\/MobileSMS.app\/MobileSMS","architectures":["x86_64"]},"name":"MobileSMS"},"install_type":"system"},{"data_container":"\/Users\/user\/Library\/Developer\/CoreSimulator\/Devices\/4B740F75-D83E-4DBA-8BA1-1A82A68FA27E\/data\/Containers\/Data\/Application\/1F486790-36FA-4DA1-8BC4-EC347466C06A","bundle":{"path":"\/Users\/user\/Library\/Developer\/CoreSimulator\/Devices\/4B740F75-D83E-4DBA-8BA1-1A82A68FA27E\/data\/Containers\/Bundle\/Application\/DE199219-B5B3-4DAA-92E4-37A9476B4885\/Bumble.app","bundle_id":"com.moxco.bumble","binary":{"name":"Bumble","path":"\/Users\/user\/Library\/Developer\/CoreSimulator\/Devices\/4B740F75-D83E-4DBA-8BA1-1A82A68FA27E\/data\/Containers\/Bundle\/Application\/DE199219-B5B3-4DAA-92E4-37A9476B4885\/Bumble.app\/Bumble","architectures":["x86_64"]},"name":"Bumble"},"install_type":"user"},{"data_container":"\/Users\/user\/Library\/Developer\/CoreSimulator\/Devices\/4B740F75-D83E-4DBA-8BA1-1A82A68FA27E\/data\/Containers\/Data\/Application\/48CF1917-F972-464E-9EA1-EF9C4140FEBB","bundle":{"path":"\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/Applications\/MobileSafari.app","bundle_id":"com.apple.mobilesafari","binary":{"name":"MobileSafari","path":"\/Applications\/Xcode.app\/Contents\/Developer\/Platforms\/iPhoneOS.platform\/Developer\/Library\/CoreSimulator\/Profiles\/Runtimes\/iOS.simruntime\/Contents\/Resources\/RuntimeRoot\/Applications\/MobileSafari.app\/MobileSafari","architectures":["x86_64"]},"name":"MobileSafari"},"install_type":"system"}],"event_type":"discrete"} {"event_type":"started","subject":{},"timestamp":1516247902,"target":{"arch":"x86_64","state":"Booted","model":"iPhone 8","name":"iPhone 8","udid":"4B740F75-D83E-4DBA-8BA1-1A82A68FA27E","os":"iOS 11.2"},"event_name":"list_apps"} {"event_type":"ended","subject":{},"timestamp":1516247902,"target":{"arch":"x86_64","state":"Booted","model":"iPhone 8","name":"iPhone 8","udid":"4B740F75-D83E-4DBA-8BA1-1A82A68FA27E","os":"iOS 11.2"},"event_name":"list_apps"} """.trimIndent() @@ -22,15 +22,15 @@ class FbSimctlResponseParserTest { private val diagnoseRunningStrings = """ {"event_type":"started","subject":{"type":"all"},"timestamp":1516634884,"target":{"arch":"x86_64","state":"Booted","model":"iPhone 6","name":"iPhone 6","udid":"FA2EC53F-E71A-4BAF-8686-840813C5348F","os":"iOS 11.0"},"event_name":"diagnose"} {"event_type":"ended","subject":{"type":"all"},"timestamp":1516634884,"target":{"arch":"x86_64","state":"Booted","model":"iPhone 6","name":"iPhone 6","udid":"FA2EC53F-E71A-4BAF-8686-840813C5348F","os":"iOS 11.0"},"event_name":"diagnose"} - {"event_name":"diagnostic","timestamp":1516634884,"subject":{"short_name":"system_log","human_name":"System Log","file_type":"log","location":"\/Users\/}|{eka\/Library\/Logs\/CoreSimulator\/FA2EC53F-E71A-4BAF-8686-840813C5348F\/system.log"},"event_type":"discrete"} - {"event_name":"diagnostic","timestamp":1516634884,"subject":{"short_name":"coresimulator","human_name":"Core Simulator Log","file_type":"log","location":"\/Users\/}|{eka\/Library\/Logs\/CoreSimulator\/CoreSimulator.log"},"event_type":"discrete"} - {"event_name":"diagnostic","timestamp":1516634884,"subject":{"short_name":"launchd_bootstrap","human_name":"Launchd Bootstrap","file_type":"plist","location":"\/Users\/}|{eka\/Library\/Developer\/CoreSimulator\/Devices\/FA2EC53F-E71A-4BAF-8686-840813C5348F\/data\/var\/run\/launchd_bootstrap.plist"},"event_type":"discrete"} + {"event_name":"diagnostic","timestamp":1516634884,"subject":{"short_name":"system_log","human_name":"System Log","file_type":"log","location":"\/Users\/user\/Library\/Logs\/CoreSimulator\/FA2EC53F-E71A-4BAF-8686-840813C5348F\/system.log"},"event_type":"discrete"} + {"event_name":"diagnostic","timestamp":1516634884,"subject":{"short_name":"coresimulator","human_name":"Core Simulator Log","file_type":"log","location":"\/Users\/user\/Library\/Logs\/CoreSimulator\/CoreSimulator.log"},"event_type":"discrete"} + {"event_name":"diagnostic","timestamp":1516634884,"subject":{"short_name":"launchd_bootstrap","human_name":"Launchd Bootstrap","file_type":"plist","location":"\/Users\/user\/Library\/Developer\/CoreSimulator\/Devices\/FA2EC53F-E71A-4BAF-8686-840813C5348F\/data\/var\/run\/launchd_bootstrap.plist"},"event_type":"discrete"} """.trimIndent() private val diagnoseShutdownStrings = """ {"event_type":"started","subject":{"type":"all"},"timestamp":1516635037,"target":{"arch":"x86_64","state":"Shutdown","model":"iPhone X","name":"iPhone X","udid":"54BC5B1F-7144-450C-8459-D61C2206D1F4","os":"iOS 11.2"},"event_name":"diagnose"} {"event_type":"ended","subject":{"type":"all"},"timestamp":1516635037,"target":{"arch":"x86_64","state":"Shutdown","model":"iPhone X","name":"iPhone X","udid":"54BC5B1F-7144-450C-8459-D61C2206D1F4","os":"iOS 11.2"},"event_name":"diagnose"} - {"event_name":"diagnostic","timestamp":1516635037,"subject":{"short_name":"coresimulator","human_name":"Core Simulator Log","file_type":"log","location":"\/Users\/}|{eka\/Library\/Logs\/CoreSimulator\/CoreSimulator.log"},"event_type":"discrete"} + {"event_name":"diagnostic","timestamp":1516635037,"subject":{"short_name":"coresimulator","human_name":"Core Simulator Log","file_type":"log","location":"\/Users\/user\/Library\/Logs\/CoreSimulator\/CoreSimulator.log"},"event_type":"discrete"} """.trimIndent() private val simulatorCreateStrings = """ @@ -84,13 +84,13 @@ class FbSimctlResponseParserTest { } @Test fun diagnoseSyslogLocationRunning() { - val expectedSyslogLocation = "/Users/}|{eka/Library/Logs/CoreSimulator/FA2EC53F-E71A-4BAF-8686-840813C5348F/system.log" + val expectedSyslogLocation = "/Users/user/Library/Logs/CoreSimulator/FA2EC53F-E71A-4BAF-8686-840813C5348F/system.log" val parsedValue = FBSimctlResponseParser().parseDiagnosticInfo(diagnoseRunningStrings) assertEquals("Wrong syslog location", expectedSyslogLocation, parsedValue.sysLogLocation) } @Test fun diagnoseSyslogLocationShutdown() { - val expectedCoreSimulatorLogLocation = "/Users/}|{eka/Library/Logs/CoreSimulator/CoreSimulator.log" + val expectedCoreSimulatorLogLocation = "/Users/user/Library/Logs/CoreSimulator/CoreSimulator.log" val parsedValue = FBSimctlResponseParser().parseDiagnosticInfo(diagnoseShutdownStrings) assertNull("Syslog should be null", parsedValue.sysLogLocation) assertEquals("Wrong syslog location", expectedCoreSimulatorLogLocation, parsedValue.coreSimulatorLogLocation) From f5a75f982d0965776f9323822a03fc286a652138 Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Wed, 1 Aug 2018 11:59:43 +0100 Subject: [PATCH 029/131] Refactor Xcode version checks --- .../deviceserver/host/DevicesNode.kt | 4 ++-- .../host/management/ISimulatorHostChecker.kt | 4 ++-- .../host/management/XcodeVersion.kt | 18 +++++++++++++- .../host/management/XcodeVersionTest.kt | 24 +++++++++++++++++++ 4 files changed, 45 insertions(+), 5 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt index 0d202782..1bc8bb29 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt @@ -263,8 +263,8 @@ class DevicesNode( val xcodeOutput = remote.execIgnoringErrors(listOf("xcodebuild", "-version")) val xcodeVersion = XcodeVersion.fromXcodeBuildOutput(xcodeOutput.stdOut) - if (!(xcodeVersion.major == 9 && xcodeVersion.minor >= 2)) { - throw RuntimeException("Expecting Xcode 9.2 or higher up to 10, but it is $xcodeVersion") + if (xcodeVersion < XcodeVersion(9, 2)) { + throw RuntimeException("Expecting Xcode 9.2 or higher, but it is $xcodeVersion") } val expectedFbsimctlVersion = "HEAD-292a1bd" diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt index 3f36014f..637ddcbe 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt @@ -50,8 +50,8 @@ class SimulatorHostChecker( val xcodeOutput = remote.execIgnoringErrors(listOf("xcodebuild", "-version")) val xcodeVersion = XcodeVersion.fromXcodeBuildOutput(xcodeOutput.stdOut) - if (xcodeVersion.major != 9) { - throw RuntimeException("Expecting Xcode 9, but received $xcodeOutput") + if (xcodeVersion < XcodeVersion(9, 0)) { + throw RuntimeException("Expecting Xcode 9 or higher, but received $xcodeOutput") } // temp solution, prereq should be satisfied without having to switch anything diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersion.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersion.kt index b427f005..fcb1354a 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersion.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersion.kt @@ -1,6 +1,6 @@ package com.badoo.automation.deviceserver.host.management; -class XcodeVersion(val major: Int, val minor: Int) { +class XcodeVersion(val major: Int, val minor: Int):Comparable { companion object { fun fromXcodeBuildOutput(output: String): XcodeVersion { val regex = Regex("Xcode (\\d+)\\.(\\d+)(\\.(\\d+))?") @@ -14,5 +14,21 @@ class XcodeVersion(val major: Int, val minor: Int) { return XcodeVersion(major, minor) } ?: throw IllegalArgumentException("Could not parse Xcode version $versionLine") } + + private val COMPARATOR = + Comparator.comparingInt { it.major } + .thenComparingInt { it.minor } + } + + override fun equals(other: Any?): Boolean { + if (other == null || other !is XcodeVersion) { + return false + } + + return compareTo(other) == 0 + } + + override operator fun compareTo(other: XcodeVersion): Int { + return COMPARATOR.compare(this, other) } } diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersionTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersionTest.kt index 1b08b976..77c805cf 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersionTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersionTest.kt @@ -23,4 +23,28 @@ class XcodeVersionTest { assertEquals(9, version.major) assertEquals(2, version.minor) } + + @Test + fun shouldCompareToLess() { + val v1 = XcodeVersion(9, 1) + val v2 = XcodeVersion(9, 2) + + assert(v1 < v2) + } + + @Test + fun shouldCompareToEqual() { + val v1 = XcodeVersion(9, 1) + val v2 = XcodeVersion(9, 1) + + assert(v1 == v2) + } + + @Test + fun shouldCompareToGreater() { + val v1 = XcodeVersion(9, 1) + val v2 = XcodeVersion(9, 0) + + assert(v1 > v2) + } } From 12adc776b1ef5c38e7db99a2a70160083c2fc9db Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Mon, 6 Aug 2018 16:37:21 +0100 Subject: [PATCH 030/131] Use fbsimctl ec54965 and WebDriverAgent e92bce2 from facebook repo --- README.md | 2 +- device-server/scripts/build_fbsimctl.sh | 4 ++-- device-server/scripts/update_wda.sh | 2 +- .../deviceserver/host/management/ISimulatorHostChecker.kt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 3bc129d0..74ae54ad 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,7 @@ server_url = 'http://localhost:4567' provider = IosDeviceServerClient::DeviceProvider.new(server_url) -rv = provider.create(model: 'iPhone 6', os: 'iOS 11.0', headless: false) +rv = provider.create(model: 'iPhone 6', os: 'iOS 11.4', headless: false) remote_device = IosDeviceServerClient::RemoteDevice.new(server_url, rv['ref']) begin diff --git a/device-server/scripts/build_fbsimctl.sh b/device-server/scripts/build_fbsimctl.sh index 266973eb..e23dce90 100755 --- a/device-server/scripts/build_fbsimctl.sh +++ b/device-server/scripts/build_fbsimctl.sh @@ -5,8 +5,8 @@ set -u # We are using forked version of fbsimctl with some fixes, see link below -readonly REPOSITORY=https://github.com/NickAb/FBSimulatorControl.git -readonly REVISION=292a1bd # https://github.com/NickAb/FBSimulatorControl/commits/patched +readonly REPOSITORY=https://github.com/facebook/FBSimulatorControl.git +readonly REVISION=ec54965 # https://github.com/facebook/FBSimulatorControl/commits/master readonly VERSION_NAME=HEAD-${REVISION} readonly FBSIMCTL_BASE_PATH=/usr/local/Cellar/fbsimctl diff --git a/device-server/scripts/update_wda.sh b/device-server/scripts/update_wda.sh index 81902444..d575799a 100755 --- a/device-server/scripts/update_wda.sh +++ b/device-server/scripts/update_wda.sh @@ -7,7 +7,7 @@ readonly SIGNING_PATCH=${SIGNING_PATCH:-} readonly NO_DEVICE_BUILD=${NO_DEVICE_BUILD:-0} readonly REPOSITORY=https://github.com/facebook/WebDriverAgent.git -readonly REVISION=20246d3e +readonly REVISION=e92bce2 readonly DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" readonly WORKING_DIR=$(mktemp -d) readonly BASE_DEST=${DIR}/../../ios/facebook diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt index 637ddcbe..afad1f24 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt @@ -25,7 +25,7 @@ class SimulatorHostChecker( private val diskCleanupInterval: Duration = Duration.ofMinutes(15) ) : ISimulatorHostChecker { companion object { - private const val EXPECTED_FBSIMCTL = "HEAD-292a1bd" + private const val EXPECTED_FBSIMCTL = "HEAD-ec54965" } private val logger = LoggerFactory.getLogger(javaClass.simpleName) From b87ff127c5a64e793c8a930de1da9b425f5b0973 Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Thu, 16 Aug 2018 14:41:07 +0100 Subject: [PATCH 031/131] Use fbsimctl ec54965 for DevicesNode --- .../com/badoo/automation/deviceserver/host/DevicesNode.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt index 1bc8bb29..a700e4b0 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt @@ -267,7 +267,7 @@ class DevicesNode( throw RuntimeException("Expecting Xcode 9.2 or higher, but it is $xcodeVersion") } - val expectedFbsimctlVersion = "HEAD-292a1bd" + val expectedFbsimctlVersion = "HEAD-ec54965" // temp solution, prerequisites should be satisfied without having to switch anything val switchRes = remote.execIgnoringErrors( @@ -312,4 +312,4 @@ class DevicesNode( // single instance of server on node is implied, so we can kill all simulators and fbsimctl processes remote.execIgnoringErrors(listOf("pkill", "-9", "fbsimctl")) } -} \ No newline at end of file +} From c77f398fab06176eb42375fd174121673ee4ac93 Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Wed, 22 Aug 2018 10:46:01 +0100 Subject: [PATCH 032/131] Add more information on no nodes to create a device error --- device-server/build.gradle | 2 +- .../com/badoo/automation/deviceserver/DeviceServer.kt | 4 ++++ .../deviceserver/host/management/DeviceManager.kt | 9 ++++++++- .../host/management/ISimulatorHostChecker.kt | 2 +- .../deviceserver/host/management/NodeRegistrar.kt | 2 +- .../deviceserver/host/management/NodeRegistry.kt | 7 ++++++- .../deviceserver/host/management/NodeWrapper.kt | 4 ++++ .../deviceserver/host/management/XcodeVersion.kt | 8 ++++++-- .../host/management/errors/DeviceCreationException.kt | 3 +++ .../host/management/errors/NoAliveNodesException.kt | 2 +- .../host/management/errors/NoNodesRegisteredException.kt | 3 +++ 11 files changed, 38 insertions(+), 8 deletions(-) create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/errors/DeviceCreationException.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/errors/NoNodesRegisteredException.kt diff --git a/device-server/build.gradle b/device-server/build.gradle index b49816df..4a33c2ec 100644 --- a/device-server/build.gradle +++ b/device-server/build.gradle @@ -119,7 +119,7 @@ run { systemProperty 'wda.bundle.path', '../ios/facebook/simulators/WebDriverAgentRunner-Runner.app' systemProperty 'wda.device.bundle.path', '../ios/facebook/devices/WebDriverAgentRunner-Runner.app' systemProperty 'device.server.config.path', '' - systemProperty 'logback.configurationFile', 'logback.xml' + systemProperty 'logback.configurationFile', 'logback-test.xml' } test { diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt index 5ce60377..06afa896 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt @@ -7,6 +7,7 @@ import com.badoo.automation.deviceserver.data.DesiredCapabilities import com.badoo.automation.deviceserver.data.ErrorDto import com.badoo.automation.deviceserver.data.toDto import com.badoo.automation.deviceserver.host.management.DeviceManager +import com.badoo.automation.deviceserver.host.management.errors.DeviceCreationException import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException import com.badoo.automation.deviceserver.host.management.errors.NoAliveNodesException import com.badoo.automation.deviceserver.host.management.errors.OverCapacityException @@ -228,8 +229,11 @@ fun Application.module() { is DeviceNotFoundException -> HttpStatusCode.NotFound is NoAliveNodesException -> HttpStatusCode.TooManyRequests is OverCapacityException -> HttpStatusCode.TooManyRequests + is DeviceCreationException -> HttpStatusCode.ServiceUnavailable else -> HttpStatusCode.InternalServerError } + + logger.error(call.request.toString(), exception) call.respond(statusCode, hashMapOf( "error" to exception.toDto() diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt index 03a77199..19975a47 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt @@ -4,6 +4,7 @@ import com.badoo.automation.deviceserver.DeviceServerConfig import com.badoo.automation.deviceserver.data.* import com.badoo.automation.deviceserver.host.HostFactory import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException +import com.badoo.automation.deviceserver.host.management.errors.NoNodesRegisteredException import com.badoo.automation.deviceserver.host.management.util.AutoreleaseLooper import com.badoo.automation.deviceserver.ios.ActiveDevices import org.slf4j.LoggerFactory @@ -110,7 +111,13 @@ class DeviceManager( } override fun createDeviceAsync(desiredCaps: DesiredCapabilities, userId: String?): DeviceDTO { - return nodeRegistry.createDeviceAsync(desiredCaps, deviceTimeoutInSecs, userId) + try { + return nodeRegistry.createDeviceAsync(desiredCaps, deviceTimeoutInSecs, userId) + } catch(e: NoNodesRegisteredException) { + val erredNodes = autoRegistrar.nodeWrappers.filter { n -> n.lastError != null } + val errors = erredNodes.joinToString { n -> "${n.node.remoteAddress} -> ${n.lastError?.localizedMessage}" } + throw(NoNodesRegisteredException(e.message+"\n$errors")) + } } override fun deleteReleaseDevice(ref: DeviceRef, reason: String) { diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt index afad1f24..88d7cc28 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt @@ -51,7 +51,7 @@ class SimulatorHostChecker( val xcodeVersion = XcodeVersion.fromXcodeBuildOutput(xcodeOutput.stdOut) if (xcodeVersion < XcodeVersion(9, 0)) { - throw RuntimeException("Expecting Xcode 9 or higher, but received $xcodeOutput") + throw RuntimeException("Expecting Xcode 9 or higher, but received $xcodeVersion. $xcodeOutput") } // temp solution, prereq should be satisfied without having to switch anything diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt index 7a7fb980..2a7fdff9 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt @@ -18,7 +18,7 @@ class NodeRegistrar( private val logger = LoggerFactory.getLogger(javaClass.simpleName) private var autoRegisteringJob: Future<*>? = null - private val nodeWrappers: List = nodesConfig.map { + val nodeWrappers: List = nodesConfig.map { NodeWrapper(it, nodeFactory, nodeRegistry) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt index f03dfa85..c9df7f9d 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt @@ -5,6 +5,7 @@ import com.badoo.automation.deviceserver.data.DesiredCapabilities import com.badoo.automation.deviceserver.data.DeviceDTO import com.badoo.automation.deviceserver.host.ISimulatorsNode import com.badoo.automation.deviceserver.host.management.errors.NoAliveNodesException +import com.badoo.automation.deviceserver.host.management.errors.NoNodesRegisteredException import com.badoo.automation.deviceserver.ios.ActiveDevices import com.badoo.automation.deviceserver.ios.simulator.simulatorsThreadPool import kotlinx.coroutines.experimental.Job @@ -46,7 +47,7 @@ class NodeRegistry(val activeDevices: ActiveDevices = ActiveDevices()) { return nodeWrappers } - fun getAlive(): Set { + private fun getAlive(): Set { return nodeWrappers.filter { it.isAlive() }.toSet() } @@ -59,6 +60,10 @@ class NodeRegistry(val activeDevices: ActiveDevices = ActiveDevices()) { } fun createDeviceAsync(desiredCapabilities: DesiredCapabilities, deviceTimeout: Duration, userId: String?): DeviceDTO { + if (getAll().isEmpty()) { + throw NoNodesRegisteredException("No nodes are registered to create a device") + } + val node: ISimulatorsNode = getAlive() .map { wrapper -> wrapper.node } .maxBy { node -> node.capacityRemaining(desiredCapabilities) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeWrapper.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeWrapper.kt index f5301e43..f3ab8199 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeWrapper.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeWrapper.kt @@ -32,6 +32,8 @@ class NodeWrapper( private var healthCheckPeriodicTask: Future<*>? = null val node: ISimulatorsNode = hostFactory.getHostFromConfig(config) + var lastError: Exception? = null + fun isAlive(): Boolean = isStarted && node.isReachable() override fun toString(): String = "NodeWrapper for ${config.host}" @@ -53,8 +55,10 @@ class NodeWrapper( node.prepareNode() logger.info(logMarker, "Successfully started the node from config: $config") isStarted = true + lastError = null } catch (e: Exception) { logger.error(logMarker, "Failed to start the node from config: $config", e) + lastError = e } return isStarted } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersion.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersion.kt index fcb1354a..2b867bf3 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersion.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/XcodeVersion.kt @@ -1,6 +1,6 @@ -package com.badoo.automation.deviceserver.host.management; +package com.badoo.automation.deviceserver.host.management -class XcodeVersion(val major: Int, val minor: Int):Comparable { +data class XcodeVersion(val major: Int, val minor: Int):Comparable { companion object { fun fromXcodeBuildOutput(output: String): XcodeVersion { val regex = Regex("Xcode (\\d+)\\.(\\d+)(\\.(\\d+))?") @@ -31,4 +31,8 @@ class XcodeVersion(val major: Int, val minor: Int):Comparable { override operator fun compareTo(other: XcodeVersion): Int { return COMPARATOR.compare(this, other) } + + override fun toString(): String { + return "$major.$minor" + } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/errors/DeviceCreationException.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/errors/DeviceCreationException.kt new file mode 100644 index 00000000..29859800 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/errors/DeviceCreationException.kt @@ -0,0 +1,3 @@ +package com.badoo.automation.deviceserver.host.management.errors + +open class DeviceCreationException(message: String): RuntimeException(message) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/errors/NoAliveNodesException.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/errors/NoAliveNodesException.kt index fe8c45ab..84d93410 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/errors/NoAliveNodesException.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/errors/NoAliveNodesException.kt @@ -1,3 +1,3 @@ package com.badoo.automation.deviceserver.host.management.errors -class NoAliveNodesException(message: String): RuntimeException(message) \ No newline at end of file +class NoAliveNodesException(message: String): DeviceCreationException(message) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/errors/NoNodesRegisteredException.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/errors/NoNodesRegisteredException.kt new file mode 100644 index 00000000..9e6e1e5b --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/errors/NoNodesRegisteredException.kt @@ -0,0 +1,3 @@ +package com.badoo.automation.deviceserver.host.management.errors + +class NoNodesRegisteredException(message: String): DeviceCreationException(message) From 9f49fc48f232ff8a0985930df6e02f0e81b40881 Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Wed, 22 Aug 2018 11:27:08 +0100 Subject: [PATCH 033/131] Add available device types to device creation error --- .../host/management/RuntimeVersion.kt | 8 +++++- .../deviceserver/ios/fbsimctl/FBSimctl.kt | 18 +++++++++++++ .../ios/fbsimctl/SimulatorTypes.kt | 26 ++++++++++++++++++ .../host/management/RuntimeVersionTest.kt | 27 +++++++++++++++++++ 4 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/SimulatorTypes.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/management/RuntimeVersionTest.kt diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/RuntimeVersion.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/RuntimeVersion.kt index 16adc484..391bfe53 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/RuntimeVersion.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/RuntimeVersion.kt @@ -8,7 +8,13 @@ class RuntimeVersion(runtime: String) { private set init { - val (a, b) = runtime.split(' ', ignoreCase = false, limit = 2) + val parts = runtime.split(' ', ignoreCase = false, limit = 2) + + if (parts.size < 2) { + throw IllegalArgumentException("Invalid runtime '$runtime' requested. Runtime is expected to contain name and version specifier separated by space, for example, 'iOS 11'.") + } + + val (a, b) = parts name = a fragments = b.split('.').toList() } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt index df8149ae..cf53e8dc 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt @@ -68,9 +68,27 @@ class FBSimctl( if (model != null) { args.add("'$model'") } val result = fbsimctl(args) + + if (result.isEmpty()) { + // FIXME: We don't take available device types and runtimes in account when routing device creation request. + // We only take in account existing devices when matching node to desired capabilities. + // This means that if none of the nodes have at least one of desired model devices already, + // we will choose a random one to handle creation request which might be the one that can not create + // specific model, while other nodes could. + val suggestions = suggestCreationArgs() + throw(RuntimeException("Could not create simulator \"$model\" \"$os\"\n$suggestions")) + } return parser.parseDeviceCreation(result, transitional) } + private fun suggestCreationArgs(): String { + val types = SimulatorTypes(shellCommand) + val models = types.availableModels() + val osVersions = types.availableRuntimes() + + return "Available models:\n " + models.joinToString("\n ") + "\nAvailable os versions:\n " + osVersions.joinToString("\n ") + } + override fun diagnose(udid: UDID): FBSimctlDeviceDiagnosticInfo { return parser.parseDiagnosticInfo(fbsimctl(cmd = "diagnose", udid = udid)) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/SimulatorTypes.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/SimulatorTypes.kt new file mode 100644 index 00000000..8be0eed3 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/SimulatorTypes.kt @@ -0,0 +1,26 @@ +package com.badoo.automation.deviceserver.ios.fbsimctl + +import com.badoo.automation.deviceserver.JsonMapper +import com.badoo.automation.deviceserver.command.IShellCommand + +class SimulatorTypes(private val shellCommand: IShellCommand) { + private val mapper = JsonMapper() + + fun availableRuntimes(): List { + return fetch("runtimes") + } + + fun availableModels(): List { + return fetch("devicetypes") + } + + private fun fetch(type: String): List { + val rv = shellCommand.exec(listOf("xcrun", "simctl", "list", "--json", type)) + + if (!rv.isSuccess) { + return emptyList() + } + + return mapper.readTree(rv.stdOut.byteInputStream())[type].mapNotNull { it["name"].toString() }.toList() + } +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/management/RuntimeVersionTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/management/RuntimeVersionTest.kt new file mode 100644 index 00000000..21535903 --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/management/RuntimeVersionTest.kt @@ -0,0 +1,27 @@ +package com.badoo.automation.deviceserver.host.management + +import org.junit.Assert.* +import org.junit.Test + +class RuntimeVersionTest { + @Test + fun shouldParseValidRuntime() { + val rt = RuntimeVersion("iOS 11") + + assertEquals("iOS", rt.name) + assertEquals(listOf("11"), rt.fragments) + } + + @Test + fun shouldParseValidRuntimeWithFragments() { + val rt = RuntimeVersion("iOS 11.1") + + assertEquals("iOS", rt.name) + assertEquals(listOf("11", "1"), rt.fragments) + } + + @Test(expected = IllegalArgumentException::class) + fun shouldThrowWhenRuntimeFormatIsInvalid() { + RuntimeVersion("iOS11") + } +} From b4cac1d9a22ba469dc01a9986b5d82b92d1099d6 Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Wed, 22 Aug 2018 11:27:48 +0100 Subject: [PATCH 034/131] Filter logged stacktrace in logback-test config --- device-server/src/main/resources/logback-test.xml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/device-server/src/main/resources/logback-test.xml b/device-server/src/main/resources/logback-test.xml index 85cf0f3a..70373c6b 100644 --- a/device-server/src/main/resources/logback-test.xml +++ b/device-server/src/main/resources/logback-test.xml @@ -2,7 +2,13 @@ - %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %logger{36} - %msg%n + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %logger{36} - %msg%n%rEx{full, + kotlinx, + io.ktor, + io.netty + } + From 77e91da0ae4d4ffdb35926545717b12fe639d81e Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Mon, 20 Aug 2018 19:16:34 +0100 Subject: [PATCH 035/131] Grant permissions to microphone by default for simulator --- .../com/badoo/automation/deviceserver/ios/simulator/Simulator.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt index a0dca0d7..d66f579d 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt @@ -465,6 +465,7 @@ class Simulator ( //region approveAccess override fun approveAccess(bundleId: String) { updatePermission(bundleId, "kTCCServiceCamera") + updatePermission(bundleId, "kTCCServiceMicrophone") updatePermission(bundleId, "kTCCServicePhotos") updatePermission(bundleId, "kTCCServiceAddressBook") } From 48964440a6887b136bedcd0a32ae904ab75e6136 Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Mon, 20 Aug 2018 19:15:57 +0100 Subject: [PATCH 036/131] Fix Safari clear cookies for iOS 12 --- .../deviceserver/ios/simulator/Simulator.kt | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt index a0dca0d7..8c9ed00e 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt @@ -524,18 +524,29 @@ class Simulator ( /** * [see Deleting-Safari-Cookies-in-iOS-Simulator.html](http://www.ryanchapin.com/fv-b-4-744/Deleting-Safari-Cookies-in-iOS-Simulator.html) */ + private val cookieJars = listOf( + "Cookies.binarycookies", // pre iOS 12 + "com.apple.SafariViewService.binarycookies" // iOS 12 + ) + override fun clearSafariCookies(): Map { val apps = remote.fbsimctl.listApps(udid) check(!apps.isEmpty()) { "Could not list apps for $this" } val safari = apps.find { SAFARI_BUNDLE_ID == it.bundle.bundle_id } - checkNotNull(safari) { "$SAFARI_BUNDLE_ID not found in $apps for $this" } - val cookiesPath = File(safari!!.data_container, listOf("Library", "Cookies", "Cookies.binarycookies").joinToString(File.separator)) + if (safari == null) { + throw IllegalStateException("$SAFARI_BUNDLE_ID not found in $apps for $this") + } - val result = remote.execIgnoringErrors(listOf("rm", "-f", cookiesPath.absolutePath)) - check(result.isSuccess) { "Failed to remove safari cookies (${cookiesPath.absolutePath} on $remote for $this: $result" } + val cookieJarPaths = cookieJars.map { cookieJar -> + File(safari.data_container, listOf("Library", "Cookies", cookieJar).joinToString(File.separator)).absolutePath + } + val cmd = mutableListOf("rm", "-f") + cmd.addAll(cookieJarPaths) + val result = remote.execIgnoringErrors(cmd) + check(result.isSuccess) { "Failed to remove safari cookies ($cookieJarPaths on $remote for $this: $result" } return mapOf("status" to "true") } From c70c331dcaf44ca057a70813d03aa80af8fd2350 Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Fri, 24 Aug 2018 06:03:57 +0200 Subject: [PATCH 037/131] Use plain text logs for WebDriver and fbsimctl --- .../automation/deviceserver/ios/device/DeviceFbsimctlProc.kt | 1 - .../com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt | 1 - .../badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt | 1 - .../automation/deviceserver/ios/proc/DeviceFbsimctlProcTest.kt | 1 - .../badoo/automation/deviceserver/ios/proc/FbsimctlProcTest.kt | 2 -- 5 files changed, 6 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceFbsimctlProc.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceFbsimctlProc.kt index c9283826..ce137ac9 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceFbsimctlProc.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceFbsimctlProc.kt @@ -25,7 +25,6 @@ class DeviceFbsimctlProc( return listOf( FBSimctl.FBSIMCTL_BIN, - FBSimctl.RESPONSE_FORMAT, udid, "listen", "--http", diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt index 4d40d2f4..a8441c13 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt @@ -60,7 +60,6 @@ open class FbsimctlProc( protected open fun getFbsimctlCommand(headless: Boolean): List { val cmd = mutableListOf( FBSimctl.FBSIMCTL_BIN, - FBSimctl.RESPONSE_FORMAT, udid, "boot" ) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt index 12b89526..3e7a6304 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt @@ -26,7 +26,6 @@ open class WebDriverAgent( ) : LongRunningProc(udid, remote.hostName) { private val launchXctestCommand: List = listOf( FBSimctl.FBSIMCTL_BIN, - FBSimctl.RESPONSE_FORMAT, udid, "launch_xctest", wdaPath, diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/DeviceFbsimctlProcTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/DeviceFbsimctlProcTest.kt index 503f2869..64d5477d 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/DeviceFbsimctlProcTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/DeviceFbsimctlProcTest.kt @@ -41,7 +41,6 @@ class DeviceFbsimctlProcTest { ).start() val expectedCommand = listOf( "/usr/local/bin/fbsimctl", - "--json", "UDID", "listen", "--http", diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProcTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProcTest.kt index 73a17e43..ecd8b6ec 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProcTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProcTest.kt @@ -32,7 +32,6 @@ class FbsimctlProcTest { FbsimctlProc(remote, udid, endpoint, headless, this::childFactory).start() val expectedCommand = listOf( "/usr/local/bin/fbsimctl", - "--json", "UDID", "boot", "--direct-launch", @@ -50,7 +49,6 @@ class FbsimctlProcTest { FbsimctlProc(remote, udid, endpoint, headless, this::childFactory).start() val expectedCommand = listOf( "/usr/local/bin/fbsimctl", - "--json", "UDID", "boot", "--", From a94f41c439cb21d7214935a6980c2b324fa3a7d2 Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Thu, 30 Aug 2018 16:55:21 +0200 Subject: [PATCH 038/131] Environment variable for Simulator's WDA_RUNNER to jar_launcher.sh --- device-server/jar_launcher.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/device-server/jar_launcher.sh b/device-server/jar_launcher.sh index 568512cc..bd93cdce 100755 --- a/device-server/jar_launcher.sh +++ b/device-server/jar_launcher.sh @@ -5,7 +5,7 @@ set -xe declare -r DEVICE_SERVER_CONFIG_PATH="${DEVICE_SERVER_CONFIG_PATH}" declare -r DEVICE_SERVER_JAR="${DEVICE_SERVER_JAR}" -declare -r WDA_RUNNER='../ios/facebook/simulators/WebDriverAgentRunner-Runner.app' +declare -r WDA_RUNNER=${DEVICE_SERVER_WDA_SIMULATOR_RUNNER:-'../ios/facebook/simulators/WebDriverAgentRunner-Runner.app'} declare -r WDA_DEVICE_RUNNER=${DEVICE_SERVER_WDA_DEVICE_RUNNER:-'../ios/facebook/devices/WebDriverAgentRunner-Runner.app'} declare -r LOG_CONFIG='logback-test.xml' From dace5de7c391bc6d81f03ecbc5727675be204540 Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Fri, 31 Aug 2018 12:52:28 +0100 Subject: [PATCH 039/131] Fix escaping for fbsimctl create call --- .../badoo/automation/deviceserver/command/IShellCommand.kt | 6 ++++++ .../automation/deviceserver/command/RemoteShellCommand.kt | 4 ++++ .../badoo/automation/deviceserver/command/ShellCommand.kt | 4 ++++ .../badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt | 5 +++-- 4 files changed, 17 insertions(+), 2 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/IShellCommand.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/IShellCommand.kt index cd19630b..43d1785e 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/IShellCommand.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/IShellCommand.kt @@ -30,4 +30,10 @@ interface IShellCommand { logMarker: Marker? = null, processListener: LongRunningProcessListener ) + + /** + * Escape exec argument string if needed + */ + fun escape(value: String): String // FIXME: this is temp workaround. Need to embed escaping into exec itself + } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt index 742fc5be..05c85192 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt @@ -85,6 +85,10 @@ class RemoteShellCommand( super.startProcess(getCommandWithSSHPrefix(command), getEnvironmentForSSH(), logMarker, processListener) } + override fun escape(value: String): String { + return ShellUtils.escape(value) + } + private fun getEnvironmentForSSH(): HashMap { val envWithSsh = HashMap(sshEnv) envWithSsh.putAll(envWithSsh) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommand.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommand.kt index eb0bdda0..2f10bc63 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommand.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommand.kt @@ -63,6 +63,10 @@ open class ShellCommand( startProcessInternal(command, environment, processListener) } + override fun escape(value: String): String { + return value + } + private fun startProcessInternal(command: List, environment: Map, processListener: NuProcessHandler): NuProcess { logger.info(logMarker, "Executing command: ${command.joinToString(" ")}") val cmdEnv = environment + commonEnvironment diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt index cf53e8dc..1210c53d 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt @@ -64,8 +64,9 @@ class FBSimctl( override fun create(model: String?, os: String?, transitional: Boolean): FBSimctlDevice { val args = mutableListOf("create") - if (os != null) { args.add("'$os'") } - if (model != null) { args.add("'$model'") } + // FIXME: escaping should be part of exec implementation and hidden from caller. Fix in separate ticket. + if (os != null) { args.add(shellCommand.escape(os)) } + if (model != null) { args.add(shellCommand.escape(model)) } val result = fbsimctl(args) From 66f6ed002003041bdb928e22c1a26edd2a367930 Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Tue, 11 Sep 2018 15:33:46 +0100 Subject: [PATCH 040/131] Allow using custom fbsimctl. Refactor configuration. --- device-server/jar_launcher.sh | 2 + .../deviceserver/ApplicationConfiguration.kt | 19 ++++++ .../automation/deviceserver/Configuration.kt | 18 ------ .../automation/deviceserver/DeviceServer.kt | 15 +++-- .../host/DefaultSimulatorHostProvider.kt | 9 --- .../deviceserver/host/DevicesNode.kt | 21 ++++--- .../deviceserver/host/HostFactory.kt | 61 +++++++++--------- .../deviceserver/host/ISimulatorFactory.kt | 5 +- .../host/ISimulatorHostProvider.kt | 12 ---- .../deviceserver/host/SimulatorsNode.kt | 7 +-- .../host/management/DeviceManager.kt | 3 +- .../host/management/ISimulatorHostChecker.kt | 24 +++---- .../deviceserver/ios/device/Device.kt | 6 +- .../deviceserver/ios/device/DeviceSlots.kt | 5 +- .../ios/device/DeviceWebDriverAgent.kt | 8 +-- .../ios/proc/SimulatorWebDriverAgent.kt | 10 +-- .../deviceserver/ios/proc/WebDriverAgent.kt | 7 ++- .../deviceserver/ios/simulator/Simulator.kt | 4 +- .../deviceserver/host/SimulatorsNodeTest.kt | 2 +- .../deviceserver/ios/HostFactoryTest.kt | 62 ------------------- 20 files changed, 117 insertions(+), 183 deletions(-) create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt delete mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/Configuration.kt delete mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DefaultSimulatorHostProvider.kt delete mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorHostProvider.kt delete mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/HostFactoryTest.kt diff --git a/device-server/jar_launcher.sh b/device-server/jar_launcher.sh index bd93cdce..063749d7 100755 --- a/device-server/jar_launcher.sh +++ b/device-server/jar_launcher.sh @@ -7,6 +7,7 @@ declare -r DEVICE_SERVER_JAR="${DEVICE_SERVER_JAR}" declare -r WDA_RUNNER=${DEVICE_SERVER_WDA_SIMULATOR_RUNNER:-'../ios/facebook/simulators/WebDriverAgentRunner-Runner.app'} declare -r WDA_DEVICE_RUNNER=${DEVICE_SERVER_WDA_DEVICE_RUNNER:-'../ios/facebook/devices/WebDriverAgentRunner-Runner.app'} +declare -r FBSIMCTL_VERSION=${DEVICE_SERVER_FBSIMCTL_VERSION:-'HEAD-d30c2a73'} declare -r LOG_CONFIG='logback-test.xml' export JAVA_HOME=$(/usr/libexec/java_home -v 10 -F || /usr/libexec/java_home -v 9 -F) @@ -20,4 +21,5 @@ exec java \ -Ddevice.server.config.path=${DEVICE_SERVER_CONFIG_PATH} \ -Dwda.bundle.path=${WDA_RUNNER} \ -Dwda.device.bundle.path=${WDA_DEVICE_RUNNER} \ + -Dfbsimctl.version=${FBSIMCTL_VERSION} \ -jar ${DEVICE_SERVER_JAR} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt new file mode 100644 index 00000000..32e06c3d --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt @@ -0,0 +1,19 @@ +package com.badoo.automation.deviceserver + +class ApplicationConfiguration { + private val wdaSimulatorBundlePathProperty = "wda.bundle.path" + val wdaSimulatorBundlePath: String = System.getProperty(wdaSimulatorBundlePathProperty) + ?: throw RuntimeException("Must set system property: -D$wdaSimulatorBundlePathProperty=" + + "/ABSOLUTE/PATH/ios/facebook/simulators/WebDriverAgentRunner-Runner.app") + + private val wdaDeviceBundlePathProperty = "wda.device.bundle.path" + val wdaDeviceBundlePath: String = System.getProperty(wdaDeviceBundlePathProperty) + ?: throw RuntimeException("Must set system property: -D$wdaDeviceBundlePathProperty=" + + "/ABSOLUTE/PATH/ios/facebook/devices/WebDriverAgentRunner-Runner.app") + + private val deviceServerConfigPathProperty = "device.server.config.path" + val deviceServerConfigPath: String = System.getProperty(deviceServerConfigPathProperty) + ?: throw RuntimeException("Must set system property: -D$deviceServerConfigPathProperty=./config/.device_config") + + val fbsimctlVersion: String = System.getProperty("fbsimctl.version", "HEAD-d30c2a73") +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/Configuration.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/Configuration.kt deleted file mode 100644 index bfacab92..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/Configuration.kt +++ /dev/null @@ -1,18 +0,0 @@ -package com.badoo.automation.deviceserver - -object Configuration { - - private const val wda_bundle_path_property = "wda.bundle.path" - private const val wda_device_bundle_path_property = "wda.device.bundle.path" - val WDA_BUNDLE_PATH: String = System.getProperty(wda_bundle_path_property) - ?: throw RuntimeException("Must set system property: -D$wda_bundle_path_property=" + - "/ABSOLUTE/PATH/ios/facebook/simulators/WebDriverAgentRunner-Runner.app") - - val WDA_DEVICE_BUNDLE_PATH: String = System.getProperty(wda_device_bundle_path_property) - ?: throw RuntimeException("Must set system property: -D$wda_device_bundle_path_property=" + - "/ABSOLUTE/PATH/ios/facebook/devices/WebDriverAgentRunner-Runner.app") - - private const val device_server_config_path = "device.server.config.path" - val DEVICE_SERVER_CONFIG_PATH: String = System.getProperty(device_server_config_path) - ?: throw RuntimeException("Must set system property: -D$device_server_config_path=./config/.device_config") -} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt index 06afa896..7aad4a02 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt @@ -6,6 +6,7 @@ import com.badoo.automation.deviceserver.data.DataPath import com.badoo.automation.deviceserver.data.DesiredCapabilities import com.badoo.automation.deviceserver.data.ErrorDto import com.badoo.automation.deviceserver.data.toDto +import com.badoo.automation.deviceserver.host.HostFactory import com.badoo.automation.deviceserver.host.management.DeviceManager import com.badoo.automation.deviceserver.host.management.errors.DeviceCreationException import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException @@ -72,13 +73,15 @@ fun getAddresses(): List { } } +private val appConfiguration = ApplicationConfiguration() + private fun serverConfig(): DeviceServerConfig { - if (Configuration.DEVICE_SERVER_CONFIG_PATH.isEmpty()) { + if (appConfiguration.deviceServerConfigPath.isEmpty()) { logger.info("Using default config") return DeviceServerConfig(nodes = listOf(NodeConfig()), timeouts = emptyMap()) } - val configFile = File(Configuration.DEVICE_SERVER_CONFIG_PATH) + val configFile = File(appConfiguration.deviceServerConfigPath) if (!configFile.exists()) { val msg = "Config file ${configFile.path} not found" @@ -97,8 +100,12 @@ private val logger = LoggerFactory.getLogger(DevicesController::class.java.simpl @Suppress("unused") fun Application.module() { val config = serverConfig() - - val deviceManager = DeviceManager(config) + val hostFactory = HostFactory( + wdaSimulatorBundle = File(appConfiguration.wdaSimulatorBundlePath).canonicalFile, + wdaDeviceBundle = File(appConfiguration.wdaDeviceBundlePath).canonicalFile, + fbsimctlVersion = appConfiguration.fbsimctlVersion + ) + val deviceManager = DeviceManager(config, hostFactory) deviceManager.startAutoRegisteringDevices() deviceManager.launchAutoReleaseLoop() diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DefaultSimulatorHostProvider.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DefaultSimulatorHostProvider.kt deleted file mode 100644 index 86e32e07..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DefaultSimulatorHostProvider.kt +++ /dev/null @@ -1,9 +0,0 @@ -package com.badoo.automation.deviceserver.host - -import java.io.File - -object DefaultSimulatorHostProvider : ISimulatorHostProvider { - override fun simulatorsNode(remote: IRemote, simulatorLimit: Int, concurrentBoots: Int, wdaPath: File): ISimulatorsNode { - return SimulatorsNode(remote, simulatorLimit = simulatorLimit, concurrentBoots = concurrentBoots, wdaPath = wdaPath) - } -} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt index a700e4b0..3e9ef09b 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt @@ -20,10 +20,13 @@ import java.util.concurrent.TimeUnit class DevicesNode( private val remote: IRemote, portAllocator: PortAllocator = PortAllocator(), - wdaPath: File, + wdaRunnerXctest: File, knownDevices: List, private val whitelistedApps: Set, - private val uninstallApps: Boolean + private val uninstallApps: Boolean, + private val wdaBundlePath: File, + private val remoteWdaBundleRoot: File, + private val fbsimctlVersion: String ) : ISimulatorsNode { private val logger = LoggerFactory.getLogger(javaClass.simpleName) private val logMarker = MapEntriesAppendingMarker( @@ -37,7 +40,7 @@ class DevicesNode( private val deviceInfoProvider = DeviceInfoProvider(remote) private val slots: DeviceSlots = - DeviceSlots(remote, wdaPath.absolutePath, portAllocator, deviceInfoProvider, knownDevices) + DeviceSlots(remote, wdaRunnerXctest, portAllocator, deviceInfoProvider, knownDevices) private var deviceRegistrar: Future? = null @@ -267,11 +270,9 @@ class DevicesNode( throw RuntimeException("Expecting Xcode 9.2 or higher, but it is $xcodeVersion") } - val expectedFbsimctlVersion = "HEAD-ec54965" - // temp solution, prerequisites should be satisfied without having to switch anything val switchRes = remote.execIgnoringErrors( - listOf("/usr/local/bin/brew", "switch", "fbsimctl", expectedFbsimctlVersion), + listOf("/usr/local/bin/brew", "switch", "fbsimctl", fbsimctlVersion), env = mapOf("RUBYOPT" to "") ) @@ -284,8 +285,8 @@ class DevicesNode( val match = Regex("/fbsimctl/([-.\\w]+)/bin/fbsimctl").find(fbsimctlPath) ?: throw RuntimeException("Could not read fbsimctl version from $fbsimctlPath") val actualFbsimctlVersion = match.groupValues[1] - if (actualFbsimctlVersion != expectedFbsimctlVersion) { - throw RuntimeException("Expecting fbsimctl $expectedFbsimctlVersion, but it was $actualFbsimctlVersion ${match.groupValues}") + if (actualFbsimctlVersion != fbsimctlVersion) { + throw RuntimeException("Expecting fbsimctl $fbsimctlVersion, but it was $actualFbsimctlVersion ${match.groupValues}") } val iproxy = remote.execIgnoringErrors((listOf(UsbProxy.IPROXY_BIN))) @@ -302,8 +303,8 @@ class DevicesNode( private fun copyWdaBundleToHost() { logger.debug(logMarker, "Setting up remote node: copying WebDriverAgent to node ${remote.hostName}") remote.rsync( - HostFactory.WDA_DEVICE_BUNDLE.absolutePath, - HostFactory.REMOTE_WDA_DEVICE_BUNDLE_ROOT, + wdaBundlePath.absolutePath, + remoteWdaBundleRoot.absolutePath, setOf("-r", "--delete") ) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt index 85399ad6..af0953a1 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt @@ -1,21 +1,21 @@ package com.badoo.automation.deviceserver.host -import com.badoo.automation.deviceserver.Configuration import com.badoo.automation.deviceserver.NodeConfig import com.badoo.automation.deviceserver.host.management.IHostFactory +import com.badoo.automation.deviceserver.host.management.SimulatorHostChecker import org.slf4j.LoggerFactory import java.io.File class HostFactory( - private val remoteProvider: (hostName: String, userName: String, publicHost: String) -> IRemote = { hostName, userName, publicHostName -> Remote(hostName, userName, publicHostName) }, - private val simulatorHostProvider: ISimulatorHostProvider = DefaultSimulatorHostProvider + private val remoteProvider: (hostName: String, userName: String, publicHost: String) -> IRemote = { hostName, userName, publicHostName -> Remote(hostName, userName, publicHostName) }, + private val wdaSimulatorBundle: File, + private val wdaDeviceBundle: File, + private val fbsimctlVersion: String ) : IHostFactory { companion object { - val WDA_BUNDLE = File(Configuration.WDA_BUNDLE_PATH).canonicalFile!! // can't be null. Configuration will blow up otherwise - val WDA_DEVICE_BUNDLE = File(Configuration.WDA_DEVICE_BUNDLE_PATH).canonicalFile!! val WDA_XCTEST = File("PlugIns/WebDriverAgentRunner.xctest") - const val REMOTE_WDA_BUNDLE_ROOT = "/tmp/web_driver_agent/" - const val REMOTE_WDA_DEVICE_BUNDLE_ROOT = "/tmp/web_driver_agent_devices/" + private val REMOTE_WDA_BUNDLE_ROOT = File("/tmp/web_driver_agent/") + private val REMOTE_WDA_DEVICE_BUNDLE_ROOT = File("/tmp/web_driver_agent_devices/") } private val logger = LoggerFactory.getLogger(javaClass.simpleName) @@ -32,39 +32,42 @@ class HostFactory( throw RuntimeException("Config for non-localhost nodes must have non-empty 'user'. Current config: $config") } - if (config.type == NodeConfig.NodeType.Simulators) { - return simulatorHostProvider.simulatorsNode( - remote, - config.simulatorLimit, - config.concurrentBoots, - getWdaSimulatorsPath(remote.isLocalhost()) + return if (config.type == NodeConfig.NodeType.Simulators) { + SimulatorsNode( + remote = remote, + hostChecker = SimulatorHostChecker( + remote, + wdaBundle = wdaSimulatorBundle, + remoteWdaBundleRoot = REMOTE_WDA_BUNDLE_ROOT, + fbsimctlVersion = fbsimctlVersion + ), + simulatorLimit = config.simulatorLimit, + concurrentBoots = config.concurrentBoots, + wdaRunnerXctest = getWdaRunnerXctest(remote.isLocalhost(), wdaSimulatorBundle, REMOTE_WDA_BUNDLE_ROOT) ) } else { - return DevicesNode( + DevicesNode( remote, whitelistedApps = config.whitelistApps, - wdaPath = getWdaDevicesPath(remote.isLocalhost()), knownDevices = config.knownDevices, - uninstallApps = config.uninstallApps + uninstallApps = config.uninstallApps, + wdaBundlePath = wdaDeviceBundle, + remoteWdaBundleRoot = REMOTE_WDA_DEVICE_BUNDLE_ROOT, + wdaRunnerXctest = getWdaRunnerXctest(remote.isLocalhost(), wdaDeviceBundle, REMOTE_WDA_DEVICE_BUNDLE_ROOT), + fbsimctlVersion = fbsimctlVersion ) } } - private fun getWdaSimulatorsPath(isLocalhost: Boolean): File { - return if (isLocalhost) { - File(WDA_BUNDLE, WDA_XCTEST.path) - } else { - val xcTestPath = File(WDA_BUNDLE.name, WDA_XCTEST.path).path - File(REMOTE_WDA_BUNDLE_ROOT, xcTestPath) - } - } + private fun getWdaRunnerXctest(isLocalHost: Boolean, wdaBundle: File, remoteWdaBundleRoot: File): File { + val wdaRunnerXctest = File(wdaBundle.name, WDA_XCTEST.path).path - private fun getWdaDevicesPath(isLocalhost: Boolean): File { - return if (isLocalhost) { - File(WDA_DEVICE_BUNDLE, WDA_XCTEST.path) + val wdaBundleRoot = if (isLocalHost) { + wdaBundle.parentFile } else { - val xcTestPath = File(WDA_DEVICE_BUNDLE.name, WDA_XCTEST.path).path - File(REMOTE_WDA_DEVICE_BUNDLE_ROOT, xcTestPath) + remoteWdaBundleRoot } + + return File(wdaBundleRoot, wdaRunnerXctest) } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorFactory.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorFactory.kt index f0597280..522e17ef 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorFactory.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorFactory.kt @@ -7,6 +7,7 @@ import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDevice import com.badoo.automation.deviceserver.ios.simulator.ISimulator import com.badoo.automation.deviceserver.ios.simulator.Simulator import kotlinx.coroutines.experimental.ThreadPoolDispatcher +import java.io.File interface ISimulatorFactory { fun newSimulator( @@ -15,11 +16,11 @@ interface ISimulatorFactory { fbdev: FBSimctlDevice, ports: DeviceAllocatedPorts, deviceSetPath: String, - wdaPath: String, + wdaRunnerXctest: File, concurrentBoot: ThreadPoolDispatcher, headless: Boolean, fbsimctlSubject: String ): ISimulator { - return Simulator(ref, remote, DeviceInfo(fbdev), ports, deviceSetPath, wdaPath, concurrentBoot, headless, fbsimctlSubject) + return Simulator(ref, remote, DeviceInfo(fbdev), ports, deviceSetPath, wdaRunnerXctest, concurrentBoot, headless, fbsimctlSubject) } } \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorHostProvider.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorHostProvider.kt deleted file mode 100644 index bb132697..00000000 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorHostProvider.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.badoo.automation.deviceserver.host - -import java.io.File - -interface ISimulatorHostProvider { - fun simulatorsNode( - remote: IRemote, - simulatorLimit: Int, - concurrentBoots: Int, - wdaPath: File - ): ISimulatorsNode -} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt index 89622217..025ae8ee 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt @@ -6,7 +6,6 @@ import com.badoo.automation.deviceserver.LogMarkers.Companion.UDID import com.badoo.automation.deviceserver.data.* import com.badoo.automation.deviceserver.host.management.ISimulatorHostChecker import com.badoo.automation.deviceserver.host.management.PortAllocator -import com.badoo.automation.deviceserver.host.management.SimulatorHostChecker import com.badoo.automation.deviceserver.host.management.errors.OverCapacityException import com.badoo.automation.deviceserver.ios.simulator.ISimulator import com.badoo.automation.deviceserver.ios.simulator.simulatorsThreadPool @@ -21,10 +20,10 @@ import java.util.concurrent.ConcurrentHashMap class SimulatorsNode( val remote: IRemote, - private val hostChecker: ISimulatorHostChecker = SimulatorHostChecker(remote), + private val hostChecker: ISimulatorHostChecker, private val simulatorLimit: Int, concurrentBoots: Int, - private val wdaPath: File, + private val wdaRunnerXctest: File, private val simulatorProvider: ISimulatorProvider = SimulatorProvider(remote), private val portAllocator: PortAllocator = PortAllocator(), private val simulatorFactory: ISimulatorFactory = object : ISimulatorFactory {} @@ -88,7 +87,7 @@ class SimulatorsNode( logger.debug(simLogMarker, "Will create simulator $ref") - val simulator = simulatorFactory.newSimulator(ref, remote, fbSimctlDevice, ports, deviceSetPath, wdaPath.path, concurrentBoot, desiredCaps.headless, fbSimctlDevice.toString()) + val simulator = simulatorFactory.newSimulator(ref, remote, fbSimctlDevice, ports, deviceSetPath, wdaRunnerXctest, concurrentBoot, desiredCaps.headless, fbSimctlDevice.toString()) simulator.prepareAsync() devicePool[ref] = simulator diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt index 19975a47..a13e8aa6 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt @@ -2,7 +2,6 @@ package com.badoo.automation.deviceserver.host.management import com.badoo.automation.deviceserver.DeviceServerConfig import com.badoo.automation.deviceserver.data.* -import com.badoo.automation.deviceserver.host.HostFactory import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException import com.badoo.automation.deviceserver.host.management.errors.NoNodesRegisteredException import com.badoo.automation.deviceserver.host.management.util.AutoreleaseLooper @@ -15,7 +14,7 @@ private val INFINITE_DEVICE_TIMEOUT: Duration = Duration.ofSeconds(Integer.MAX_V class DeviceManager( config: DeviceServerConfig, - nodeFactory: IHostFactory = HostFactory(), + nodeFactory: IHostFactory, activeDevices: ActiveDevices = ActiveDevices(), private val autoreleaseLooper: IAutoreleaseLooper = AutoreleaseLooper() ) : IDeviceManager { diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt index 88d7cc28..857acf2a 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt @@ -1,13 +1,12 @@ package com.badoo.automation.deviceserver.host.management import com.badoo.automation.deviceserver.LogMarkers -import com.badoo.automation.deviceserver.host.HostFactory.Companion.REMOTE_WDA_BUNDLE_ROOT -import com.badoo.automation.deviceserver.host.HostFactory.Companion.WDA_BUNDLE import com.badoo.automation.deviceserver.host.IRemote import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl import com.badoo.automation.deviceserver.ios.simulator.periodicTasksPool import net.logstash.logback.marker.MapEntriesAppendingMarker import org.slf4j.LoggerFactory +import java.io.File import java.time.Duration import java.util.concurrent.ScheduledFuture import java.util.concurrent.TimeUnit @@ -22,12 +21,11 @@ interface ISimulatorHostChecker { class SimulatorHostChecker( val remote: IRemote, - private val diskCleanupInterval: Duration = Duration.ofMinutes(15) + private val diskCleanupInterval: Duration = Duration.ofMinutes(15), + private val wdaBundle: File, + private val remoteWdaBundleRoot: File, + private val fbsimctlVersion: String ) : ISimulatorHostChecker { - companion object { - private const val EXPECTED_FBSIMCTL = "HEAD-ec54965" - } - private val logger = LoggerFactory.getLogger(javaClass.simpleName) private val logMarker = MapEntriesAppendingMarker(mapOf( LogMarkers.HOSTNAME to remote.hostName @@ -37,7 +35,11 @@ class SimulatorHostChecker( override fun copyWdaBundleToHost() { logger.debug(logMarker, "Setting up remote node: copying WebDriverAgent to node ${remote.hostName}") - remote.rsync(WDA_BUNDLE.absolutePath, REMOTE_WDA_BUNDLE_ROOT, setOf("-r", "--delete")) + remote.rsync( + wdaBundle.absolutePath, + remoteWdaBundleRoot.absolutePath, + setOf("-r", "--delete") + ) } override fun killDiskCleanupThread() { @@ -55,7 +57,7 @@ class SimulatorHostChecker( } // temp solution, prereq should be satisfied without having to switch anything - val rv = remote.execIgnoringErrors(listOf("/usr/local/bin/brew", "switch", "fbsimctl", EXPECTED_FBSIMCTL), env = mapOf("RUBYOPT" to "")) + val rv = remote.execIgnoringErrors(listOf("/usr/local/bin/brew", "switch", "fbsimctl", fbsimctlVersion), env = mapOf("RUBYOPT" to "")) if (!rv.isSuccess) { logger.warn(logMarker, "fbsimctl switch failed, see: $rv") } @@ -64,8 +66,8 @@ class SimulatorHostChecker( val match = Regex("/fbsimctl/([-.\\w]+)/bin/fbsimctl").find(fbsimctlPath) ?: throw RuntimeException("Could not read fbsimctl version from $fbsimctlPath") val actualFbsimctlVersion = match.groupValues[1] - if (actualFbsimctlVersion != EXPECTED_FBSIMCTL) { - throw RuntimeException("Expecting fbsimctl $EXPECTED_FBSIMCTL, but it was $actualFbsimctlVersion ${match.groupValues}") + if (actualFbsimctlVersion != fbsimctlVersion) { + throw RuntimeException("Expecting fbsimctl $fbsimctlVersion, but it was $actualFbsimctlVersion ${match.groupValues}") } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt index fec034e5..bc0768c6 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt @@ -18,13 +18,13 @@ import java.net.URL import java.time.Duration import java.util.concurrent.Executors import java.util.concurrent.Future - +import java.io.File class Device( private val remote: IRemote, val deviceInfo: DeviceInfo, val allocatedPorts: DeviceAllocatedPorts, - wdaPath: String, + wdaRunnerXctest: File, usbProxy: UsbProxyFactory = UsbProxyFactory(remote) ) { val udid: String = deviceInfo.udid @@ -58,7 +58,7 @@ class Device( } private val fbsimctlProc: DeviceFbsimctlProc = DeviceFbsimctlProc(remote, deviceInfo.udid, fbsimctlEndpoint, false) - private val wdaProc = DeviceWebDriverAgent(remote, wdaPath, deviceInfo.udid, wdaEndpoint, wdaProxy.devicePort) + private val wdaProc = DeviceWebDriverAgent(remote, wdaRunnerXctest, deviceInfo.udid, wdaEndpoint, wdaProxy.devicePort) private val status = SimulatorStatus() diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlots.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlots.kt index cfdb3c6c..20418be6 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlots.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlots.kt @@ -13,10 +13,11 @@ import net.logstash.logback.marker.MapEntriesAppendingMarker import org.slf4j.LoggerFactory import org.slf4j.Marker import java.util.concurrent.ConcurrentLinkedQueue +import java.io.File class DeviceSlots( val remote: IRemote, - val wdaPath: String, + private val wdaRunnerXctest: File, private val portAllocator: PortAllocator, private val deviceInfoProvider: DeviceInfoProvider, knownDevicesList: List @@ -142,7 +143,7 @@ class DeviceSlots( remote =remote, deviceInfo = deviceInfo, allocatedPorts = allocatedPorts, - wdaPath = wdaPath + wdaRunnerXctest = wdaRunnerXctest ) device.prepareAsync() diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceWebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceWebDriverAgent.kt index f6f1de0d..b944efc8 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceWebDriverAgent.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceWebDriverAgent.kt @@ -4,18 +4,18 @@ import com.badoo.automation.deviceserver.data.UDID import com.badoo.automation.deviceserver.host.IRemote import com.badoo.automation.deviceserver.ios.proc.WebDriverAgent import java.net.URI -import java.nio.file.Paths +import java.io.File class DeviceWebDriverAgent( remote: IRemote, - wdaPath: String, + wdaRunnerXctest: File, udid: UDID, wdaEndpoint: URI, port: Int, - hostApp: String = Paths.get(wdaPath).parent.parent.toString() + hostApp: String = wdaRunnerXctest.parentFile.parentFile.absolutePath ) : WebDriverAgent( remote = remote, - wdaPath = wdaPath, + wdaRunnerXctest = wdaRunnerXctest, hostApp = hostApp, udid = udid, wdaEndpoint = wdaEndpoint, diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt index a0dfdc00..78ec31ca 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt @@ -2,18 +2,18 @@ package com.badoo.automation.deviceserver.ios.proc import com.badoo.automation.deviceserver.data.UDID import com.badoo.automation.deviceserver.host.IRemote +import java.io.File import java.net.URI class SimulatorWebDriverAgent( remote: IRemote, - wdaPath: String, + wdaRunnerXctest: File, udid: UDID, - wdaEndpoint: URI, - hostApp: String = "com.apple.MobileAddressBook" // seems like AddressBook has the least memory usage and no alerts + wdaEndpoint: URI ) : WebDriverAgent( remote = remote, - wdaPath = wdaPath, - hostApp = hostApp, + wdaRunnerXctest = wdaRunnerXctest, + hostApp = "com.apple.MobileAddressBook", // seems like AddressBook has the least memory usage and no alerts udid = udid, wdaEndpoint = wdaEndpoint ) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt index 3e7a6304..059c7706 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt @@ -6,11 +6,12 @@ import com.badoo.automation.deviceserver.host.IRemote import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl import com.badoo.automation.deviceserver.util.ensure import com.badoo.automation.deviceserver.util.uriWithPath +import java.io.File import java.net.URI open class WebDriverAgent( protected val remote: IRemote, - protected val wdaPath: String, + protected val wdaRunnerXctest: File, protected val hostApp: String, protected val udid: UDID, private val wdaEndpoint: URI, @@ -28,7 +29,7 @@ open class WebDriverAgent( FBSimctl.FBSIMCTL_BIN, udid, "launch_xctest", - wdaPath, + wdaRunnerXctest.absolutePath, hostApp, "--port", port.toString(), @@ -41,7 +42,7 @@ open class WebDriverAgent( override fun start() { ensure(childProcess == null) { WebDriverAgentError("Previous WebDriverAgent childProcess $childProcess has not been killed") } - ensure(remote.isDirectory(wdaPath)) { WebDriverAgentError("WebDriverAgent $wdaPath does not exist or is not a directory") } + ensure(remote.isDirectory(wdaRunnerXctest.absolutePath)) { WebDriverAgentError("WebDriverAgent ${wdaRunnerXctest.absolutePath} does not exist or is not a directory") } logger.debug(logMarker, "$this — Starting child process") terminateHostApp() diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt index f485c24b..93e88785 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt @@ -35,7 +35,7 @@ class Simulator ( deviceInfo: DeviceInfo, private val allocatedPorts: DeviceAllocatedPorts, private val deviceSetPath: String, - wdaPath: String, + wdaRunnerXctest: File, private val concurrentBootsPool: ThreadPoolDispatcher, headless: Boolean, override val fbsimctlSubject: String @@ -65,7 +65,7 @@ class Simulator ( private lateinit var criticalAsyncPromise: Job // 1-1 from ruby private val fbsimctlProc: FbsimctlProc = FbsimctlProc(remote, deviceInfo.udid, fbsimctlEndpoint, headless) - private val wdaProc = SimulatorWebDriverAgent(remote, wdaPath, deviceInfo.udid, wdaEndpoint) + private val wdaProc = SimulatorWebDriverAgent(remote, wdaRunnerXctest, deviceInfo.udid, wdaEndpoint) private val backup: ISimulatorBackup = SimulatorBackup(remote, udid, deviceSetPath) private val simulatorStatus = SimulatorStatus() private val logger = LoggerFactory.getLogger(javaClass.simpleName) diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt index 680fe9a8..54fd1257 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt @@ -118,7 +118,7 @@ class SimulatorsNodeTest { eq(fbsimulatorDevice), eq(DeviceAllocatedPorts(1,2, 3)), eq("/node/specific/device/set"), - eq("some/file/from/wdaPathProc"), + eq(File("some/file/from/wdaPathProc")), any(), eq(false), eq("FBSimctlDevice(arch=Arch, state=State, model=Model, name=Name, udid=Udid1, os=Os)") diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/HostFactoryTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/HostFactoryTest.kt deleted file mode 100644 index b76fb4a8..00000000 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/HostFactoryTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -package com.badoo.automation.deviceserver.ios - -import com.badoo.automation.deviceserver.DeviceServerConfig -import com.badoo.automation.deviceserver.NodeConfig -import com.badoo.automation.deviceserver.host.HostFactory -import com.badoo.automation.deviceserver.host.IRemote -import com.badoo.automation.deviceserver.host.ISimulatorHostProvider -import com.badoo.automation.deviceserver.host.ISimulatorsNode -import com.badoo.automation.deviceserver.mockThis -import com.nhaarman.mockito_kotlin.any -import com.nhaarman.mockito_kotlin.eq -import com.nhaarman.mockito_kotlin.whenever -import org.hamcrest.CoreMatchers.equalTo -import org.hamcrest.CoreMatchers.sameInstance -import org.hamcrest.MatcherAssert.assertThat -import org.junit.Test - -class HostFactoryTest { - companion object { - const val WDA_BUNDLE_PATH_STRING = "/ABSOLUTE/PATH/ios/facebook/simulators/WebDriverAgentRunner-Runner.app" - const val WDA_DEVICE_BUNDLE_PATH_STRING = "/ABSOLUTE/PATH/ios/facebook/devices/WebDriverAgentRunner-Runner.app" - } - init { - System.setProperty("wda.bundle.path", WDA_BUNDLE_PATH_STRING) - System.setProperty("wda.device.bundle.path", WDA_DEVICE_BUNDLE_PATH_STRING) - System.setProperty("device.server.config.path", "/some/dir/some/file") - } - - private val hostName: String = "some.node.name" - - private val deviceServerConfig = DeviceServerConfig( - mapOf("" to "1"), - listOf(NodeConfig(host = hostName, simulatorLimit = 99, concurrentBoots = 9)) - ) - - private var remoteMock: IRemote = mockThis() - private var remoteMockProvider: (String, String, String) -> IRemote = { host, _, _ -> - assertThat(host, equalTo(hostName)) - remoteMock - } - - private var simulatorHostProvider: ISimulatorHostProvider = mockThis() - - private var simNodeMock: ISimulatorsNode = mockThis() - - private var factory = HostFactory(remoteMockProvider, simulatorHostProvider) - - @Test - fun getHostFromConfigHandlesReachableLocalDevice() { - whenever(remoteMock.isReachable()).thenReturn(true) - whenever(remoteMock.isLocalhost()).thenReturn(true) - whenever(simulatorHostProvider.simulatorsNode( - eq(remoteMock), eq(99), eq(9), - any() - )).thenReturn(simNodeMock) - - val nodeConfig = deviceServerConfig.nodes.first() - val actual = factory.getHostFromConfig(nodeConfig) - - assertThat(actual, sameInstance(simNodeMock)) - } -} \ No newline at end of file From fb2c6337e7d31e754aa1443b4edf91cf4edb8586 Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Wed, 19 Sep 2018 11:14:30 +0100 Subject: [PATCH 041/131] Cleanup only fbsimctl processes --- .../com/badoo/automation/deviceserver/host/DevicesNode.kt | 2 +- .../deviceserver/host/management/ISimulatorHostChecker.kt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt index 3e9ef09b..bdb72ec5 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt @@ -311,6 +311,6 @@ class DevicesNode( private fun cleanup() { // single instance of server on node is implied, so we can kill all simulators and fbsimctl processes - remote.execIgnoringErrors(listOf("pkill", "-9", "fbsimctl")) + remote.execIgnoringErrors(listOf("pkill", "-9", "/usr/local/bin/fbsimctl")) } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt index 857acf2a..b4376499 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt @@ -74,7 +74,7 @@ class SimulatorHostChecker( override fun cleanup() { try { logger.info(logMarker, "Will kill abandoned long living fbsimctl processes") - remote.pkill("fbsimctl") + remote.pkill("/usr/local/bin/fbsimctl") logger.info(logMarker, "Will shutdown booted simulators") remote.fbsimctl.shutdownAllBooted() logger.info(logMarker, "Done shutting down booted simulators") From 94012f787bce7c27f4d41c7ce40ef78f00707a24 Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Wed, 19 Sep 2018 18:38:47 +0100 Subject: [PATCH 042/131] Use WebDriverAgentRunner-Runner.app as host app for WebDriverAgent --- .../deviceserver/ios/fbsimctl/FBSimctl.kt | 9 ++++++ .../deviceserver/ios/fbsimctl/FBSimctlDTO.kt | 5 +++ .../ios/fbsimctl/FBSimctlResponseParser.kt | 25 +++++++++++++++ .../deviceserver/ios/fbsimctl/IFBSimctl.kt | 3 ++ .../ios/fbsimctl/IFBSimctlResponseParser.kt | 1 + .../ios/proc/SimulatorWebDriverAgent.kt | 31 +++++++++++++++++-- .../fbsimctl/FbSimctlResponseParserTest.kt | 29 +++++++++++++++-- 7 files changed, 98 insertions(+), 5 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt index 1210c53d..4a759b07 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt @@ -6,6 +6,7 @@ import com.badoo.automation.deviceserver.command.RemoteShellCommand import com.badoo.automation.deviceserver.data.UDID import com.badoo.automation.deviceserver.util.ensure import org.slf4j.LoggerFactory +import java.io.File import java.time.Duration class FBSimctl( @@ -21,6 +22,14 @@ class FBSimctl( private val logger = LoggerFactory.getLogger(javaClass.simpleName) + override fun installApp(udid: UDID, bundlePath: File) { + val response = fbsimctl(listOf("install", bundlePath.absolutePath), udid, raiseOnError = true) + val result = parser.parseInstallApp(response) + if (!result.isSuccess) { + throw FBSimctlError("Failed to install application [${bundlePath.absolutePath}]. Error:\n${result.errorMessage}") + } + } + override fun listSimulators(): List { val cmd = listOf("--simulators", "list") return parser.parseDeviceList(fbsimctl(cmd, raiseOnError = false)) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlDTO.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlDTO.kt index 0c06f527..10ed87d9 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlDTO.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlDTO.kt @@ -34,4 +34,9 @@ data class FBSimctlDeviceDiagnosticInfo( val sysLogLocation: String?, val coreSimulatorLogLocation: String?, val videoLocation: String? +) + +data class FBSimctlInstallResult( + val isSuccess: Boolean, + val errorMessage: String = "" ) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlResponseParser.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlResponseParser.kt index 5cd54dfd..19fcc565 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlResponseParser.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlResponseParser.kt @@ -71,6 +71,31 @@ class FBSimctlResponseParser : IFBSimctlResponseParser { return parsedResponse.subject } + override fun parseInstallApp(response: String): FBSimctlInstallResult { + val mapper = JsonMapper() + val events = response.lines() + .asSequence() + .filter { !isLogEvent(it) } + .map { mapper.fromJson>(it) } + .toList() + + val failureEvent = events.find { + it["event_name"] == "failure" + } + + val errorMessage = if (failureEvent != null) { + failureEvent["subject"] ?: response + } else { + "" + } + + val isInstalled = failureEvent == null && events.any { + it["event_type"] == "ended" && it["event_name"] == "install" + } + + return FBSimctlInstallResult(isInstalled, errorMessage) + } + private fun getFileLocation(result: List>, fileType: String): String? { val found = result .map { diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctl.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctl.kt index 3a7ca6b0..99180f6c 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctl.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctl.kt @@ -1,8 +1,11 @@ package com.badoo.automation.deviceserver.ios.fbsimctl import com.badoo.automation.deviceserver.data.UDID +import java.io.File interface IFBSimctl { + fun installApp(udid: UDID, bundlePath: File) + /** * List simulators */ diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctlResponseParser.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctlResponseParser.kt index bb355ef7..5be80ead 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctlResponseParser.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctlResponseParser.kt @@ -7,4 +7,5 @@ interface IFBSimctlResponseParser { fun parseDeviceCreation(response: String, isTransitional: Boolean): FBSimctlDevice fun parse(response: String): List> fun parseDeviceSets(response: String): List + fun parseInstallApp(response: String): FBSimctlInstallResult } \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt index 78ec31ca..18c6f3b9 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt @@ -2,8 +2,10 @@ package com.badoo.automation.deviceserver.ios.proc import com.badoo.automation.deviceserver.data.UDID import com.badoo.automation.deviceserver.host.IRemote +import com.badoo.automation.deviceserver.util.pollFor import java.io.File import java.net.URI +import java.time.Duration class SimulatorWebDriverAgent( remote: IRemote, @@ -13,7 +15,32 @@ class SimulatorWebDriverAgent( ) : WebDriverAgent( remote = remote, wdaRunnerXctest = wdaRunnerXctest, - hostApp = "com.apple.MobileAddressBook", // seems like AddressBook has the least memory usage and no alerts + hostApp = wdaRunnerXctest.parentFile.parentFile.absolutePath, udid = udid, wdaEndpoint = wdaEndpoint -) \ No newline at end of file +) { + override fun start() { + installHostApp() + super.start() + } + + private fun installHostApp() { + remote.fbsimctl.installApp(udid, File(hostApp)) + + pollFor( + Duration.ofSeconds(20), + "Installing WDA host application $hostApp", + true, + Duration.ofSeconds(2), + logger, + logMarker + ) { + isHostAppInstalled() + } + } + + private fun isHostAppInstalled(): Boolean { + return remote.fbsimctl.listApps(udid) + .any { it.bundle.bundle_id.contains("WebDriverAgentRunner-Runner") } + } +} \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FbSimctlResponseParserTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FbSimctlResponseParserTest.kt index 752e353e..a3f18bcd 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FbSimctlResponseParserTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FbSimctlResponseParserTest.kt @@ -1,7 +1,6 @@ package com.badoo.automation.deviceserver.ios.fbsimctl -import org.hamcrest.Matchers.equalTo -import org.hamcrest.Matchers.instanceOf +import org.hamcrest.Matchers.* import org.junit.Assert.* import org.junit.Test @@ -39,7 +38,19 @@ class FbSimctlResponseParserTest { {"event_name":"log","timestamp":1521028581,"level":"info","subject":"Did Change State => Booted","event_type":"discrete"} {"event_name":"log","timestamp":1521028581,"level":"info","subject":"Simulator Did launch => Process launchd_sim | PID 88778","event_type":"discrete"} {"event_name":"create","timestamp":1521028581,"subject":{"pid":0,"arch":"x86_64","os":"iOS 11.2","container-pid":0,"model":"iPhone 6","udid":"7CA9DCE7-22A2-434B-A9EE-3E2A497E3881","name":"iPhone 6","state":"Shutdown"},"event_type":"ended"} - """.trimIndent() + """.trimIndent() + + private val appInstallFailure = """ + { "event_name": "log", "timestamp": 1537374100, "level": "info", "subject": "Running \/usr\/bin\/xcode-select --print-path with environment {\n HOME = \"\/Users\/vfrolov\";\n}", "event_type": "discrete" } + { "event_type": "started", "subject": { "application_path": "\/Users\/vfrolov\/GitHub\/ios-device-server\/ios\/facebook\/simulators-Facebook-c233bf07-x10\/WebDriverAgentRunner-Runner.app\/aasdfasdfasdf", "codesign": false }, "timestamp": 1537374100, "target": { "arch": "x86_64", "state": "Booted", "model": "iPhone 6", "name": "iPhone 6", "udid": "AB3C83FF-C05F-4CAA-9199-18E6C87FA9FB", "os": "iOS 11.4" }, "event_name": "install" } + { "event_name": "failure", "timestamp": 1537374100, "subject": "Error Domain=com.facebook.FBControlCore Code=0 \"File at path \/Users\/vfrolov\/GitHub\/ios-device-server\/ios\/facebook\/simulators-Facebook-c233bf07-x10\/WebDriverAgentRunner-Runner.app\/aasdfasdfasdf is neither an IPA not a .app\" UserInfo={NSLocalizedDescription=File at path \/Users\/vfrolov\/GitHub\/ios-device-server\/ios\/facebook\/simulators-Facebook-c233bf07-x10\/WebDriverAgentRunner-Runner.app\/aasdfasdfasdf is neither an IPA not a .app, NSUnderlyingError=0x7fb40a114ba0 {Error Domain=com.facebook.FBControlCore Code=0 \"Failed to open \/Users\/vfrolov\/GitHub\/ios-device-server\/ios\/facebook\/simulators-Facebook-c233bf07-x10\/WebDriverAgentRunner-Runner.app\/aasdfasdfasdf for reading\" UserInfo={NSLocalizedDescription=Failed to open \/Users\/vfrolov\/GitHub\/ios-device-server\/ios\/facebook\/simulators-Facebook-c233bf07-x10\/WebDriverAgentRunner-Runner.app\/aasdfasdfasdf for reading}}}", "event_type": "discrete" }] + """.trimIndent() + + private val appInstallSuccess = """ + { "event_name": "log", "timestamp": 1537373327, "level": "info", "subject": "Running \/usr\/bin\/xcode-select --print-path with environment {\n HOME = \"\/Users\/vfrolov\";\n}", "event_type": "discrete" } + { "event_type": "started", "subject": { "application_path": "\/Users\/vfrolov\/GitHub\/ios-device-server\/ios\/facebook\/simulators-Facebook-c233bf07-x10\/WebDriverAgentRunner-Runner.app", "codesign": false }, "timestamp": 1537373327, "target": { "arch": "x86_64", "state": "Booted", "model": "iPhone 6", "name": "iPhone 6", "udid": "AB3C83FF-C05F-4CAA-9199-18E6C87FA9FB", "os": "iOS 11.4" }, "event_name": "install" } + { "event_type": "ended", "subject": { "application_path": "\/Users\/vfrolov\/GitHub\/ios-device-server\/ios\/facebook\/simulators-Facebook-c233bf07-x10\/WebDriverAgentRunner-Runner.app", "codesign": false }, "timestamp": 1537373330, "target": { "arch": "x86_64", "state": "Booted", "model": "iPhone 6", "name": "iPhone 6", "udid": "AB3C83FF-C05F-4CAA-9199-18E6C87FA9FB", "os": "iOS 11.4" }, "event_name": "install" } + """.trimIndent() @Test fun parseList() { val parsedValue = FBSimctlResponseParser().parse(simulatorsListString) @@ -53,6 +64,18 @@ class FbSimctlResponseParserTest { assertEquals("7CA9DCE7-22A2-434B-A9EE-3E2A497E3881", parsedValue.udid) } + @Test fun installAppSuccess() { + val result = FBSimctlResponseParser().parseInstallApp(appInstallSuccess) + assertTrue("Wrong install result", result.isSuccess) + assertTrue("Wrong install error message", result.errorMessage.isBlank()) + } + + @Test fun installAppFail() { + val result = FBSimctlResponseParser().parseInstallApp(appInstallFailure) + assertFalse("Wrong install result", result.isSuccess) + assertThat(result.errorMessage, containsString("is neither an IPA not a .app")) + } + @Test fun parseDeviceList() { val parsedValue = FBSimctlResponseParser().parseDeviceList(simulatorsListString) val fbSimctlDevice = parsedValue[0] From 0f541851aa0f57bb38929800738ca1289d2ae5b0 Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Mon, 1 Oct 2018 20:59:14 +0100 Subject: [PATCH 043/131] Fix compiling with Java 8 --- .../deviceserver/host/management/ISimulatorHostChecker.kt | 4 ++-- .../badoo/automation/deviceserver/data/ErrorDtoKtTest.kt | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt index b4376499..9b396763 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt @@ -108,8 +108,8 @@ class SimulatorHostChecker( cleanUpTask = periodicTasksPool.scheduleWithFixedDelay( cleanUpRunnable, 0, - diskCleanupInterval.toSeconds(), - TimeUnit.SECONDS) + diskCleanupInterval.toMinutes(), + TimeUnit.MINUTES) } override fun setupHost() { diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/ErrorDtoKtTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/ErrorDtoKtTest.kt index 5f9e8785..b08e045d 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/ErrorDtoKtTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/ErrorDtoKtTest.kt @@ -11,9 +11,9 @@ class ErrorDtoKtTest { @Test fun toDto() { val stackTrace: Array = listOf( - StackTraceElement("", "", "", "com.badoo.SomeClass", "someMethod", "SomeFile.kt", 2), - StackTraceElement("", "", "", "com.badoo.SomeClass", "someMethod", "SomeFile.kt", 1), - StackTraceElement("", "", "", "io.ktor.SomeClass", "someMethod", "SomeFile.kt", 2) + StackTraceElement("com.badoo.SomeClass", "someMethod", "SomeFile.kt", 2), + StackTraceElement("com.badoo.SomeClass", "someMethod", "SomeFile.kt", 1), + StackTraceElement("io.ktor.SomeClass", "someMethod", "SomeFile.kt", 2) ).toTypedArray() whenever(exception.stackTrace).thenReturn(stackTrace) From b89455f5e69bd26fa6aaac642303c2df545ce00e Mon Sep 17 00:00:00 2001 From: TimBav Date: Wed, 3 Oct 2018 13:26:35 +0100 Subject: [PATCH 044/131] Fix warnings for this qualifiers, log default NodeConfig, fail fast jar_launcher if no jar (#19) --- device-server/jar_launcher.sh | 2 +- .../automation/deviceserver/DeviceServer.kt | 5 ++-- .../deviceserver/ios/simulator/Simulator.kt | 24 +++++++++---------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/device-server/jar_launcher.sh b/device-server/jar_launcher.sh index 063749d7..e43e664c 100755 --- a/device-server/jar_launcher.sh +++ b/device-server/jar_launcher.sh @@ -3,7 +3,7 @@ set -xe declare -r DEVICE_SERVER_CONFIG_PATH="${DEVICE_SERVER_CONFIG_PATH}" -declare -r DEVICE_SERVER_JAR="${DEVICE_SERVER_JAR}" +declare -r DEVICE_SERVER_JAR="${DEVICE_SERVER_JAR:?Jar file is required}" declare -r WDA_RUNNER=${DEVICE_SERVER_WDA_SIMULATOR_RUNNER:-'../ios/facebook/simulators/WebDriverAgentRunner-Runner.app'} declare -r WDA_DEVICE_RUNNER=${DEVICE_SERVER_WDA_DEVICE_RUNNER:-'../ios/facebook/devices/WebDriverAgentRunner-Runner.app'} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt index 7aad4a02..b2f0b1f0 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt @@ -77,8 +77,9 @@ private val appConfiguration = ApplicationConfiguration() private fun serverConfig(): DeviceServerConfig { if (appConfiguration.deviceServerConfigPath.isEmpty()) { - logger.info("Using default config") - return DeviceServerConfig(nodes = listOf(NodeConfig()), timeouts = emptyMap()) + val defaultNodeConfig = NodeConfig() + logger.info("Using default config: $defaultNodeConfig") + return DeviceServerConfig(nodes = listOf(defaultNodeConfig), timeouts = emptyMap()) } val configFile = File(appConfiguration.deviceServerConfigPath) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt index 93e88785..1efe62e6 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt @@ -91,12 +91,12 @@ class Simulator ( val elapsed = measureTimeMillis { prepare(clean = true) } - logger.info(logMarker, "Device ${Simulator@this.toString()} ready in ${elapsed / 1000} seconds") + logger.info(logMarker, "Device ${this@Simulator} ready in ${elapsed / 1000} seconds") } } private fun prepare(timeout: Duration = PREPARE_TIMEOUT, clean: Boolean) { - logger.info(logMarker, "Starting to prepare ${Simulator@this}. Will wait for ${timeout.seconds} seconds") + logger.info(logMarker, "Starting to prepare ${this@Simulator}. Will wait for ${timeout.seconds} seconds") lastException = null wdaProc.kill() shutdown() @@ -104,7 +104,7 @@ class Simulator ( //FIXME: add checks for cancellation of criticalAsyncPromise executeWithTimeout(timeout, "Preparing simulator") { // erase simulator if there is no existing backup, this is to ensure backup is created from a clean state - logger.info(logMarker, "Launch prepare sequence for ${Simulator@this} asynchronously") + logger.info(logMarker, "Launch prepare sequence for ${this@Simulator} asynchronously") if (backup.isExist()) { if (clean) { @@ -175,7 +175,7 @@ class Simulator ( } private fun shutdown() { - logger.info(logMarker, "Shutting down ${Simulator@this}") + logger.info(logMarker, "Shutting down ${this@Simulator}") ignoringErrors({ fbsimctlProc.kill() }) if (remote.fbsimctl.listDevice(udid)?.state != FBSimctlDeviceState.SHUTDOWN.value) { @@ -186,11 +186,11 @@ class Simulator ( } } - logger.info(logMarker, "Successfully shut down ${Simulator@this}") + logger.info(logMarker, "Successfully shut down ${this@Simulator}") } private fun boot() { - logger.info(logMarker, "Booting ${Simulator@this} asynchronously") + logger.info(logMarker, "Booting ${this@Simulator} asynchronously") val bootJob = async(context = concurrentBootsPool) { truncateSystemLogIfExists() @@ -272,15 +272,15 @@ class Simulator ( } else { try { remote.shell("echo -n > $sysLog", returnOnFailure = true) - logger.debug(logMarker, "Truncated syslog of simulator ${Simulator@ this}") + logger.debug(logMarker, "Truncated syslog of simulator ${this@Simulator}") } catch (e: RuntimeException) { - logger.error(logMarker, "Error while truncating syslog of simulator ${Simulator@ this}", e) + logger.error(logMarker, "Error while truncating syslog of simulator ${this@Simulator}", e) } } } private fun logTiming(actionName: String, action: () -> Unit) { - logger.info(logMarker, "Device ${Simulator@this} starting action <$actionName>") + logger.info(logMarker, "Device ${this@Simulator} starting action <$actionName>") val millis = measureTimeMillis(action) val seconds = millis / 1000 val measurement = mutableMapOf( @@ -288,7 +288,7 @@ class Simulator ( "duration" to seconds ) measurement.putAll(commonLogMarkerDetails) - logger.info(MapEntriesAppendingMarker(measurement), "Device ${Simulator@this} action <$actionName> took $seconds seconds") + logger.info(MapEntriesAppendingMarker(measurement), "Device ${this@Simulator} action <$actionName> took $seconds seconds") } //endregion @@ -340,7 +340,7 @@ class Simulator ( private fun executeCritical(action: () -> Unit) { if (deviceLock.isLocked) { - logger.info(logMarker, "Awaiting for previous action. Likely a criticalAsyncPromise $criticalAsyncPromise on ${Simulator@this}") + logger.info(logMarker, "Awaiting for previous action. Likely a criticalAsyncPromise $criticalAsyncPromise on ${this@Simulator}") } deviceLock.withLock { @@ -351,7 +351,7 @@ class Simulator ( lastException = e // FIXME: force shutdown failed sim logger.error(logMarker, "Execute critical block finished with exception. Message: [${e.message}]", e) - logger.warn(logMarker, "Host stats on ${Simulator@this} are:\n${getSystemStats()}") + logger.warn(logMarker, "Host stats on ${this@Simulator} are:\n${getSystemStats()}") } } } From 5ea8582dd7b9659a6f4abe8fef297a627b0de592 Mon Sep 17 00:00:00 2001 From: sergey-plevako-badoo Date: Wed, 3 Oct 2018 14:15:59 +0100 Subject: [PATCH 045/131] added shake support (#20) Added "shake" gesture support for iOS simulator --- .../deviceserver/controllers/DevicesController.kt | 1 + .../com/badoo/automation/deviceserver/host/DevicesNode.kt | 4 ++++ .../badoo/automation/deviceserver/host/ISimulatorsNode.kt | 1 + .../badoo/automation/deviceserver/host/SimulatorsNode.kt | 4 ++++ .../deviceserver/host/management/DeviceManager.kt | 4 ++++ .../deviceserver/host/management/IDeviceManager.kt | 1 + .../automation/deviceserver/ios/simulator/ISimulator.kt | 1 + .../automation/deviceserver/ios/simulator/Simulator.kt | 6 ++++++ 8 files changed, 22 insertions(+) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt index ec8ceb4b..730becf4 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt @@ -26,6 +26,7 @@ class DevicesController(private val deviceManager: IDeviceManager) { when (action) { "reset" -> deviceManager.resetAsyncDevice(ref) "clear_safari_cookies" -> deviceManager.clearSafariCookies(ref) + "shake" -> deviceManager.shake(ref) else -> throw IllegalArgumentException("Unknown action $action") } return happy diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt index bdb72ec5..ba1b890d 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt @@ -68,6 +68,10 @@ class DevicesNode( throw(NotImplementedError("Clear Safari Cookies is not supported by physical devices")) } + override fun shake(deviceRef: DeviceRef) { + throw(NotImplementedError("Shake gesture is not supported by physical devices")) + } + override fun endpointFor(deviceRef: DeviceRef, port: Int): URL { val device = slotByExternalRef(deviceRef).device return device.endpointFor(port) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt index d6f6e6eb..9cd45f82 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt @@ -10,6 +10,7 @@ interface ISimulatorsNode { fun approveAccess(deviceRef: DeviceRef, bundleId: String) fun clearSafariCookies(deviceRef: DeviceRef) + fun shake(deviceRef: DeviceRef) fun endpointFor(deviceRef: DeviceRef, port: Int): URL fun lastCrashLog(deviceRef: DeviceRef): CrashLog fun state(deviceRef: DeviceRef): SimulatorStatusDTO diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt index 025ae8ee..4164ccc0 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt @@ -130,6 +130,10 @@ class SimulatorsNode( getDeviceFor(deviceRef).clearSafariCookies() } + override fun shake(deviceRef: DeviceRef) { + getDeviceFor(deviceRef).shake() + } + override fun count(): Int = devicePool.size override fun dispose() { diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt index a13e8aa6..3175f18d 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt @@ -89,6 +89,10 @@ class DeviceManager( return nodeRegistry.activeDevices.getNodeFor(ref).lastCrashLog(ref) } + override fun shake(ref: DeviceRef) { + nodeRegistry.activeDevices.getNodeFor(ref).shake(ref) + } + override fun startVideo(ref: DeviceRef) { nodeRegistry.activeDevices.getNodeFor(ref).videoRecordingStart(ref) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt index 7a3f6b5e..1eaf8cb4 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt @@ -13,6 +13,7 @@ interface IDeviceManager { fun approveAccess(ref: DeviceRef, bundleId: String) fun getEndpointFor(ref: DeviceRef, port: Int): URL fun getLastCrashLog(ref: DeviceRef): CrashLog + fun shake(ref: DeviceRef) fun startVideo(ref: DeviceRef) fun stopVideo(ref: DeviceRef) fun getVideo(ref: DeviceRef): ByteArray diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt index 5716bbc2..c524b720 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt @@ -27,6 +27,7 @@ interface ISimulator { fun approveAccess(bundleId: String) fun release(reason: String) fun clearSafariCookies(): Map + fun shake(): Boolean fun lastCrashLog(): CrashLog fun dataContainer(bundleId: String): DataContainer } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt index 1efe62e6..ac14e725 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt @@ -551,6 +551,12 @@ class Simulator ( return mapOf("status" to "true") } + override fun shake() : Boolean { + val command = listOf("xcrun", "simctl", "notify_post", udid, "com.apple.UIKit.SimulatorShake") + val result = remote.execIgnoringErrors(command) + return result.isSuccess + } + //region last crash log override fun lastCrashLog(): CrashLog { val crashLogs = listCrashLogs() From 8fd00b3556926088bb474cd60c54ebe6ecdafae3 Mon Sep 17 00:00:00 2001 From: Nick Abalov Date: Fri, 12 Oct 2018 13:57:37 +0100 Subject: [PATCH 046/131] Fix permissions update sql (#21) --- .../badoo/automation/deviceserver/ios/simulator/Simulator.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt index ac14e725..e6a4d68b 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt @@ -475,7 +475,7 @@ class Simulator ( val sqlCmd = "sqlite3 ${path.absolutePath}/data/Library/TCC/TCC.db" val insert = "$sqlCmd \"INSERT INTO access (service, client, client_type, allowed, prompt_count) VALUES ('$key','$bundleId',0,1,1);\"" - val update = "$sqlCmd \"UPDATE access SET allowed=1 where client='#{bundle_id}' AND service='#{key}'\"" + val update = "$sqlCmd \"UPDATE access SET allowed=1 where client='$bundleId' AND service='$key'\"" // FIXME: should we fail if sqlite3 fails (insert or update) or shall we do a separate check for access to be granted? remote.shell(insert) From 1c6e98a6ab84f02269822a5ac282bae80955a703 Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Tue, 30 Oct 2018 11:21:52 +0000 Subject: [PATCH 047/131] Add Uninstall application API (#22) --- .../badoo/automation/deviceserver/DeviceServer.kt | 12 ++++++++---- .../deviceserver/controllers/DevicesController.kt | 7 ++++++- .../automation/deviceserver/host/DevicesNode.kt | 5 +++++ .../automation/deviceserver/host/ISimulatorsNode.kt | 1 + .../automation/deviceserver/host/SimulatorsNode.kt | 4 ++++ .../deviceserver/host/management/DeviceManager.kt | 4 ++++ .../deviceserver/host/management/IDeviceManager.kt | 3 ++- .../automation/deviceserver/ios/device/Device.kt | 4 ++++ .../deviceserver/ios/simulator/ISimulator.kt | 1 + .../deviceserver/ios/simulator/Simulator.kt | 4 ++++ 10 files changed, 39 insertions(+), 6 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt index b2f0b1f0..22d8b1b1 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt @@ -2,10 +2,7 @@ package com.badoo.automation.deviceserver import com.badoo.automation.deviceserver.controllers.DevicesController import com.badoo.automation.deviceserver.controllers.StatusController -import com.badoo.automation.deviceserver.data.DataPath -import com.badoo.automation.deviceserver.data.DesiredCapabilities -import com.badoo.automation.deviceserver.data.ErrorDto -import com.badoo.automation.deviceserver.data.toDto +import com.badoo.automation.deviceserver.data.* import com.badoo.automation.deviceserver.host.HostFactory import com.badoo.automation.deviceserver.host.management.DeviceManager import com.badoo.automation.deviceserver.host.management.errors.DeviceCreationException @@ -202,6 +199,13 @@ fun Application.module() { call.respond(devicesController.listFiles(ref, dataPath)) } } + route("app") { + delete("{bundleId}") { + val ref = param(call, "ref") + val bundleId = param(call, "bundleId") + call.respond(devicesController.uninstallApplication(ref, bundleId)) + } + } route("video") { get { //FIXME: should be a better way of streaming a file over HTTP. without caching bytes in server's memory. Investigating ByteReadChannel diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt index 730becf4..a85f1b33 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt @@ -92,4 +92,9 @@ class DevicesController(private val deviceManager: IDeviceManager) { fun pullFile(ref: DeviceRef, dataPath: DataPath): ByteArray { return deviceManager.pullFile(ref, dataPath) } -} \ No newline at end of file + + fun uninstallApplication(ref: DeviceRef, bundleId: String): EmptyMap { + deviceManager.uninstallApplication(ref, bundleId) + return happy + } +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt index ba1b890d..7b97a9f9 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt @@ -238,6 +238,11 @@ class DevicesNode( override fun pullFile(deviceRef: DeviceRef, dataPath: DataPath): ByteArray = throw(NotImplementedError()) // endregion + override fun uninstallApplication(deviceRef: DeviceRef, bundleId: String) { + val device = slotByExternalRef(deviceRef).device + device.uninstallApplication(bundleId) + } + private fun deviceToDto(deviceRef: DeviceRef, device: Device): DeviceDTO { return DeviceDTO( ref = deviceRef, diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt index 9cd45f82..48c6b7e6 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt @@ -34,4 +34,5 @@ interface ISimulatorsNode { fun capacityRemaining(desiredCaps: DesiredCapabilities): Float fun createDeviceAsync(desiredCaps: DesiredCapabilities): DeviceDTO fun dispose() + fun uninstallApplication(deviceRef: DeviceRef, bundleId: String) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt index 4164ccc0..a21bca6b 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt @@ -225,6 +225,10 @@ class SimulatorsNode( return getDeviceFor(deviceRef).dataContainer(dataPath.bundleId).readFile(dataPath.path) } + override fun uninstallApplication(deviceRef: DeviceRef, bundleId: String) { + getDeviceFor(deviceRef).uninstallApplication(bundleId) + } + override fun toString(): String { return "${javaClass.simpleName} at $remoteAddress" } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt index 3175f18d..04047d1e 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt @@ -109,6 +109,10 @@ class DeviceManager( nodeRegistry.activeDevices.getNodeFor(ref).videoRecordingDelete(ref) } + override fun uninstallApplication(ref: DeviceRef, bundleId: String) { + nodeRegistry.activeDevices.getNodeFor(ref).uninstallApplication(ref, bundleId) + } + override fun getDeviceState(ref: DeviceRef): SimulatorStatusDTO { return nodeRegistry.activeDevices.getNodeFor(ref).state(ref) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt index 1eaf8cb4..85902b0f 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt @@ -27,4 +27,5 @@ interface IDeviceManager { fun isReady(): Boolean fun listFiles(ref: DeviceRef, dataPath: DataPath): List fun pullFile(ref: DeviceRef, dataPath: DataPath): ByteArray -} \ No newline at end of file + fun uninstallApplication(ref: String, bundleId: String) +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt index bc0768c6..52194ce6 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt @@ -362,6 +362,10 @@ class Device( } } + fun uninstallApplication(bundleId: String) { + remote.fbsimctl.uninstallApp(udid, bundleId) + } + private companion object { private const val CALABASH_PORT = 37265 private const val WDA_PORT = 8100 diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt index c524b720..967dee26 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt @@ -30,4 +30,5 @@ interface ISimulator { fun shake(): Boolean fun lastCrashLog(): CrashLog fun dataContainer(bundleId: String): DataContainer + fun uninstallApplication(bundleId: String) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt index e6a4d68b..23498fab 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt @@ -611,4 +611,8 @@ class Simulator ( } //endregion + + override fun uninstallApplication(bundleId: String) { + remote.fbsimctl.uninstallApp(udid, bundleId) + } } From 8726353bbccd18c2c9f69af12b05b743f2e076da Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Mon, 5 Nov 2018 13:44:32 +0000 Subject: [PATCH 048/131] Terminate SafariViewService when cleaning cookies (#23) --- .../deviceserver/ios/simulator/Simulator.kt | 6 +- .../ios/simulator/SimulatorProcess.kt | 42 +++++++++++++ .../ios/simulator/SimulatorProcessTest.kt | 60 +++++++++++++++++++ 3 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcess.kt create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcessTest.kt diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt index 23498fab..c6338e32 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt @@ -76,6 +76,7 @@ class Simulator ( ) private val logMarker: Marker = MapEntriesAppendingMarker(commonLogMarkerDetails) private val fileSystem = FileSystem(remote, udid) + private val simulatorProcess = SimulatorProcess(remote, udid) //endregion //region properties from ruby with backing mutable field @@ -540,6 +541,9 @@ class Simulator ( throw IllegalStateException("$SAFARI_BUNDLE_ID not found in $apps for $this") } + // Have to kill Simulator's SafariViewService process as it holds cookies loaded + simulatorProcess.terminateChildProcess("SafariViewService") + val cookieJarPaths = cookieJars.map { cookieJar -> File(safari.data_container, listOf("Library", "Cookies", cookieJar).joinToString(File.separator)).absolutePath } @@ -548,6 +552,7 @@ class Simulator ( cmd.addAll(cookieJarPaths) val result = remote.execIgnoringErrors(cmd) check(result.isSuccess) { "Failed to remove safari cookies ($cookieJarPaths on $remote for $this: $result" } + return mapOf("status" to "true") } @@ -611,7 +616,6 @@ class Simulator ( } //endregion - override fun uninstallApplication(bundleId: String) { remote.fbsimctl.uninstallApp(udid, bundleId) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcess.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcess.kt new file mode 100644 index 00000000..ecac6671 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcess.kt @@ -0,0 +1,42 @@ +package com.badoo.automation.deviceserver.ios.simulator + +import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.host.IRemote + +class SimulatorProcess( + private val remote: IRemote, + private val udid: UDID +) { + val mainProcessPid: Int + get() { + val command = listOf("/usr/bin/pgrep", "-fl", "launchd_sim") + val result = remote.execIgnoringErrors(command) + + check(result.isSuccess) { + "No launchd_sim process is found for simulator with udid: $udid. " + + "Found: stdout: [${result.stdOut}], stderr: [${result.stdErr}]." + } + + val processList = result + .stdOut + .lines() + .filter { it.contains(udid) } + + check(processList.isNotEmpty()) { + "No launchd_sim process is found for simulator with udid: $udid. " + + "Found: stdout: [${result.stdOut}], stderr: [${result.stdErr}]." + } + + return processList + .first() + .split(" ") + .first() + .toInt() + } + + fun terminateChildProcess(processName: String) { + // Sends SIGKILL to process with parent pid $mainProcessPid and name $processName + val command = listOf("/usr/bin/pkill", "-9", "-P", "$mainProcessPid", "-f", processName) + remote.execIgnoringErrors(command) + } +} diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcessTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcessTest.kt new file mode 100644 index 00000000..010ab19d --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcessTest.kt @@ -0,0 +1,60 @@ +package com.badoo.automation.deviceserver.ios.simulator + +import com.badoo.automation.deviceserver.command.CommandResult +import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.host.IRemote +import com.badoo.automation.deviceserver.mockThis +import com.nhaarman.mockito_kotlin.* +import org.junit.Assert.assertEquals +import org.junit.Test +import kotlin.test.assertFailsWith + +class SimulatorProcessTest { + private val udid: UDID = "ADB25768-5C9D-487E-A787-D271934B78B0" + private val remote = mockThis() + private val stdOutWithSimulatorPid = """ + 41757 launchd_sim /Users/z/Library/Developer/CoreSimulator/Devices/ADB25768-5C9D-487E-A787-D271934B78B0/data/var/run/launchd_bootstrap.plist + 47009 /usr/bin/ssh -o ConnectTimeout=10 -o PreferredAuthentications=publickey -q -t -t z@localhost /usr/bin/pgrep -fl launchd_sim + """.trimIndent() + + @Test + fun testSimulatorProcessFound() { + val simulatorProcess = SimulatorProcess(remote, udid) + val simulatorFoundCommandResult = CommandResult(stdOutWithSimulatorPid, "", ByteArray(0), 0) + + whenever(remote.execIgnoringErrors(any(), any(), any())) + .thenReturn(simulatorFoundCommandResult) + + assertEquals(41757, simulatorProcess.mainProcessPid) + } + + @Test + fun testSimulatorProcessNotFound() { + val simulatorProcess = SimulatorProcess(remote, udid) + val noSimulatorFoundCommandResult = CommandResult("", "", ByteArray(0), 0) + + whenever(remote.execIgnoringErrors(any(), any(), any())) + .thenReturn(noSimulatorFoundCommandResult) + + assertFailsWith { + simulatorProcess.mainProcessPid + } + } + + @Test + fun testKillSimulatorProcess() { + val simulatorProcess = SimulatorProcess(remote, udid) + + whenever(remote.execIgnoringErrors(any(), any(), any())) + .thenReturn(CommandResult(stdOutWithSimulatorPid, "", ByteArray(0), 0)) + .thenReturn(CommandResult("", "", ByteArray(0), 0)) + + simulatorProcess.terminateChildProcess("SafariViewService") + + val argumentCaptor = argumentCaptor>() + verify(remote, times(2)).execIgnoringErrors(argumentCaptor.capture(), any(), any()) + + val pkillCommand = listOf("/usr/bin/pkill", "-9", "-P", "41757", "-f", "SafariViewService") + assertEquals(pkillCommand, argumentCaptor.secondValue) + } +} From 4170eaf84095f92771e5e2ccee533da41bf677cb Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Fri, 2 Nov 2018 13:43:38 +0000 Subject: [PATCH 049/131] Use xcrun simctl to uninstall application --- .../badoo/automation/deviceserver/ios/simulator/Simulator.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt index c6338e32..615afdb3 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt @@ -617,6 +617,7 @@ class Simulator ( //endregion override fun uninstallApplication(bundleId: String) { - remote.fbsimctl.uninstallApp(udid, bundleId) + logger.debug(logMarker, "Uninstalling aplication $bundleId from Simulator $this") + remote.execIgnoringErrors(listOf("xcrun", "simctl", "uninstall", udid, bundleId)) } } From 20ddc2cc39b8c4512c8eb77d0da65d9288a93c3b Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Mon, 10 Dec 2018 11:47:40 +0000 Subject: [PATCH 050/131] Remove retries when pinging node by SSH. Reduce timeout to 15 seconds. --- .../automation/deviceserver/host/Remote.kt | 20 ++++--------------- 1 file changed, 4 insertions(+), 16 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt index c8284b65..d4237a39 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt @@ -42,26 +42,14 @@ class Remote( override fun isReachable(): Boolean { //FIXME: We need a reliable way to determine if node is available. SSH request might just time-out if node is under heavy load. - var attempts = 3 - - do { - attempts-- - - if (isReachableBySSH()) { - return true - } - - } while (attempts > 0) - - logger.error(logMarker, "Node $hostName is NOT reachable by SSH.") - return false + return isReachableBySSH() } private fun isReachableBySSH(): Boolean { - try { - return remoteExecutor.exec(listOf("echo", "1"), returnFailure = true).isSuccess + return try { + remoteExecutor.exec(listOf("echo", "1"), returnFailure = true, timeOut = Duration.ofSeconds(15)).isSuccess } catch (e: SshConnectionException) { - return false + false } } From fd52787e0c940475cfc0f0a1cfdb86f9147976e8 Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Mon, 10 Dec 2018 12:18:04 +0000 Subject: [PATCH 051/131] Get alive nodes and calculate capacity in parallel --- .../deviceserver/host/management/NodeRegistry.kt | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt index c9df7f9d..14b8e074 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt @@ -16,6 +16,8 @@ import org.slf4j.LoggerFactory import org.slf4j.Marker import java.time.Duration import java.util.concurrent.ConcurrentHashMap +import java.util.stream.Collectors.toSet +import java.util.stream.Stream class NodeRegistry(val activeDevices: ActiveDevices = ActiveDevices()) { private var initialRegistrationComplete: Boolean = false @@ -48,13 +50,18 @@ class NodeRegistry(val activeDevices: ActiveDevices = ActiveDevices()) { } private fun getAlive(): Set { - return nodeWrappers.filter { it.isAlive() }.toSet() + val filteredStream: Stream = nodeWrappers + .parallelStream() + .filter { it.isAlive() } + return filteredStream.collect(toSet()) } fun capacitiesTotal(desiredCapabilities: DesiredCapabilities): Map { - val count = getAlive() + val capacities: Stream = getAlive() + .parallelStream() .map { it.node.totalCapacity(desiredCapabilities) } - .sum() + + val count = capacities.reduce(0, Integer::sum) return mapOf("total" to count) } From 5b6d3596b323ce38a930371e50d70711c396af41 Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Tue, 11 Dec 2018 10:30:39 +0000 Subject: [PATCH 052/131] Reduce logging of child processes --- .../com/badoo/automation/deviceserver/ios/device/UsbProxy.kt | 4 ++-- .../badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt | 4 ++-- .../badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxy.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxy.kt index d901e5f8..d9887c45 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxy.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxy.kt @@ -41,7 +41,7 @@ class UsbProxy( remote.userName, listOf(IPROXY_BIN, localPort.toString(), devicePort.toString(), udid), false, - { message -> logger.debug(logMarker, "${this}: iproxy : ${message.trim()}") }, + { message -> logger.trace(logMarker, "${this}: iproxy : ${message.trim()}") }, { message -> logger.debug(logMarker, "${this}: iproxy : ${message.trim()}") } ) @@ -50,7 +50,7 @@ class UsbProxy( remote.userName, listOf(SOCAT_BIN, "tcp-listen:$localPort,reuseaddr,fork", "tcp:0.0.0.0:$localPort"), false, - { message -> logger.debug(logMarker, "${this}: socat : ${message.trim()}") }, + { message -> logger.trace(logMarker, "${this}: socat : ${message.trim()}") }, { message -> logger.debug(logMarker, "${this}: socat : ${message.trim()}") } ) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt index a8441c13..ee93652d 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt @@ -34,8 +34,8 @@ open class FbsimctlProc( remote.userName, getFbsimctlCommand(headless), true, - { logger.info(logMarker, "${this@FbsimctlProc}: FbSimCtl : ${it.trim()}") }, - { logger.warn(logMarker, "${this@FbsimctlProc}: FbSimCtl : ${it.trim()}") } + { logger.trace(logMarker, "${this@FbsimctlProc}: FbSimCtl : ${it.trim()}") }, + { logger.debug(logMarker, "${this@FbsimctlProc}: FbSimCtl : ${it.trim()}") } ) logger.debug(logMarker, "$this FBSimCtl: $childProcess") diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt index 059c7706..8c38ac2a 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt @@ -52,8 +52,8 @@ open class WebDriverAgent( remote.userName, launchXctestCommand, false, - { message -> logger.info(logMarker, "${this@WebDriverAgent}: WDA : ${message.trim()}") }, - { message -> logger.warn(logMarker, "${this@WebDriverAgent}: WDA : ${message.trim()}") } + { message -> logger.trace(logMarker, "${this@WebDriverAgent}: WDA : ${message.trim()}") }, + { message -> logger.debug(logMarker, "${this@WebDriverAgent}: WDA : ${message.trim()}") } ) Thread.sleep(5000) // 5 should be ok From c5bacaf3e73792f1493ea30f2903c55d1f558940 Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Thu, 13 Dec 2018 16:40:53 +0000 Subject: [PATCH 053/131] Disable STDOUT from WebDriverAgent (too much XCTest output) --- .../automation/deviceserver/command/ChildProcess.kt | 4 ++-- .../ios/proc/LongRunningProcessListener.kt | 12 ++++++++---- .../deviceserver/ios/proc/WebDriverAgent.kt | 6 +++--- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ChildProcess.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ChildProcess.kt index 3413b29f..31f6eecd 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ChildProcess.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ChildProcess.kt @@ -42,8 +42,8 @@ class ChildProcess private constructor( companion object { fun fromCommand( remoteHost: String, userName: String, cmd: List, isInteractiveShell: Boolean, - out_reader: (line: String) -> Unit, - err_reader: (line: String) -> Unit + out_reader: ((line: String) -> Unit)?, + err_reader: ((line: String) -> Unit)? ): ChildProcess { val executor = Remote.getRemoteCommandExecutor(hostName = remoteHost, userName = userName, isInteractiveShell = isInteractiveShell) return ChildProcess(cmd, executor, remoteHost, LongRunningProcessListener(out_reader, err_reader)) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProcessListener.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProcessListener.kt index 9d491515..cad63aa1 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProcessListener.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProcessListener.kt @@ -11,8 +11,8 @@ import java.time.Duration import java.util.concurrent.TimeUnit class LongRunningProcessListener( - private val outReader: (line: String) -> Unit, - private val errReader: (line: String) -> Unit + private val outReader: ((line: String) -> Unit)?, + private val errReader: ((line: String) -> Unit)? ) : NuAbstractCharsetHandler(UTF_8.newEncoder(), UTF_8.newDecoder(), UTF_8.newDecoder()) { private val logger = LoggerFactory.getLogger(javaClass.simpleName) private val errStringBuilder = StringBuilder(BUFFER_SIZE) @@ -51,12 +51,16 @@ class LongRunningProcessListener( //region read out and err override fun onStderrChars(buffer: CharBuffer?, closed: Boolean, coderResult: CoderResult?) { - fetchOutput(buffer, closed, errStringBuilder, errReader) + if (errReader != null) { + fetchOutput(buffer, closed, errStringBuilder, errReader) + } super.onStderrChars(buffer, closed, coderResult) } override fun onStdoutChars(buffer: CharBuffer?, closed: Boolean, coderResult: CoderResult?) { - fetchOutput(buffer, closed, outStringBuilder, outReader) + if (outReader != null) { + fetchOutput(buffer, closed, outStringBuilder, outReader) + } super.onStdoutChars(buffer, closed, coderResult) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt index 8c38ac2a..ea9466ec 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt @@ -21,8 +21,8 @@ open class WebDriverAgent( userName: String, cmd: List, isInteractiveShell: Boolean, - out_reader: (line: String) -> Unit, - err_reader: (line: String) -> Unit + out_reader: ((line: String) -> Unit)?, + err_reader: ((line: String) -> Unit)? ) -> ChildProcess = ChildProcess.Companion::fromCommand ) : LongRunningProc(udid, remote.hostName) { private val launchXctestCommand: List = listOf( @@ -52,7 +52,7 @@ open class WebDriverAgent( remote.userName, launchXctestCommand, false, - { message -> logger.trace(logMarker, "${this@WebDriverAgent}: WDA : ${message.trim()}") }, + null, { message -> logger.debug(logMarker, "${this@WebDriverAgent}: WDA : ${message.trim()}") } ) From 2fa27017d69c8470b6fb3856a2477c2db4719d7b Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Mon, 10 Dec 2018 16:52:45 +0000 Subject: [PATCH 054/131] Fix video recording --- .../deviceserver/ios/simulator/Simulator.kt | 8 +- .../simulator/video/IOSurfaceAttributes.kt | 51 ++++++ .../simulator/video/SimulatorVideoRecorder.kt | 172 ++++++++++-------- .../video/SimulatorVideoRecorderTest.kt | 59 ------ 4 files changed, 156 insertions(+), 134 deletions(-) create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/IOSurfaceAttributes.kt delete mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorderTest.kt diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt index 615afdb3..11866262 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt @@ -23,6 +23,7 @@ import java.io.FileOutputStream import java.io.IOException import java.net.URI import java.net.URL +import java.nio.file.Paths import java.time.Duration import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -47,7 +48,6 @@ class Simulator ( private const val SAFARI_BUNDLE_ID = "com.apple.mobilesafari" } - //region public properties copied from Ruby override val ref = deviceRef override val udid = deviceInfo.udid override val fbsimctlEndpoint = URI("http://${remote.publicHostName}:${allocatedPorts.fbsimctlPort}/$udid/") @@ -55,8 +55,10 @@ class Simulator ( override val userPorts = allocatedPorts override val info = deviceInfo override val calabashPort: Int = allocatedPorts.calabashPort - override val videoRecorder: SimulatorVideoRecorder = SimulatorVideoRecorder(udid, remote) - //endregion + + private val recordingLocation = Paths.get(deviceSetPath, udid, "video.mp4").toFile() + + override val videoRecorder: SimulatorVideoRecorder = SimulatorVideoRecorder(deviceInfo, remote, location = recordingLocation) //region instance state variables private val deviceLock = ReentrantLock() diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/IOSurfaceAttributes.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/IOSurfaceAttributes.kt new file mode 100644 index 00000000..be643cf8 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/IOSurfaceAttributes.kt @@ -0,0 +1,51 @@ +package com.badoo.automation.deviceserver.ios.simulator.video + +data class IOSurfaceAttributes( + val bytesPerRow: Int, + val size: Int, + val bytesPerElement: Int, + val pixelFormat: String +) { + val width = bytesPerRow / bytesPerElement + val height = size / bytesPerRow + + companion object { + fun fromFbSimctlLog(input: String): IOSurfaceAttributes { + val rowSize = intValueFor("\"row_size\"", input) + val frameSize = intValueFor("\"frame_size\"", input) + val pixelFormat = stringValueFor("format", input) + + if (rowSize == null || frameSize == null || pixelFormat == null) { + throw RuntimeException( + "Cannot parse IOSurface attributes from [$input]." + + "row size: $rowSize, frame size: $frameSize, pixel format $pixelFormat" + ) + } + + if (!pixelFormat.contains("BGRA")) { + throw RuntimeException("Unsupported format $pixelFormat. Only BGRA is supported.") + } + + val bytesPerElement = 4 // default for BGRA + + return IOSurfaceAttributes( + bytesPerRow = rowSize, + size = frameSize, + bytesPerElement = bytesPerElement, + pixelFormat = pixelFormat + ) + } + + private fun intValueFor(key: String, input: String): Int? { + val pattern = """^\s*${Regex.escape(key)}\s*=\s(\d+);$""".toRegex(RegexOption.MULTILINE) + + return pattern.find(input)?.groupValues?.get(1)?.toInt() + } + + private fun stringValueFor(key: String, input: String): String? { + val pattern = """^\s*${Regex.escape(key)}\s*=\s(.+);$""".toRegex(RegexOption.MULTILINE) + + return pattern.find(input)?.groupValues?.get(1) + } + } +} \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt index 865100d7..e4ddc6ec 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt @@ -1,7 +1,7 @@ package com.badoo.automation.deviceserver.ios.simulator.video import com.badoo.automation.deviceserver.command.ChildProcess -import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.data.DeviceInfo import com.badoo.automation.deviceserver.host.IRemote import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl.Companion.FBSIMCTL_BIN import com.badoo.automation.deviceserver.ios.proc.LongRunningProc @@ -13,52 +13,39 @@ import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock class SimulatorVideoRecorder( - private val udid: UDID, - private val remote: IRemote, - private val childFactory: (remoteHost: String, username: String, cmd: List, isInteractiveShell: Boolean, + private val deviceInfo: DeviceInfo, + private val remote: IRemote, + private val childFactory: (remoteHost: String, username: String, cmd: List, isInteractiveShell: Boolean, out_reader: (line: String) -> Unit, err_reader: (line: String) -> Unit ) -> ChildProcess? = ChildProcess.Companion::fromCommand, - private val recorderStopTimeout: Duration = RECORDER_STOP_TIMEOUT -) : LongRunningProc(udid, remote.hostName) { - @Volatile private var isStarted: Boolean = false + private val recorderStopTimeout: Duration = RECORDER_STOP_TIMEOUT, + location: File +) : LongRunningProc(deviceInfo.udid, remote.hostName) { + + private val udid = deviceInfo.udid + + @Volatile + private var isStarted: Boolean = false + + private val recordingLocation = location.absoluteFile + private val lock = ReentrantLock(true) - private val startVideoRecordingCommand = "set -x ;" + - "$FBSIMCTL_BIN $udid record start -- listen -- record stop -- diagnose &" + // background fbsimctl - "PID=$! && " + - "trap \"kill \$PID\" EXIT && " + // ensure we kill fbsimctl even when ssh is killed without sending input to stdin - "read && " + // block until there is an input on stdin - "kill -INT \$PID" // gracefully terminate fbsimctl recording override fun toString(): String = udid override fun checkHealth(): Boolean = childProcess?.isAlive() ?: false + private val uniqueTag = "video-recording-$udid" + fun delete() { logger.debug(logMarker, "Deleting video recording") - val info = remote.fbsimctl.diagnose(udid) - if (info.videoLocation == null || info.videoLocation.isBlank()) { - logger.debug(logMarker, "No video recording to delete") - return - } - - val result = remote.execIgnoringErrors(listOf("rm", "-f", info.videoLocation)) + val result = remote.execIgnoringErrors(listOf("rm", "-f", recordingLocation.toString())) ensure(result.isSuccess) { SimulatorVideoRecordingException("Could not delete stale recordings. Reason: $result") } } - fun dispose() { - if (childProcess?.isAlive() != true) { - return - } - - logger.info(logMarker, "Terminating video recording process") - childProcess?.kill(Duration.ofSeconds(3)) - delete() - logger.info(logMarker, "Disposed video recording") - } - override fun start() { logger.info(logMarker, "Starting video recording") lock.withLock { @@ -69,7 +56,19 @@ class SimulatorVideoRecorder( } delete() - childProcess = childFactory(remote.hostName, remote.userName, listOf(startVideoRecordingCommand), false, + + val properties = surfaceAttributes() + + if (!properties.pixelFormat.contains("BGRA")) { + throw RuntimeException("Unexpected pixel format ${properties.pixelFormat}") + } + + val frameWidth = properties.width + val frameHeight = properties.height + + val cmd = shell(videoRecordingCmd(fps = 5, frameWidth = frameWidth, frameHeight = frameHeight)) + + childProcess = childFactory(remote.hostName, remote.userName, cmd, true, { logger.debug(logMarker, "$udid: VideoRecorder : ${it.trim()}") }, { logger.debug(logMarker, "$udid: VideoRecorder : ${it.trim()}") } ) @@ -87,30 +86,32 @@ class SimulatorVideoRecorder( throw SimulatorVideoRecordingException(message) } - logger.info(logMarker, "Stopping video recording") - childProcess?.writeStdin("q\n") - pollFor(recorderStopTimeout, "Stop video recording", false, Duration.ofMillis(500), logger, logMarker) { - childProcess?.isAlive() == false + try { + logger.info(logMarker, "Stopping video recording") + + // Regex.escape is incompatible with pkill regex, so let's not escape and hope + val pattern = """^$FFMPEG_PATH.*$uniqueTag""" + remote.execIgnoringErrors(listOf("pkill", "-f", pattern)) + + pollFor(recorderStopTimeout, "Stop video recording", false, Duration.ofMillis(500), logger, logMarker) { + logger.warn("${childProcess?.isAlive()}") + + childProcess?.isAlive() == false + } + } + finally { + childProcess?.kill() + childProcess = null + logger.info(logMarker, "Stopped video recording") + isStarted = false } - childProcess?.kill() - childProcess = null - logger.info(logMarker, "Stopped video recording") - isStarted = false } } fun getRecording(): ByteArray { logger.info(logMarker, "Getting video recording") - val info = remote.fbsimctl.diagnose(udid) - - if (info.videoLocation == null) { - val message = "Could not find diagnostic video events in $info" - logger.error(logMarker, message) - throw SimulatorVideoRecordingException(message) - } - logger.debug(logMarker, "Found video recording ${info.videoLocation}") - val videoFile: File = tryCompressVideo(File(info.videoLocation)) + val videoFile = recordingLocation // TODO: is there a better way to read binary file over ssh without rsyncing? // We should get rid of ssh and move to having 1 http server per 1 host and some proxy node to tie them together @@ -125,33 +126,60 @@ class SimulatorVideoRecorder( return result.stdOutBytes } - // because we compress videos, we can't simple forward response from fbsimctl http request to /diagnose/video - // if we move compression to consumer side, this method can be simplified - private fun tryCompressVideo(srcVideo: File): File { - //FIXME: move this ["which", FFMPEG_PATH] logic to host checker. no need to do it every time when fetching a video. - if (!remote.execIgnoringErrors(listOf("which", FFMPEG_PATH)).isSuccess) { - return srcVideo + fun dispose() { + if (childProcess?.isAlive() != true) { + return } - val dstVideo = File(srcVideo.parent, "compressed.mp4") - val compressionResult = remote.execIgnoringErrors( - listOf( - FFMPEG_PATH, - "-loglevel", "panic", - "-i", srcVideo.absolutePath, - "-y", "-preset", "ultrafast", - dstVideo.absolutePath - ) - ) + logger.info(logMarker, "Terminating video recording process") + childProcess?.kill(Duration.ofSeconds(1)) + delete() + logger.info(logMarker, "Disposed video recording") + } - if (compressionResult.isSuccess) { - remote.execIgnoringErrors(listOf("rm", "-f", srcVideo.absolutePath)) - logger.debug(logMarker, "Successfully compressed video to $dstVideo") - return dstVideo + private fun shell(command: String): List { + return if (remote.isLocalhost()) { + listOf("bash", "-c", command) } else { - logger.error(logMarker, "Failed to compress video $srcVideo. $compressionResult") - remote.execIgnoringErrors(listOf("rm", "-f", dstVideo.absolutePath)) - return srcVideo + listOf(command) + } + } + + private fun videoRecordingCmd(fps: Int = 5, frameWidth: Int, frameHeight: Int, crf: Int = 35): String { + val fbsimctlStream = "$FBSIMCTL_BIN $udid stream --bgra --fps $fps -" + + val maxRecording = Duration.ofMinutes(15) // Video recording duration is capped + + val frameSize = "${frameWidth}x$frameHeight" + val recorder = "$FFMPEG_PATH -hide_banner -loglevel warning " + + "-f rawvideo " + + "-pixel_format bgra " + + "-s:v $frameSize " + + "-framerate $fps " + + "-i pipe:0 " + + "-f mp4 -vcodec h264 " + + "-t ${durationToString(maxRecording)} " + + "-crf $crf " + + "-metadata comment=$uniqueTag " + + "-y $recordingLocation" + + return "set -xeo pipefail; $fbsimctlStream | $recorder" + } + + private fun durationToString(duration: Duration): String { + return "%02d:%02d:%02d".format(duration.toHoursPart(), duration.toMinutesPart(), duration.toSecondsPart()) + } + + private fun surfaceAttributes(): IOSurfaceAttributes { + val cmd = shell("set -eo pipefail; $FBSIMCTL_BIN --debug-logging $udid stream --bgra --fps 1 - | exit") + + val rv = remote.execIgnoringErrors(cmd) + + + try { + return IOSurfaceAttributes.fromFbSimctlLog(rv.stdErr) + } catch(e: RuntimeException) { + throw(RuntimeException("Could not get IO surface attributes: $rv", e)) } } diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorderTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorderTest.kt deleted file mode 100644 index 1485e1f5..00000000 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorderTest.kt +++ /dev/null @@ -1,59 +0,0 @@ -package com.badoo.automation.deviceserver.ios.simulator.video - -import com.badoo.automation.deviceserver.command.ChildProcess -import com.badoo.automation.deviceserver.host.IRemote -import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl -import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDeviceDiagnosticInfo -import com.badoo.automation.deviceserver.mockThis -import com.nhaarman.mockito_kotlin.any -import com.nhaarman.mockito_kotlin.whenever -import org.junit.Before -import org.junit.Test -import java.time.Duration -import kotlin.test.assertFailsWith -import kotlin.test.assertFalse - -class SimulatorVideoRecorderTest { - private val childProcess: ChildProcess = mockThis() - private val remote = mockThis() - private val udid = "udid" - @Suppress("UNUSED_PARAMETER") - private fun childFactory( - remoteHost: String, username: String, cmd: List, isInteractiveShell: Boolean, - out_reader: (line: String) -> Unit, err_reader: (line: String) -> Unit - ): ChildProcess? { - return childProcess - } - - private val fbsimctl: FBSimctl = mockThis() - private val info: FBSimctlDeviceDiagnosticInfo = FBSimctlDeviceDiagnosticInfo("", "", "") - - @Before - fun setUp() { - whenever(remote.hostName).thenReturn("hostname") - whenever(remote.userName).thenReturn("username") - whenever(remote.fbsimctl).thenReturn(fbsimctl) - whenever(fbsimctl.diagnose(any())).thenReturn(info) - } - - @Test - fun stopFails() { - val recording = SimulatorVideoRecorder(udid, remote, ::childFactory, Duration.ofMillis(10)) - assertFailsWith { recording.stop() } - } - - @Test - fun doubleStartFails() { - val recording = SimulatorVideoRecorder(udid, remote, ::childFactory, Duration.ofMillis(10)) - recording.start() - assertFailsWith { recording.start() } - } - - @Test - fun stopSuccess() { - val recording = SimulatorVideoRecorder(udid, remote, ::childFactory, Duration.ofMillis(10)) - recording.start() - recording.stop() - assertFalse(recording.isProcessAlive, "Child process should not be alive") - } -} \ No newline at end of file From bb45bb3edfe0fbcc92871db9b24893dd286ddb7d Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Fri, 14 Dec 2018 13:53:27 +0000 Subject: [PATCH 055/131] Update selenium-webdriver gem --- client/rb/Gemfile | 2 +- client/rb/Gemfile.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/client/rb/Gemfile b/client/rb/Gemfile index 97c6a7fa..0d4d9bc1 100644 --- a/client/rb/Gemfile +++ b/client/rb/Gemfile @@ -1,3 +1,3 @@ source 'https://rubygems.org' -gem 'selenium-webdriver', '3.5.0' +gem 'selenium-webdriver', '3.12.0' diff --git a/client/rb/Gemfile.lock b/client/rb/Gemfile.lock index 9922595d..ea41bcbc 100644 --- a/client/rb/Gemfile.lock +++ b/client/rb/Gemfile.lock @@ -1,19 +1,19 @@ GEM remote: https://rubygems.org/ specs: - childprocess (0.8.0) + childprocess (0.9.0) ffi (~> 1.0, >= 1.0.11) - ffi (1.9.18) - rubyzip (1.2.1) - selenium-webdriver (3.5.0) + ffi (1.9.25) + rubyzip (1.2.2) + selenium-webdriver (3.12.0) childprocess (~> 0.5) - rubyzip (~> 1.0) + rubyzip (~> 1.2) PLATFORMS ruby DEPENDENCIES - selenium-webdriver (= 3.5.0) + selenium-webdriver (= 3.12.0) BUNDLED WITH - 1.16.1 + 1.16.5 From 7230f110441a5a56a07e99ef2804c4c358316b53 Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Fri, 14 Dec 2018 14:28:31 +0000 Subject: [PATCH 056/131] Add videoCapture actual capability for simulators --- .../automation/deviceserver/data/DeviceDTO.kt | 3 ++- .../deviceserver/host/DevicesNode.kt | 3 ++- .../deviceserver/host/SimulatorsNode.kt | 21 ++++++++++--------- .../deviceserver/host/SimulatorsNodeTest.kt | 4 ++-- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceDTO.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceDTO.kt index c5a8f00c..b14ad863 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceDTO.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceDTO.kt @@ -16,5 +16,6 @@ data class DeviceDTO( data class ActualCapabilities( val setLocation: Boolean, - val terminateApp: Boolean + val terminateApp: Boolean, + val videoCapture: Boolean ) \ No newline at end of file diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt index 7b97a9f9..867df7eb 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt @@ -255,7 +255,8 @@ class DevicesNode( last_error = device.lastException?.toDto(), capabilities = ActualCapabilities( setLocation = false, - terminateApp = false + terminateApp = false, + videoCapture = false ) ) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt index a21bca6b..866a7cdb 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt @@ -100,17 +100,18 @@ class SimulatorsNode( private fun simulatorToDTO(device: ISimulator): DeviceDTO { with(device) { return DeviceDTO( - ref, - state, - fbsimctlEndpoint, - wdaEndpoint, - calabashPort, - device.userPorts.toSet(), - device.info, - device.lastError?.toDto(), - capabilities = ActualCapabilities( + ref, + state, + fbsimctlEndpoint, + wdaEndpoint, + calabashPort, + device.userPorts.toSet(), + device.info, + device.lastError?.toDto(), + capabilities = ActualCapabilities( setLocation = true, - terminateApp = true + terminateApp = true, + videoCapture = true ) ) } diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt index 54fd1257..24e364ec 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt @@ -81,7 +81,7 @@ class SimulatorsNodeTest { setOf(1, 2, 3, 37265), DeviceInfo("", "", "", "", ""), null, - ActualCapabilities(true, true) + ActualCapabilities(true, true, true) ) private val expectedDeviceDTOJson = JsonMapper().toJson(expectedDeviceDTO) @@ -274,7 +274,7 @@ class SimulatorsNodeTest { setOf(1,2,3, 37265), DeviceInfo("", "", "", "", ""), null, - ActualCapabilities(true, true) + ActualCapabilities(true, true, true) ))) } From c4f58de50dfddbc2ad6b1b708190ce19e9aae00a Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Fri, 14 Dec 2018 15:55:46 +0000 Subject: [PATCH 057/131] Fix actual capabilities serialization --- .../com/badoo/automation/deviceserver/data/DeviceDTO.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceDTO.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceDTO.kt index b14ad863..d5b35068 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceDTO.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceDTO.kt @@ -1,5 +1,6 @@ package com.badoo.automation.deviceserver.data +import com.fasterxml.jackson.annotation.JsonProperty import java.net.URI data class DeviceDTO( @@ -15,7 +16,12 @@ data class DeviceDTO( ) data class ActualCapabilities( + @JsonProperty("set_location") val setLocation: Boolean, + + @JsonProperty("terminate_app") val terminateApp: Boolean, + + @JsonProperty("video_capture") val videoCapture: Boolean ) \ No newline at end of file From f0d4605d7abcb1605fc7fa19389987ceccf16d1e Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Wed, 19 Dec 2018 15:08:00 +0000 Subject: [PATCH 058/131] Add get crashes since and delete crashes methods --- .../automation/deviceserver/DeviceServer.kt | 15 +++- .../controllers/DevicesController.kt | 12 +++ .../deviceserver/host/DevicesNode.kt | 8 ++ .../deviceserver/host/ISimulatorsNode.kt | 2 + .../deviceserver/host/SimulatorsNode.kt | 8 ++ .../host/management/DeviceManager.kt | 8 ++ .../host/management/IDeviceManager.kt | 2 + .../deviceserver/ios/simulator/ISimulator.kt | 2 + .../deviceserver/ios/simulator/Simulator.kt | 77 ++++++++++++------- 9 files changed, 103 insertions(+), 31 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt index 22d8b1b1..f2d43d78 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt @@ -184,8 +184,19 @@ fun Application.module() { get("endpoint/{port}") { call.respond(devicesController.getEndpointFor(param(call, "ref"), paramInt(call, "port"))) } - get("crashes/last") { - call.respond(devicesController.getLastCrashLog(param(call, "ref"))) + route("crashes") { + get { + val pastMinutes = call.request.queryParameters["pastMinutes"]?.toLong() + + call.respond(devicesController.crashLogs(param(call, "ref"), pastMinutes)) + } + delete { + val rv = devicesController.deleteCrashLogs(param(call, "ref")) + call.respond(mapOf("result" to rv)) + } + get("last") {// this route is going to be deprecated + call.respond(devicesController.getLastCrashLog(param(call, "ref"))) + } } route("data") { post("pull_file") { diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt index a85f1b33..ce6ef89a 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt @@ -50,6 +50,18 @@ class DevicesController(private val deviceManager: IDeviceManager) { return mapOf("endpoint" to deviceManager.getEndpointFor(ref, port).toString()) } + fun crashLogs(ref: DeviceRef, pastMinutes: Long?): List> { + var logs = deviceManager.crashLogs(ref, pastMinutes) + + return logs.map { + mapOf("filename" to it.filename, "content" to it.content) + } + } + + fun deleteCrashLogs(ref: DeviceRef): Boolean { + return deviceManager.deleteCrashLogs(ref) + } + fun getLastCrashLog(ref: DeviceRef): Map { val log = deviceManager.getLastCrashLog(ref) return mapOf("filename" to log.filename, "content" to log.content) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt index 867df7eb..3e3ff218 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt @@ -225,6 +225,14 @@ class DevicesNode( return device.lastCrashLog() ?: CrashLog("", "") } + override fun crashLogs(deviceRef: DeviceRef, pastMinutes: Long?): List { + throw NotImplementedError() + } + + override fun deleteCrashLogs(deviceRef: DeviceRef): Boolean { + return false // crash logs are not supported on devices yet + } + override fun videoRecordingDelete(deviceRef: DeviceRef): Unit = throw(NotImplementedError()) override fun videoRecordingGet(deviceRef: DeviceRef): ByteArray = throw(NotImplementedError()) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt index 48c6b7e6..7a39e38c 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt @@ -13,6 +13,8 @@ interface ISimulatorsNode { fun shake(deviceRef: DeviceRef) fun endpointFor(deviceRef: DeviceRef, port: Int): URL fun lastCrashLog(deviceRef: DeviceRef): CrashLog + fun crashLogs(deviceRef: DeviceRef, pastMinutes: Long?): List + fun deleteCrashLogs(deviceRef: DeviceRef): Boolean fun state(deviceRef: DeviceRef): SimulatorStatusDTO fun videoRecordingDelete(deviceRef: DeviceRef) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt index 866a7cdb..77eaf962 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt @@ -171,6 +171,14 @@ class SimulatorsNode( return getDeviceFor(deviceRef).lastCrashLog() } + override fun crashLogs(deviceRef: DeviceRef, pastMinutes: Long?): List { + return getDeviceFor(deviceRef).crashLogs(pastMinutes) + } + + override fun deleteCrashLogs(deviceRef: DeviceRef): Boolean { + return getDeviceFor(deviceRef).deleteCrashLogs() + } + override fun list(): List { return devicePool.map { simulatorToDTO(it.value) } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt index 04047d1e..8a006066 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt @@ -85,6 +85,14 @@ class DeviceManager( return nodeRegistry.activeDevices.getNodeFor(ref).endpointFor(ref, port) } + override fun crashLogs(ref: DeviceRef, pastMinutes: Long?): List { + return nodeRegistry.activeDevices.getNodeFor(ref).crashLogs(ref, pastMinutes) + } + + override fun deleteCrashLogs(ref: DeviceRef): Boolean { + return nodeRegistry.activeDevices.getNodeFor(ref).deleteCrashLogs(ref) + } + override fun getLastCrashLog(ref: DeviceRef): CrashLog { return nodeRegistry.activeDevices.getNodeFor(ref).lastCrashLog(ref) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt index 85902b0f..0388aa4f 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt @@ -12,6 +12,8 @@ interface IDeviceManager { fun resetAsyncDevice(ref: DeviceRef) fun approveAccess(ref: DeviceRef, bundleId: String) fun getEndpointFor(ref: DeviceRef, port: Int): URL + fun crashLogs(ref: DeviceRef, pastMinutes: Long?): List + fun deleteCrashLogs(ref: DeviceRef): Boolean fun getLastCrashLog(ref: DeviceRef): CrashLog fun shake(ref: DeviceRef) fun startVideo(ref: DeviceRef) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt index 967dee26..e601aecf 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt @@ -29,6 +29,8 @@ interface ISimulator { fun clearSafariCookies(): Map fun shake(): Boolean fun lastCrashLog(): CrashLog + fun crashLogs(pastMinutes: Long?): List fun dataContainer(bundleId: String): DataContainer fun uninstallApplication(bundleId: String) + fun deleteCrashLogs(): Boolean } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt index 11866262..56ccf413 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt @@ -565,52 +565,71 @@ class Simulator ( } //region last crash log + + @Deprecated("Will be removed in favor of crashLogs. Note that crashLogs does not delete old crashes") override fun lastCrashLog(): CrashLog { - val crashLogs = listCrashLogs() + val crashLog = crashLogs(pastMinutes = null).firstOrNull() ?: CrashLog("no crash logs found", "") - if (crashLogs.isEmpty()) { - //FIXME: in Ruby there is a JSON parse exception for not found case - return CrashLog("no crash logs found", "") - } + deleteCrashLogs() // FIXME: this will delete current crash log which makes method non-idempotent - val logFileName = crashLogs.first() + return crashLog + } - if (logFileName.isBlank()) { - return CrashLog("no crash logs found", "") - } + override fun crashLogs(pastMinutes: Long?): List { + var crashLogFiles = listCrashLogs(pastMinutes) - val result = remote.execIgnoringErrors(listOf("cat", logFileName)) + var crashLogs = crashLogFiles.map { + val rv = remote.execIgnoringErrors(listOf("cat", it)) - if (!result.isSuccess) { - throw SimulatorError("Failed to read crash file $logFileName on $remote for $this: $result") - } else { - cleanupOldCrashLogs(crashLogs) // FIXME: this will delete current crash log which makes method non-idempotent + if (rv.isSuccess) { + CrashLog(filename = File(it).name, content = rv.stdOut) + } else { + logger.warn(logMarker, "Cannot read crash log file $it") + null + } } - return CrashLog(filename = File(logFileName).name, content = result.stdOut) + return crashLogs.filterNotNull() } - private fun cleanupOldCrashLogs(crashLogs: List) { - crashLogs.forEach { logFileName -> - try { - remote.execIgnoringErrors(listOf("rm", "-f", "'$logFileName'"), timeOutSeconds = 5) - } catch (e: RuntimeException) { - logger.warn(logMarker, "Failed to delete crash log $logFileName: $e") - } + /** + * Returns list of crashes (file names) since {@code pastMinutes} sorted by most recent first + * @param pastMinutes optional duration to search from, for example Duration.ofMinutes(5) + */ + private fun listCrashLogs(pastMinutes: Long? = null):List { + if (pastMinutes != null && pastMinutes < 0) { + throw IllegalArgumentException("pastMinutes should be positive ") } - } - private fun listCrashLogs(): List { - val cmd = "ls -t \$HOME/Library/Logs/DiagnosticReports/*.crash | xargs grep -l $udid || true" + val predicate = when (pastMinutes) { + null -> "" + else -> "-mmin -$pastMinutes" + } + + // `ls -1t` is for backwards compatibility with lastCrashLog as it expects this method to return most recent crashes first + val cmd = "find \$HOME/Library/Logs/DiagnosticReports $predicate -type f -name \\*.crash -print0 | " + + "xargs -0 grep --files-with-matches --null $udid | " + + "xargs -0 ls -1t" val result = remote.shell(cmd, returnOnFailure = true) if (!result.isSuccess) { SimulatorError("Failed to list crash logs for $this: $result") } return result.stdOut - .split("\n") - .map { it.trim() } - .filter { it.isNotBlank() } + .lineSequence() + .map { it.trim() } + .filter { it.isNotBlank() } + .toList() + } + + override fun deleteCrashLogs(): Boolean { + val cmd = "find \$HOME/Library/Logs/DiagnosticReports -type f -name \\*.crash -print0 | " + + "xargs -0 grep --files-with-matches --null $udid | " + + "xargs -0 rm" + + val result = remote.shell(cmd, returnOnFailure = true) + + return result.isSuccess } override fun dataContainer(bundleId: String): DataContainer { @@ -619,7 +638,7 @@ class Simulator ( //endregion override fun uninstallApplication(bundleId: String) { - logger.debug(logMarker, "Uninstalling aplication $bundleId from Simulator $this") + logger.debug(logMarker, "Uninstalling application $bundleId from Simulator $this") remote.execIgnoringErrors(listOf("xcrun", "simctl", "uninstall", udid, bundleId)) } } From edeb267978820c7f867fa875f791f4f3e20dc4c9 Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Wed, 6 Feb 2019 11:42:50 +0000 Subject: [PATCH 059/131] Allow setting and unsetting permissions individually. Change `/devices/${ref}/permissions` endpoint to allow setting and unsetting permissions individually. Changed request format for the endpoint to take following JSON: ``` { "bundle_id": "test.TestApp", "permissions": { "camera": "unset", "contacts": "no" } } ``` Allowed permission types are: calendar camera contacts homekit microphone photos reminders medialibrary motion health siri speech Allowed permission values are: yes no unset Old request format is deprecated but support was left for backwards compatibility: ``` [{"bundle_id": "test.TestApp"}] ``` The old request will set camera, microphone, photos, contacts permissions for specified bundle ids. Note that support for old request format will be deleted later. --- .../device_provider.rb | 13 ++++ .../ios-device-server-client/permissions.rb | 27 +++++++++ .../ios-device-server-client/remote_device.rb | 8 +++ .../automation/deviceserver/DeviceServer.kt | 2 +- .../automation/deviceserver/JsonMapper.kt | 5 ++ .../controllers/DevicesController.kt | 12 ++++ .../deviceserver/data/AppPermissionsDto.kt | 11 ++++ .../deviceserver/data/PermissionType.kt | 46 +++++++++++++++ .../deviceserver/host/DevicesNode.kt | 4 ++ .../deviceserver/host/ISimulatorsNode.kt | 1 + .../deviceserver/host/SimulatorsNode.kt | 4 ++ .../host/management/DeviceManager.kt | 6 +- .../host/management/IDeviceManager.kt | 1 + .../deviceserver/ios/simulator/ISimulator.kt | 1 + .../deviceserver/ios/simulator/Simulator.kt | 28 ++++----- .../ios/simulator/SimulatorPermissions.kt | 59 +++++++++++++++++++ 16 files changed, 210 insertions(+), 18 deletions(-) create mode 100644 client/rb/lib/ios-device-server-client/permissions.rb create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/AppPermissionsDto.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/PermissionType.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt diff --git a/client/rb/lib/ios-device-server-client/device_provider.rb b/client/rb/lib/ios-device-server-client/device_provider.rb index 106bb0ad..0b7e4cba 100644 --- a/client/rb/lib/ios-device-server-client/device_provider.rb +++ b/client/rb/lib/ios-device-server-client/device_provider.rb @@ -123,6 +123,19 @@ def approve_access(device_ref, bundle_ids) end end + def set_permissions(device_ref, bundle_id:, permissions:) + raise_if_ref_is_empty(device_ref) + + payload = { + bundle_id: bundle_id, + permissions: permissions + } + + with_http do |http| + http.post("/devices/#{device_ref}/permissions", JSON.dump(payload)) + end + end + def video_start(device_ref) raise_if_ref_is_empty(device_ref) diff --git a/client/rb/lib/ios-device-server-client/permissions.rb b/client/rb/lib/ios-device-server-client/permissions.rb new file mode 100644 index 00000000..6f795049 --- /dev/null +++ b/client/rb/lib/ios-device-server-client/permissions.rb @@ -0,0 +1,27 @@ +module IosDeviceServerClient + module Permissions + module Type + CALENDAR = 'calendar'.freeze + CAMERA = 'camera'.freeze + CONTACTS = 'contacts'.freeze + HOME_KIT = 'homekit'.freeze + MICROPHONE = 'microphone'.freeze + PHOTOS = 'photos'.freeze + REMINDERS = 'reminders'.freeze + MEDIA_LIBRARY = 'medialibrary'.freeze + MOTION = 'motion'.freeze + HEALTH = 'health'.freeze + SIRI = 'siri'.freeze + SPEECH = 'speech'.freeze + end + + module Allowed + YES = 'yes'.freeze + NO = 'no'.freeze + ALWAYS = 'always'.freeze + INUSE = 'inuse'.freeze + NEVER = 'never'.freeze + UNSET = 'unset'.freeze + end + end +end diff --git a/client/rb/lib/ios-device-server-client/remote_device.rb b/client/rb/lib/ios-device-server-client/remote_device.rb index 8079bbf8..ae433331 100644 --- a/client/rb/lib/ios-device-server-client/remote_device.rb +++ b/client/rb/lib/ios-device-server-client/remote_device.rb @@ -3,6 +3,7 @@ require_relative 'device_provider' require_relative 'device_client' +require_relative 'permissions' module IosDeviceServerClient module RemoteDeviceError @@ -97,6 +98,13 @@ def approve_access(bundle_ids) @server.approve_access(@device_ref, bundle_ids) end + # @param [String] bundle_id + # @param [Hash] permissions map {Permissions::Type} to {Permissions::Allowed} + def set_permissions(bundle_id:, permissions:) + ensure_ready + @server.set_permissions(@device_ref, bundle_id: bundle_id, permissions: permissions) + end + def app_installed?(bundle_id) ensure_ready @device.app_installed?(bundle_id) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt index f2d43d78..4f151690 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt @@ -179,7 +179,7 @@ fun Application.module() { call.respond(devicesController.deleteReleaseDevice(param(call, "ref"))) } post("permissions") { - call.respond(devicesController.setAccessToCameraAndThings(param(call, "ref"), jsonContent(call))) + call.respond(devicesController.setPermissions(param(call, "ref"), jsonContent(call))) } get("endpoint/{port}") { call.respond(devicesController.getEndpointFor(param(call, "ref"), paramInt(call, "port"))) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/JsonMapper.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/JsonMapper.kt index 26422307..22a4fd28 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/JsonMapper.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/JsonMapper.kt @@ -22,12 +22,17 @@ class JsonMapper { return mapper.readValue(stream, clazz) } + fun fromJson(json: JsonNode, clazz: Class): T { + return mapper.treeToValue(json, clazz) + } + fun readTree(stream: InputStream): JsonNode { return mapper.readTree(stream) ?: throw RuntimeException("Failed to parse json") } inline fun fromJson(string: String): T = fromJson(string, T::class.java) inline fun fromJson(stream: InputStream): T = fromJson(stream, T::class.java) + inline fun fromJson(json: JsonNode): T = fromJson(json, T::class.java) fun toJson(value: T): String { return mapper.writeValueAsString(value) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt index ce6ef89a..795454ad 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt @@ -1,6 +1,7 @@ package com.badoo.automation.deviceserver.controllers import com.badoo.automation.deviceserver.EmptyMap +import com.badoo.automation.deviceserver.JsonMapper import com.badoo.automation.deviceserver.data.* import com.badoo.automation.deviceserver.host.management.IDeviceManager import com.fasterxml.jackson.databind.JsonNode @@ -46,6 +47,17 @@ class DevicesController(private val deviceManager: IDeviceManager) { return happy } + fun setPermissions(ref: DeviceRef, json: JsonNode): EmptyMap { + if (json.isArray) { + return setAccessToCameraAndThings(ref, json) + } + + val permissions = JsonMapper().fromJson(json) + deviceManager.setPermissions(ref, permissions) + + return happy + } + fun getEndpointFor(ref: DeviceRef, port: Int): Map { return mapOf("endpoint" to deviceManager.getEndpointFor(ref, port).toString()) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/AppPermissionsDto.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/AppPermissionsDto.kt new file mode 100644 index 00000000..78ee9b5f --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/AppPermissionsDto.kt @@ -0,0 +1,11 @@ +package com.badoo.automation.deviceserver.data + +import com.fasterxml.jackson.annotation.JsonProperty + +data class AppPermissionsDto( + @JsonProperty("bundle_id") + val bundleId: String, + + @JsonProperty("permissions") + val permissions: PermissionSet +) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/PermissionType.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/PermissionType.kt new file mode 100644 index 00000000..5ee87c8f --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/PermissionType.kt @@ -0,0 +1,46 @@ +package com.badoo.automation.deviceserver.data + +import com.fasterxml.jackson.annotation.JsonValue + +enum class PermissionType(@JsonValue val value: String) { + Calendar("calendar"), + + Camera("camera"), + + Contacts("contacts"), + + HomeKit("homekit"), + + Microphone("microphone"), + + Photos("photos"), + + Reminders("reminders"), + + MediaLibrary("medialibrary"), + + Motion("motion"), + + Health("health"), + + Siri("siri"), + + Speech("speech"); +} + +enum class PermissionAllowed(@JsonValue val value: String) { + Yes("yes"), + + No("no"), + + Always("always"), + + Inuse("inuse"), + + Never("never"), + + Unset("unset"); +} + +class PermissionSet: HashMap() + diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt index 3e3ff218..76b45658 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt @@ -64,6 +64,10 @@ class DevicesNode( throw(NotImplementedError("Approve Access is not supported by physical devices")) } + override fun setPermissions(deviceRef: DeviceRef, appPermissions: AppPermissionsDto) { + throw(NotImplementedError("Set Permissions is not supported by physical devices")) + } + override fun clearSafariCookies(deviceRef: DeviceRef) { throw(NotImplementedError("Clear Safari Cookies is not supported by physical devices")) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt index 7a39e38c..50ce3454 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt @@ -9,6 +9,7 @@ interface ISimulatorsNode { fun resetAsync(deviceRef: DeviceRef) fun approveAccess(deviceRef: DeviceRef, bundleId: String) + fun setPermissions(deviceRef: DeviceRef, appPermissions: AppPermissionsDto) fun clearSafariCookies(deviceRef: DeviceRef) fun shake(deviceRef: DeviceRef) fun endpointFor(deviceRef: DeviceRef, port: Int): URL diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt index 77eaf962..b8a12e5f 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt @@ -123,6 +123,10 @@ class SimulatorsNode( getDeviceFor(deviceRef).approveAccess(bundleId) } + override fun setPermissions(deviceRef: DeviceRef, appPermissions: AppPermissionsDto) { + getDeviceFor(deviceRef).setPermissions(appPermissions.bundleId, appPermissions.permissions) + } + override fun capacityRemaining(desiredCaps: DesiredCapabilities): Float { return (simulatorLimit - count()) * 1F / simulatorLimit } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt index 8a006066..493b7ec3 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt @@ -77,10 +77,14 @@ class DeviceManager( nodeRegistry.activeDevices.getNodeFor(ref).resetAsync(ref) } - override fun approveAccess(ref: DeviceRef, bundleId: String) { + override fun approveAccess(ref: DeviceRef, bundleId: String) { nodeRegistry.activeDevices.getNodeFor(ref).approveAccess(ref, bundleId) } + override fun setPermissions(ref: DeviceRef, permissions: AppPermissionsDto) { + nodeRegistry.activeDevices.getNodeFor(ref).setPermissions(ref, permissions) + } + override fun getEndpointFor(ref: DeviceRef, port: Int): URL { return nodeRegistry.activeDevices.getNodeFor(ref).endpointFor(ref, port) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt index 0388aa4f..ce74f621 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt @@ -11,6 +11,7 @@ interface IDeviceManager { fun clearSafariCookies(ref: DeviceRef) fun resetAsyncDevice(ref: DeviceRef) fun approveAccess(ref: DeviceRef, bundleId: String) + fun setPermissions(ref: DeviceRef, permissions: AppPermissionsDto) fun getEndpointFor(ref: DeviceRef, port: Int): URL fun crashLogs(ref: DeviceRef, pastMinutes: Long?): List fun deleteCrashLogs(ref: DeviceRef): Boolean diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt index e601aecf..d32dff84 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt @@ -25,6 +25,7 @@ interface ISimulator { fun status(): SimulatorStatusDTO fun endpointFor(port: Int): URL fun approveAccess(bundleId: String) + fun setPermissions(bundleId: String, permissions: PermissionSet) fun release(reason: String) fun clearSafariCookies(): Map fun shake(): Boolean diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt index 56ccf413..70c4a09a 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt @@ -466,28 +466,24 @@ class Simulator ( } //region approveAccess + override fun approveAccess(bundleId: String) { - updatePermission(bundleId, "kTCCServiceCamera") - updatePermission(bundleId, "kTCCServiceMicrophone") - updatePermission(bundleId, "kTCCServicePhotos") - updatePermission(bundleId, "kTCCServiceAddressBook") - } + val permissions = SimulatorPermissions(remote, deviceSetPath, this) - private fun updatePermission(bundleId: String, key: String) { - val path = File(deviceSetPath, udid) - val sqlCmd = "sqlite3 ${path.absolutePath}/data/Library/TCC/TCC.db" - val insert = - "$sqlCmd \"INSERT INTO access (service, client, client_type, allowed, prompt_count) VALUES ('$key','$bundleId',0,1,1);\"" - val update = "$sqlCmd \"UPDATE access SET allowed=1 where client='$bundleId' AND service='$key'\"" + permissions.setServicePermission(bundleId, PermissionType.Camera, PermissionAllowed.Yes) + permissions.setServicePermission(bundleId, PermissionType.Microphone, PermissionAllowed.Yes) + permissions.setServicePermission(bundleId, PermissionType.Photos, PermissionAllowed.Yes) + permissions.setServicePermission(bundleId, PermissionType.Contacts, PermissionAllowed.Yes) + } - // FIXME: should we fail if sqlite3 fails (insert or update) or shall we do a separate check for access to be granted? - remote.shell(insert) - val result = remote.shell(update) + override fun setPermissions(bundleId: String, permissions: PermissionSet) { + val manager = SimulatorPermissions(remote, deviceSetPath, this) - if (!result.isSuccess) { - logger.error(logMarker, "Device $this permission update failed ${result.stdErr}") + permissions.forEach { type, allowed -> + manager.setServicePermission(bundleId, type, allowed) } } + //endregion //region release diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt new file mode 100644 index 00000000..b3adb535 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt @@ -0,0 +1,59 @@ +package com.badoo.automation.deviceserver.ios.simulator + +import com.badoo.automation.deviceserver.data.PermissionAllowed +import com.badoo.automation.deviceserver.data.PermissionType +import com.badoo.automation.deviceserver.host.IRemote +import java.io.File + +class SimulatorPermissions( + private val remote: IRemote, + private val deviceSetPath: String, + private val simulator: ISimulator +) { + + private val serviceKeys = mapOf( + PermissionType.Calendar to "kTCCServiceCalendar", + PermissionType.Camera to "kTCCServiceCamera", + PermissionType.Contacts to "kTCCServiceAddressBook", + PermissionType.HomeKit to "kTCCServiceWillow", + PermissionType.Microphone to "kTCCServiceMicrophone", + PermissionType.Photos to "kTCCServicePhotos", + PermissionType.Reminders to "kTCCServiceReminders", + PermissionType.MediaLibrary to "kTCCServiceMediaLibrary", + PermissionType.Motion to "kTCCServiceMotion", + PermissionType.Health to "kTCCServiceMSO", + PermissionType.Siri to "kTCCServiceSiri", + PermissionType.Speech to "kTCCServiceSpeechRecognition" + ) + + fun setServicePermission(bundleId: String, type: PermissionType, value: PermissionAllowed) { + val key = serviceKeys[type] + ?: throw(IllegalArgumentException("Permission $type is not a service type")) + + val path = File(deviceSetPath, simulator.udid) + val sqlCmd = "sqlite3 ${path.absolutePath}/data/Library/TCC/TCC.db" + + val delete = "$sqlCmd \"DELETE FROM access WHERE service = '$key' AND client = '$bundleId' AND client_type = 0;\"" + + if (!remote.shell(delete).isSuccess) { + throw(SimulatorError("Failed to unset type $type for $this ")) + } + + if (value == PermissionAllowed.Unset) { + return + } + + val allowed = when (value) { + PermissionAllowed.Yes -> 1 + PermissionAllowed.No -> 0 + else -> throw(IllegalArgumentException("Unsupported value $value for type $type")) + } + + val replace = + "$sqlCmd \"REPLACE INTO access (service, client, client_type, allowed, prompt_count) VALUES ('$key','$bundleId',0,$allowed,1);\"" + + if (!remote.shell(replace).isSuccess) { + throw(SimulatorError("Failed to update type $type for $this")) + } + } +} \ No newline at end of file From 406f8de580c97ce3602929b63914444c2bb8686b Mon Sep 17 00:00:00 2001 From: Max Oleynik <33117161+MaxCiv@users.noreply.github.com> Date: Wed, 20 Feb 2019 11:41:25 +0300 Subject: [PATCH 060/131] Set environment variables (#34) Add ability to set environment variables on simulator --- .../ios-device-server-client/device_provider.rb | 16 ++++++++++++++++ .../ios-device-server-client/remote_device.rb | 4 ++++ .../automation/deviceserver/DeviceServer.kt | 5 +++++ .../controllers/DevicesController.kt | 5 +++++ .../automation/deviceserver/host/DevicesNode.kt | 4 ++++ .../deviceserver/host/ISimulatorsNode.kt | 1 + .../deviceserver/host/SimulatorsNode.kt | 6 +++++- .../host/management/DeviceManager.kt | 4 ++++ .../host/management/IDeviceManager.kt | 1 + .../deviceserver/ios/simulator/ISimulator.kt | 1 + .../deviceserver/ios/simulator/Simulator.kt | 15 +++++++++++++++ .../controllers/DevicesControllerTest.kt | 13 ++++++++++++- .../deviceserver/host/SimulatorsNodeTest.kt | 9 ++++++++- .../deviceserver/ios/DeviceManagerTest.kt | 8 ++++++++ 14 files changed, 89 insertions(+), 3 deletions(-) diff --git a/client/rb/lib/ios-device-server-client/device_provider.rb b/client/rb/lib/ios-device-server-client/device_provider.rb index 0b7e4cba..76816789 100644 --- a/client/rb/lib/ios-device-server-client/device_provider.rb +++ b/client/rb/lib/ios-device-server-client/device_provider.rb @@ -186,6 +186,22 @@ def release_user_devices end end + def set_env(device_ref, environment_variables) + raise_if_ref_is_empty(device_ref) + + with_http do |http| + headers = { 'Content-Type' => 'application/json' } + + if credentials + headers['Authorization'] = auth_header_value + end + + request = Net::HTTP::Post.new("/devices/#{device_ref}/environment", headers) + request.body = JSON.dump(environment_variables) + return http.request(request) + end + end + # @note internal use only def list with_http do |http| diff --git a/client/rb/lib/ios-device-server-client/remote_device.rb b/client/rb/lib/ios-device-server-client/remote_device.rb index ae433331..6ca8d0d8 100644 --- a/client/rb/lib/ios-device-server-client/remote_device.rb +++ b/client/rb/lib/ios-device-server-client/remote_device.rb @@ -90,6 +90,10 @@ def clear_safari_cookies @server.clear_safari_cookies(@device_ref) end + def set_env(environment_variables) + ensure_ready + return @server.set_env(@device_ref, environment_variables) + end # endregion # region: app management diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt index 4f151690..1d87d0d0 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt @@ -233,6 +233,11 @@ fun Application.module() { get("state") { call.respond(devicesController.getDeviceState(param(call, "ref"))) } + post("environment") { + val ref = param(call, "ref") + val environmentVariables = jsonContent>(call) + call.respond(devicesController.setEnvironmentVariables(ref, environmentVariables)) + } } } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt index 795454ad..a16c0d3f 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt @@ -121,4 +121,9 @@ class DevicesController(private val deviceManager: IDeviceManager) { deviceManager.uninstallApplication(ref, bundleId) return happy } + + fun setEnvironmentVariables(ref: DeviceRef, environmentVariables: Map): EmptyMap { + deviceManager.setEnvironmentVariables(ref, environmentVariables) + return happy + } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt index 76b45658..85316ffc 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt @@ -335,4 +335,8 @@ class DevicesNode( // single instance of server on node is implied, so we can kill all simulators and fbsimctl processes remote.execIgnoringErrors(listOf("pkill", "-9", "/usr/local/bin/fbsimctl")) } + + override fun setEnvironmentVariables(deviceRef: DeviceRef, envs: Map) { + throw(NotImplementedError("Setting environment variables is not supported by physical devices")) + } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt index 50ce3454..52a25198 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt @@ -38,4 +38,5 @@ interface ISimulatorsNode { fun createDeviceAsync(desiredCaps: DesiredCapabilities): DeviceDTO fun dispose() fun uninstallApplication(deviceRef: DeviceRef, bundleId: String) + fun setEnvironmentVariables(deviceRef: DeviceRef, envs: Map) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt index b8a12e5f..116f1898 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt @@ -242,7 +242,11 @@ class SimulatorsNode( getDeviceFor(deviceRef).uninstallApplication(bundleId) } + override fun setEnvironmentVariables(deviceRef: DeviceRef, envs: Map) { + getDeviceFor(deviceRef).setEnvironmentVariables(envs) + } + override fun toString(): String { return "${javaClass.simpleName} at $remoteAddress" } -} \ No newline at end of file +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt index 493b7ec3..954ec304 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt @@ -168,4 +168,8 @@ class DeviceManager( override fun pullFile(ref: DeviceRef, dataPath: DataPath): ByteArray { return nodeRegistry.activeDevices.getNodeFor(ref).pullFile(ref, dataPath) } + + override fun setEnvironmentVariables(ref: DeviceRef, envs: Map) { + nodeRegistry.activeDevices.getNodeFor(ref).setEnvironmentVariables(ref, envs) + } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt index ce74f621..dbaf33e8 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt @@ -31,4 +31,5 @@ interface IDeviceManager { fun listFiles(ref: DeviceRef, dataPath: DataPath): List fun pullFile(ref: DeviceRef, dataPath: DataPath): ByteArray fun uninstallApplication(ref: String, bundleId: String) + fun setEnvironmentVariables(ref: DeviceRef, envs: Map) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt index d32dff84..bcda57ca 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt @@ -34,4 +34,5 @@ interface ISimulator { fun dataContainer(bundleId: String): DataContainer fun uninstallApplication(bundleId: String) fun deleteCrashLogs(): Boolean + fun setEnvironmentVariables(envs: Map) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt index 70c4a09a..ccadf195 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt @@ -2,6 +2,7 @@ package com.badoo.automation.deviceserver.ios.simulator import com.badoo.automation.deviceserver.LogMarkers import com.badoo.automation.deviceserver.WaitTimeoutError +import com.badoo.automation.deviceserver.command.ShellUtils import com.badoo.automation.deviceserver.data.* import com.badoo.automation.deviceserver.host.IRemote import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDeviceState @@ -637,4 +638,18 @@ class Simulator ( logger.debug(logMarker, "Uninstalling application $bundleId from Simulator $this") remote.execIgnoringErrors(listOf("xcrun", "simctl", "uninstall", udid, bundleId)) } + + override fun setEnvironmentVariables(envs: Map) { + if (envs.isEmpty()) { + logger.debug(logMarker, "Passed empty list of environment variables for Simulator $this") + return + } + + logger.debug(logMarker, "Setting environment variables $envs for Simulator $this") + val envsArguments = mutableListOf() + envs.keys.forEach { + envsArguments.addAll(listOf(it, ShellUtils.escape(envs.getValue(it)))) + } + remote.shell("xcrun simctl spawn $udid launchctl setenv ${envsArguments.joinToString(" ")}") + } } diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/DevicesControllerTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/DevicesControllerTest.kt index b669bde8..e2431dd2 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/DevicesControllerTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/DevicesControllerTest.kt @@ -209,5 +209,16 @@ class DevicesControllerTest { val actualResult = deviceServer.getDeviceState(deviceRef) assertThat(actualResult, equalTo(expectedState)) } -} + @Test + fun setEnvironmentVariables() { + val environmentVariables = mapOf( + "ENV_NAME1" to "ENV_VALUE1", + "ENV_NAME2" to "ENV_VALUE2" + ) + val actualResult = deviceServer.setEnvironmentVariables(deviceRef, environmentVariables) + + verify(deviceManager).setEnvironmentVariables(deviceRef, environmentVariables) + assertThat(actualResult, equalTo(happyEmpty)) + } +} diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt index 24e364ec..f2bf0be9 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt @@ -385,7 +385,14 @@ class SimulatorsNodeTest { simulatorsNode.videoRecordingStop(ref1) verify(videoRecorderMock).stop() - } + @Test + fun setEnvironmentVariables() { + createDeviceForTest() + + simulatorsNode.setEnvironmentVariables(ref1, mapOf()) + + verify(simulatorMock).setEnvironmentVariables(mapOf()) + } } diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/DeviceManagerTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/DeviceManagerTest.kt index df6a03ae..31be6ba8 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/DeviceManagerTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/DeviceManagerTest.kt @@ -195,4 +195,12 @@ class DeviceManagerTest { deviceManager.launchAutoReleaseLoop() verify(autoreleaseLooper).autoreleaseLoop(deviceManager) } + + @Test + fun setEnvironmentVariables() { + withDeviceOnHost(hostTwo) { + deviceManager.setEnvironmentVariables(ref, mapOf()) + verify(hostTwo).setEnvironmentVariables(ref, mapOf()) + } + } } From bd54e9421c823f7c6005eedce5cf92ee3f8ada6c Mon Sep 17 00:00:00 2001 From: Max Oleynik <33117161+MaxCiv@users.noreply.github.com> Date: Wed, 20 Feb 2019 12:01:03 +0300 Subject: [PATCH 061/131] Add useWDA property (#33) Expose useWDA capability to optionally skip WebDriver initialization --- .../deviceserver/data/DesiredCapabilities.kt | 7 ++++++- .../deviceserver/host/ISimulatorFactory.kt | 6 ++++-- .../deviceserver/host/SimulatorsNode.kt | 3 ++- .../deviceserver/ios/simulator/Simulator.kt | 8 +++++--- .../data/DesiredCapabilitiesTest.kt | 18 +++++++++++++++++- .../deviceserver/host/SimulatorsNodeTest.kt | 6 ++++-- 6 files changed, 38 insertions(+), 10 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DesiredCapabilities.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DesiredCapabilities.kt index 139cfc82..83d9d5af 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DesiredCapabilities.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DesiredCapabilities.kt @@ -1,10 +1,15 @@ package com.badoo.automation.deviceserver.data +import com.fasterxml.jackson.annotation.JsonProperty + data class DesiredCapabilities( val udid: String?, val model: String?, val os: String?, val headless: Boolean = true, val existing: Boolean = true, - val arch: String? = null + val arch: String? = null, + + @JsonProperty("use_wda") + val useWda: Boolean = true ) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorFactory.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorFactory.kt index 522e17ef..e0f811ee 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorFactory.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorFactory.kt @@ -19,8 +19,10 @@ interface ISimulatorFactory { wdaRunnerXctest: File, concurrentBoot: ThreadPoolDispatcher, headless: Boolean, + useWda: Boolean, fbsimctlSubject: String ): ISimulator { - return Simulator(ref, remote, DeviceInfo(fbdev), ports, deviceSetPath, wdaRunnerXctest, concurrentBoot, headless, fbsimctlSubject) + return Simulator(ref, remote, DeviceInfo(fbdev), ports, deviceSetPath, wdaRunnerXctest, concurrentBoot, + headless, useWda, fbsimctlSubject) } -} \ No newline at end of file +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt index 116f1898..ac23abac 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt @@ -87,7 +87,8 @@ class SimulatorsNode( logger.debug(simLogMarker, "Will create simulator $ref") - val simulator = simulatorFactory.newSimulator(ref, remote, fbSimctlDevice, ports, deviceSetPath, wdaRunnerXctest, concurrentBoot, desiredCaps.headless, fbSimctlDevice.toString()) + val simulator = simulatorFactory.newSimulator(ref, remote, fbSimctlDevice, ports, deviceSetPath, + wdaRunnerXctest, concurrentBoot, desiredCaps.headless, desiredCaps.useWda, fbSimctlDevice.toString()) simulator.prepareAsync() devicePool[ref] = simulator diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt index ccadf195..960954b5 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt @@ -30,7 +30,6 @@ import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock import kotlin.system.measureTimeMillis - class Simulator ( private val deviceRef: DeviceRef, private val remote: IRemote, @@ -40,6 +39,7 @@ class Simulator ( wdaRunnerXctest: File, private val concurrentBootsPool: ThreadPoolDispatcher, headless: Boolean, + private val useWda: Boolean, override val fbsimctlSubject: String ) : ISimulator { @@ -120,7 +120,9 @@ class Simulator ( logTiming("simulator boot") { boot() } - logTiming("starting WebDriverAgent") { startWdaWithRetry() } + if (useWda) { + logTiming("starting WebDriverAgent") { startWdaWithRetry() } + } logger.info(logMarker, "Finished preparing $this") deviceState = DeviceState.CREATED @@ -413,7 +415,7 @@ class Simulator ( runBlocking { val isFbsimctlHealthyTask = async { fbsimctlProc.isHealthy() } - val isWdaHealthyTask = async { wdaProc.isHealthy() } + val isWdaHealthyTask = async { if (useWda) wdaProc.isHealthy() else true } val isFbsimctlHealthy: Boolean = isFbsimctlHealthyTask.await() val isWdaHealthy: Boolean = isWdaHealthyTask.await() diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/DesiredCapabilitiesTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/DesiredCapabilitiesTest.kt index 6f261fb8..3ff034c9 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/DesiredCapabilitiesTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/DesiredCapabilitiesTest.kt @@ -60,4 +60,20 @@ class DesiredCapabilitiesTest { assertEquals(DesiredCapabilities(null, null, null, true, true), actual) } -} \ No newline at end of file + + @Test + fun fromJsonParsesUseWdaFalseCapability() { + val json = """{"use_wda": false}""" + val actual = fromJson(json) + + assertEquals(DesiredCapabilities(null, null, null, true, true, useWda = false), actual) + } + + @Test + fun fromJsonParsesUseWdaBoolAsTextCapability() { + val json = """{"use_wda": "false"}""" + val actual = fromJson(json) + + assertEquals(DesiredCapabilities(null, null, null, true, useWda = false), actual) + } +} diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt index f2bf0be9..16778c79 100644 --- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt @@ -13,7 +13,8 @@ import com.nhaarman.mockito_kotlin.* import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.sameInstance import org.hamcrest.MatcherAssert.assertThat -import org.junit.Assert.* +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Test import java.io.File import java.net.URI @@ -121,6 +122,7 @@ class SimulatorsNodeTest { eq(File("some/file/from/wdaPathProc")), any(), eq(false), + eq(false), eq("FBSimctlDevice(arch=Arch, state=State, model=Model, name=Name, udid=Udid1, os=Os)") ) verify(simulatorMock).prepareAsync() @@ -151,7 +153,7 @@ class SimulatorsNodeTest { fbsimmock = fbsimmock.thenReturn(pair.second) } - var simfac = whenever(simulatorFactory.newSimulator(any(), any(), any(), any(), any(), any(), any(), any(), any())) + var simfac = whenever(simulatorFactory.newSimulator(any(), any(), any(), any(), any(), any(), any(), any(), any(), any())) simulatorMocks.forEach { pair -> simfac = simfac.thenReturn(pair.first) } From 3dbb746aba7256c5e3163c102141444e0ca883a3 Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Mon, 18 Feb 2019 18:39:43 +0000 Subject: [PATCH 062/131] Check that WebDriverAgent is deployed to remote host --- .../com/badoo/automation/deviceserver/host/DevicesNode.kt | 5 +++++ .../badoo/automation/deviceserver/host/ISimulatorsNode.kt | 1 + .../com/badoo/automation/deviceserver/host/SimulatorsNode.kt | 3 +++ .../deviceserver/host/management/ISimulatorHostChecker.kt | 4 ++++ .../automation/deviceserver/host/management/NodeWrapper.kt | 2 +- 5 files changed, 14 insertions(+), 1 deletion(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt index 85316ffc..f3a81eb7 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt @@ -35,6 +35,11 @@ class DevicesNode( ) ) + private val isWebDriverAgentDeployed = remote.execIgnoringErrors(listOf("test", "-d", remoteWdaBundleRoot.absolutePath)).isSuccess + + override val isNodePrepared: Boolean + get() = remote.isLocalhost() || isWebDriverAgentDeployed + private val deviceRegistrationInterval = Duration.ofMinutes(1) override val remoteAddress: String get() = remote.hostName diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt index 52a25198..dff28d01 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt @@ -29,6 +29,7 @@ interface ISimulatorsNode { val remoteAddress: String fun isReachable(): Boolean fun prepareNode() + val isNodePrepared: Boolean fun count(): Int fun list(): List fun deleteRelease(deviceRef: DeviceRef, reason: String): Boolean diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt index ac23abac..a830c435 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt @@ -36,6 +36,9 @@ class SimulatorsNode( HOSTNAME to remote.hostName )) + override val isNodePrepared: Boolean + get() = remote.isLocalhost() || hostChecker.isWdaBundleDeployed + override fun prepareNode() { logger.info(logMarker, "Preparing node ${remote.hostName}") hostChecker.checkPrerequisites() diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt index 9b396763..421c9de2 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt @@ -17,6 +17,7 @@ interface ISimulatorHostChecker { fun setupHost() fun killDiskCleanupThread() fun copyWdaBundleToHost() + val isWdaBundleDeployed: Boolean } class SimulatorHostChecker( @@ -26,6 +27,9 @@ class SimulatorHostChecker( private val remoteWdaBundleRoot: File, private val fbsimctlVersion: String ) : ISimulatorHostChecker { + override val isWdaBundleDeployed: Boolean + get() = remote.execIgnoringErrors(listOf("test", "-d", remoteWdaBundleRoot.absolutePath)).isSuccess + private val logger = LoggerFactory.getLogger(javaClass.simpleName) private val logMarker = MapEntriesAppendingMarker(mapOf( LogMarkers.HOSTNAME to remote.hostName diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeWrapper.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeWrapper.kt index f3ab8199..aff30bfc 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeWrapper.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeWrapper.kt @@ -34,7 +34,7 @@ class NodeWrapper( var lastError: Exception? = null - fun isAlive(): Boolean = isStarted && node.isReachable() + fun isAlive(): Boolean = isStarted && node.isReachable() && node.isNodePrepared override fun toString(): String = "NodeWrapper for ${config.host}" From abb976b7fb64f57dcda9d077aea10a3e4b855479 Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Thu, 21 Feb 2019 21:14:11 +0000 Subject: [PATCH 063/131] Shuffle list of nodes randomly --- .../automation/deviceserver/host/management/NodeRegistry.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt index 14b8e074..96d84514 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt @@ -73,6 +73,7 @@ class NodeRegistry(val activeDevices: ActiveDevices = ActiveDevices()) { val node: ISimulatorsNode = getAlive() .map { wrapper -> wrapper.node } + .shuffled() .maxBy { node -> node.capacityRemaining(desiredCapabilities) } ?: throw NoAliveNodesException("No alive nodes are available to create device at the moment") From b662489cd09848817a95c2c752033c096c5f5675 Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Thu, 21 Feb 2019 16:26:23 +0000 Subject: [PATCH 064/131] Add location and notifications permissons wix/AppleSimulatorUtils 0.6.4 or higher is required --- .../ios-device-server-client/permissions.rb | 7 ++- .../deviceserver/data/PermissionType.kt | 22 ++++--- .../deviceserver/ios/simulator/Simulator.kt | 2 +- .../ios/simulator/SimulatorPermissions.kt | 59 +++++++++++++++++-- .../deviceserver/data/PermissionSetTest.kt | 31 ++++++++++ 5 files changed, 105 insertions(+), 16 deletions(-) create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/PermissionSetTest.kt diff --git a/client/rb/lib/ios-device-server-client/permissions.rb b/client/rb/lib/ios-device-server-client/permissions.rb index 6f795049..91e5d369 100644 --- a/client/rb/lib/ios-device-server-client/permissions.rb +++ b/client/rb/lib/ios-device-server-client/permissions.rb @@ -4,13 +4,14 @@ module Type CALENDAR = 'calendar'.freeze CAMERA = 'camera'.freeze CONTACTS = 'contacts'.freeze + HEALTH = 'health'.freeze HOME_KIT = 'homekit'.freeze + LOCATION = 'location'.freeze + MEDIA_LIBRARY = 'medialibrary'.freeze MICROPHONE = 'microphone'.freeze + MOTION = 'motion'.freeze PHOTOS = 'photos'.freeze REMINDERS = 'reminders'.freeze - MEDIA_LIBRARY = 'medialibrary'.freeze - MOTION = 'motion'.freeze - HEALTH = 'health'.freeze SIRI = 'siri'.freeze SPEECH = 'speech'.freeze end diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/PermissionType.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/PermissionType.kt index 5ee87c8f..dec34863 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/PermissionType.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/PermissionType.kt @@ -3,29 +3,37 @@ package com.badoo.automation.deviceserver.data import com.fasterxml.jackson.annotation.JsonValue enum class PermissionType(@JsonValue val value: String) { + // region yes/no/unset permissions Calendar("calendar"), Camera("camera"), Contacts("contacts"), + Health("health"), + HomeKit("homekit"), - Microphone("microphone"), + MediaLibrary("medialibrary"), - Photos("photos"), + Microphone("microphone"), - Reminders("reminders"), + Motion("motion"), - MediaLibrary("medialibrary"), + Notifications("notifications"), - Motion("motion"), + Photos("photos"), - Health("health"), + Reminders("reminders"), Siri("siri"), - Speech("speech"); + Speech("speech"), + // endregion + + // region always/inuse/never/unset permissions + Location("location"), + // endregion } enum class PermissionAllowed(@JsonValue val value: String) { diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt index 960954b5..87cd017f 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt @@ -483,7 +483,7 @@ class Simulator ( val manager = SimulatorPermissions(remote, deviceSetPath, this) permissions.forEach { type, allowed -> - manager.setServicePermission(bundleId, type, allowed) + manager.setPermission(bundleId, type, allowed) } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt index b3adb535..d9966ed4 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt @@ -26,7 +26,15 @@ class SimulatorPermissions( PermissionType.Speech to "kTCCServiceSpeechRecognition" ) - fun setServicePermission(bundleId: String, type: PermissionType, value: PermissionAllowed) { + fun setPermission(bundleId: String, type: PermissionType, value: PermissionAllowed) { + when (type) { + PermissionType.Location -> setLocationPermission(bundleId, value) + PermissionType.Notifications -> setNotificationsPermission(bundleId, value) + else -> setServicePermission(bundleId, type, value) + } + } + + fun setServicePermission(bundleId: String, type: PermissionType, allowed: PermissionAllowed) { val key = serviceKeys[type] ?: throw(IllegalArgumentException("Permission $type is not a service type")) @@ -39,21 +47,62 @@ class SimulatorPermissions( throw(SimulatorError("Failed to unset type $type for $this ")) } - if (value == PermissionAllowed.Unset) { + if (allowed == PermissionAllowed.Unset) { return } - val allowed = when (value) { + val value = when (allowed) { PermissionAllowed.Yes -> 1 PermissionAllowed.No -> 0 - else -> throw(IllegalArgumentException("Unsupported value $value for type $type")) + else -> throw IllegalArgumentException("Unsupported value $allowed for type $type") } val replace = - "$sqlCmd \"REPLACE INTO access (service, client, client_type, allowed, prompt_count) VALUES ('$key','$bundleId',0,$allowed,1);\"" + "$sqlCmd \"REPLACE INTO access (service, client, client_type, allowed, prompt_count) VALUES ('$key','$bundleId',0,$value,1);\"" if (!remote.shell(replace).isSuccess) { throw(SimulatorError("Failed to update type $type for $this")) } } + + + private val appleSimUtils = "/usr/local/bin/applesimutils" + + private fun setNotificationsPermission(bundleId: String, allowed: PermissionAllowed) { + val value = when (allowed) { + PermissionAllowed.Yes -> "YES" + PermissionAllowed.No -> "NO" + PermissionAllowed.Unset -> "unset" + else -> throw IllegalArgumentException("Unsupported value $allowed for type ${PermissionType.Location}") + } + + val cmd = listOf(appleSimUtils, "--byId", simulator.udid, "--bundle", bundleId, "--setPermissions", "notifications=$value") + + val rv = remote.execIgnoringErrors(cmd) + + if (!rv.isSuccess){ + throw RuntimeException("Could not set notifications permission: $rv") + } + } + + private fun setLocationPermission(bundleId: String, allowed: PermissionAllowed) { + // map to applesimutils values + val value = when (allowed) { + PermissionAllowed.Always -> "always" + PermissionAllowed.Inuse -> "inuse" + PermissionAllowed.Never -> "never" + PermissionAllowed.Unset -> "unset" + else -> throw IllegalArgumentException("Unsupported value $allowed for type ${PermissionType.Location}") + } + + val cmd = listOf(appleSimUtils, "--byId", simulator.udid, "--bundle", bundleId, "--setPermissions", "location=$value") + + // Without PATH applesimutils will crash with 'NSInvalidArgumentException', reason: 'must provide a launch path' + val env = mapOf("PATH" to "/usr/bin") + val rv = remote.execIgnoringErrors(cmd, env) + + if (!rv.isSuccess){ + throw RuntimeException("Could not set location permission: $rv") + } + } } \ No newline at end of file diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/PermissionSetTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/PermissionSetTest.kt new file mode 100644 index 00000000..16ba9bea --- /dev/null +++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/data/PermissionSetTest.kt @@ -0,0 +1,31 @@ +package com.badoo.automation.deviceserver.data + +import com.badoo.automation.deviceserver.JsonMapper +import org.junit.Assert.* +import org.junit.Test + +class PermissionSetTest { + private fun fromJson(json: String): PermissionSet { + return JsonMapper().fromJson(json) + } + + @Test + fun fromJsonParsesPermissionSet() { + val json = """ + { + "location": "always", + "calendar": "yes", + "homekit": "unset" + } + """ + val actual = fromJson(json) + + val expected = PermissionSet() + + expected[PermissionType.Calendar] = PermissionAllowed.Yes + expected[PermissionType.Location] = PermissionAllowed.Always + expected[PermissionType.HomeKit] = PermissionAllowed.Unset + + assertEquals(expected, actual) + } +} From 7fbbbfdc637c3157f8f7defaa5bd36b80f50db1b Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Mon, 25 Feb 2019 14:26:26 +0000 Subject: [PATCH 065/131] Disallow notifications permission setting Setting notifications permission requires SpringBoard restart, which takes about 10 seconds and breaks WebDriverAgent. Requiring WebDriver to be restarted too, making this too time prohibitive. It is easier to confirm permission dialogue from tests. --- .../ios/simulator/SimulatorPermissions.kt | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt index d9966ed4..a2faf5f3 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt @@ -65,24 +65,13 @@ class SimulatorPermissions( } } - private val appleSimUtils = "/usr/local/bin/applesimutils" + @Suppress("UNUSED_PARAMETER") private fun setNotificationsPermission(bundleId: String, allowed: PermissionAllowed) { - val value = when (allowed) { - PermissionAllowed.Yes -> "YES" - PermissionAllowed.No -> "NO" - PermissionAllowed.Unset -> "unset" - else -> throw IllegalArgumentException("Unsupported value $allowed for type ${PermissionType.Location}") - } - - val cmd = listOf(appleSimUtils, "--byId", simulator.udid, "--bundle", bundleId, "--setPermissions", "notifications=$value") - - val rv = remote.execIgnoringErrors(cmd) - - if (!rv.isSuccess){ - throw RuntimeException("Could not set notifications permission: $rv") - } + // Setting notifications permission is disallowed as it results in SpringBoard restart + // which breaks WebDriverAgent. Restarting SpringBoard and WebDriverAgent will take too much time. + throw RuntimeException("Setting notifications permission is not supported") } private fun setLocationPermission(bundleId: String, allowed: PermissionAllowed) { From 0b78b5fccc1e582eb2ceae38d2835b8ce1472ace Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Mon, 25 Feb 2019 15:42:13 +0000 Subject: [PATCH 066/131] Add Brewfile to describe and install dependencies Use ``` brew update brew bundle ``` We have separate base and devices Brewfiles. Installing devices dependencies might take a while, if you don't need to work with devices, skip it and install base dependencies only instead by running: ``` brew bundle --file=homebrew/Brewfile ``` To install devices dependencies later use ``` brew bundle --file=homebrew/Brewfile.devices ``` Some of the dependencies needed for devices have outdated stable releases and have to be installed from the latest head. Brew install formula --HEAD does not check if head has moved, i.e. after installing Brewfile you might still need to run ``` brew upgrade usbmuxd libimobiledevice --fetch-HEAD ``` --- Brewfile | 6 ++++++ homebrew/Brewfile | 16 ++++++++++++++++ homebrew/Brewfile.devices | 14 ++++++++++++++ 3 files changed, 36 insertions(+) create mode 100644 Brewfile create mode 100644 homebrew/Brewfile create mode 100644 homebrew/Brewfile.devices diff --git a/Brewfile b/Brewfile new file mode 100644 index 00000000..40131d0d --- /dev/null +++ b/Brewfile @@ -0,0 +1,6 @@ +[ + "homebrew/Brewfile", + "homebrew/Brewfile.devices", +].each do |f| + eval(File.new(f).read) +end diff --git a/homebrew/Brewfile b/homebrew/Brewfile new file mode 100644 index 00000000..48affd2b --- /dev/null +++ b/homebrew/Brewfile @@ -0,0 +1,16 @@ +tap "homebrew/bundle" +tap "homebrew/core" + +tap "wix/brew" +tap "facebook/fb" + +# Play, record, convert, and stream audio and video +brew "ffmpeg" + +# Apple simulator utilities +brew "wix/brew/applesimutils" + +# TODO: We need a custom formula/tap for fbsimctl as it has to be built from a fork +# as we have to build it from https://github.com/badoo/FBSimulatorControl head +# A Powerful Command Line for Managing iOS Simulators +# brew "facebook/fb/fbsimctl", args: ["HEAD"] diff --git a/homebrew/Brewfile.devices b/homebrew/Brewfile.devices new file mode 100644 index 00000000..5051c9e6 --- /dev/null +++ b/homebrew/Brewfile.devices @@ -0,0 +1,14 @@ +tap "homebrew/bundle" +tap "homebrew/core" + +# Both usbmuxd and libimobiledevice should be installed from the latest HEAD +# but if they are already installed, brew will not auto-fetch new head + +# USB multiplexor daemon for iPhone and iPod Touch devices +brew "usbmuxd", args: ["HEAD"] + +# Library to communicate with iOS devices natively +brew "libimobiledevice", args: ["HEAD"] + +# netcat on steroids +brew "socat" From 220b3fcd8d65c16f4f38b862e2db0f47c601ab7b Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Tue, 26 Feb 2019 10:04:40 +0000 Subject: [PATCH 067/131] Consistent naming for permissions value parameter --- .../deviceserver/ios/simulator/SimulatorPermissions.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt index a2faf5f3..1d6f2f68 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt @@ -26,11 +26,11 @@ class SimulatorPermissions( PermissionType.Speech to "kTCCServiceSpeechRecognition" ) - fun setPermission(bundleId: String, type: PermissionType, value: PermissionAllowed) { + fun setPermission(bundleId: String, type: PermissionType, allowed: PermissionAllowed) { when (type) { - PermissionType.Location -> setLocationPermission(bundleId, value) - PermissionType.Notifications -> setNotificationsPermission(bundleId, value) - else -> setServicePermission(bundleId, type, value) + PermissionType.Location -> setLocationPermission(bundleId, allowed) + PermissionType.Notifications -> setNotificationsPermission(bundleId, allowed) + else -> setServicePermission(bundleId, type, allowed) } } From 9554a388d396d407c3e4b0b4682ff89e43c3f3bc Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Tue, 26 Feb 2019 11:07:23 +0000 Subject: [PATCH 068/131] Update readme --- README.md | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 74ae54ad..f9c57586 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,34 @@ A server for managing, booting, and controlling simulators and devices on remote export JAVA_HOME=$(/usr/libexec/java_home -v 10 -F) ``` -### Physical Devices +### Dependencies -To use physical devices extra dependencies are required: +Device Server uses fbsimctl, libimobiledevice and other tools under the hood. + +To install dependencies run: + +```bash +brew update +brew bundle ``` -brew install libimobiledevice --HEAD -brew install socat + +#### Simulators + +It might take some time to build dependencies used for physical devices. You can skip it by running following instead: + +```bash +brew update +brew bundle --file="homebrew/Brewfile" +``` + +#### Devices + +Some of the dependencies needed for physical devices have outdated stable releases and have to be installed from the latest head. + +After installing Brewfile you might still need to run + +```bash +brew upgrade usbmuxd libimobiledevice --fetch-HEAD ``` ## Usage From 129575d7a9651c193e23a518214dc30a9d59ec63 Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Wed, 6 Mar 2019 11:49:56 +0000 Subject: [PATCH 069/131] Do not fail device creation if untrusted device is connected --- .../host/management/DesiredCapabilitiesMatcher.kt | 7 ++++++- .../deviceserver/host/management/RuntimeVersion.kt | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DesiredCapabilitiesMatcher.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DesiredCapabilitiesMatcher.kt index 0b61bb30..3cfc0744 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DesiredCapabilitiesMatcher.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DesiredCapabilitiesMatcher.kt @@ -29,7 +29,12 @@ class DesiredCapabilitiesMatcher : IDesiredCapabilitiesMatcher { internal fun isRuntimeMatch(desired: String, actual: String): Boolean { val desiredRuntime = RuntimeVersion(desired) - val actualRuntime = RuntimeVersion(actual) + + val actualRuntime = try { + RuntimeVersion(actual) + } catch (e: IllegalArgumentException) { + return false + } if (desiredRuntime.name != actualRuntime.name) { return false diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/RuntimeVersion.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/RuntimeVersion.kt index 391bfe53..c98d355b 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/RuntimeVersion.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/RuntimeVersion.kt @@ -11,7 +11,7 @@ class RuntimeVersion(runtime: String) { val parts = runtime.split(' ', ignoreCase = false, limit = 2) if (parts.size < 2) { - throw IllegalArgumentException("Invalid runtime '$runtime' requested. Runtime is expected to contain name and version specifier separated by space, for example, 'iOS 11'.") + throw IllegalArgumentException("Invalid runtime '$runtime'. Runtime is expected to contain name and version specifier separated by space, for example, 'iOS 11'.") } val (a, b) = parts From 3e0438d787fc56f2cb0ab3aeeb3dc0023dc1277b Mon Sep 17 00:00:00 2001 From: Max Oleynik <33117161+MaxCiv@users.noreply.github.com> Date: Thu, 7 Mar 2019 23:31:14 +0300 Subject: [PATCH 070/131] Add reset_async and configurable read_timeout into ruby client --- .../rb/lib/ios-device-server-client/remote_device.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/client/rb/lib/ios-device-server-client/remote_device.rb b/client/rb/lib/ios-device-server-client/remote_device.rb index 6ca8d0d8..b28b9486 100644 --- a/client/rb/lib/ios-device-server-client/remote_device.rb +++ b/client/rb/lib/ios-device-server-client/remote_device.rb @@ -32,10 +32,10 @@ class RemoteDevice # @return [String] attr_reader :device_ref - def initialize(server_endpoint, device_ref) + def initialize(server_endpoint, device_ref, read_timeout: 120) raise(ArgumentError, 'device_ref cannot be null') if device_ref.nil? - @server = DeviceProviderFactory.create(server_endpoint) + @server = DeviceProviderFactory.create(server_endpoint, read_timeout: read_timeout) @device_ref = device_ref @device = nil # fbsimctl client device = @server.get(@device_ref) @@ -63,6 +63,12 @@ def reset(timeout: 60) await(timeout: timeout) end + def reset_async() + ensure_ready + @device = nil + @server.reset(@device_ref) + end + def await(timeout: 60) last_state = nil rv = wait_for(timeout: timeout, retry_frequency: 1.5) do From e40f5e0613e45bb3568c2c1ded12d605b4f216b7 Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Mon, 11 Mar 2019 12:15:26 +0000 Subject: [PATCH 071/131] Fix uninstallUserApps method Instead of only two types `system` and `user` being return, fbsimctl now returns `system`, `user_development` and `user_enterprise`. --- .../com/badoo/automation/deviceserver/ios/device/Device.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt index 52194ce6..17743645 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt @@ -259,7 +259,10 @@ class Device( private fun uninstallUserApps(whitelistedApps: Set) { logger.debug(logMarker, "About to uninstall user apps on $this") val listApps = remote.fbsimctl.listApps(udid) - val userApps = listApps.filter { it.install_type == "user" }.map { it.bundle.bundle_id } + val userApps = listApps + .filter { it.install_type != null && it.install_type.startsWith("user") } + .map { it.bundle.bundle_id } + val bundlesToUninstall = userApps.toSet() - whitelistedApps logger.info(logMarker, "Uninstalling user apps: $bundlesToUninstall") From 9a2a10c1f62cbe8160ea9f3e912c9d8bfca6d26c Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Thu, 14 Mar 2019 12:07:14 +0000 Subject: [PATCH 072/131] Set log level to debug in logback.xml --- device-server/jar_launcher.sh | 2 +- device-server/src/main/resources/logback.xml | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/device-server/jar_launcher.sh b/device-server/jar_launcher.sh index e43e664c..5228d9f2 100755 --- a/device-server/jar_launcher.sh +++ b/device-server/jar_launcher.sh @@ -8,7 +8,7 @@ declare -r DEVICE_SERVER_JAR="${DEVICE_SERVER_JAR:?Jar file is required}" declare -r WDA_RUNNER=${DEVICE_SERVER_WDA_SIMULATOR_RUNNER:-'../ios/facebook/simulators/WebDriverAgentRunner-Runner.app'} declare -r WDA_DEVICE_RUNNER=${DEVICE_SERVER_WDA_DEVICE_RUNNER:-'../ios/facebook/devices/WebDriverAgentRunner-Runner.app'} declare -r FBSIMCTL_VERSION=${DEVICE_SERVER_FBSIMCTL_VERSION:-'HEAD-d30c2a73'} -declare -r LOG_CONFIG='logback-test.xml' +declare -r LOG_CONFIG=${DEVICE_SERVER_LOG_CONFIG:-'logback-test.xml'} export JAVA_HOME=$(/usr/libexec/java_home -v 10 -F || /usr/libexec/java_home -v 9 -F) diff --git a/device-server/src/main/resources/logback.xml b/device-server/src/main/resources/logback.xml index 116a8368..14396bab 100644 --- a/device-server/src/main/resources/logback.xml +++ b/device-server/src/main/resources/logback.xml @@ -2,11 +2,17 @@ - %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %logger{36} - %msg%n + + %d{YYYY-MM-dd HH:mm:ss.SSS} [%thread] %highlight(%-5level) %logger{36} - %msg%n%rEx{full, + kotlinx, + io.ktor, + io.netty + } + - + From ee9af2ef1ab5708a188ca6e681ba26710664af8a Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Tue, 19 Mar 2019 11:09:10 +0000 Subject: [PATCH 073/131] Downgrade logging of executed command to debug --- .../com/badoo/automation/deviceserver/command/ShellCommand.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommand.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommand.kt index 2f10bc63..a7307a9a 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommand.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommand.kt @@ -68,7 +68,7 @@ open class ShellCommand( } private fun startProcessInternal(command: List, environment: Map, processListener: NuProcessHandler): NuProcess { - logger.info(logMarker, "Executing command: ${command.joinToString(" ")}") + logger.debug(logMarker, "Executing command: ${command.joinToString(" ")}") val cmdEnv = environment + commonEnvironment val processBuilder = builderFactory(command, cmdEnv) processBuilder.setProcessListener(processListener) From 24b20698879071d84fc14c325b1750297554388d Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Wed, 20 Mar 2019 14:22:25 +0000 Subject: [PATCH 074/131] Remove obsolete fbsimctl failures filtering Remove unused logic for splitting fbsimctl into warnings and errors. Treat all fbsimctl failure events as errors and log them. --- .../deviceserver/ios/fbsimctl/FBSimctl.kt | 31 ++++++++----------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt index 4a759b07..96895039 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt @@ -118,20 +118,18 @@ class FBSimctl( fbsimctl(listOf("uninstall", bundleId), udid, raiseOnError = true) } - //region private fun private fun fbsimctl(cmd: String, udid: UDID? = null, jsonFormat: Boolean = true, timeOut: Duration = Duration.ofSeconds(30), - raiseOnError: Boolean = true, isWarning: (stdOut: String) -> Boolean = { false }) + raiseOnError: Boolean = true) = fbsimctl( - cmd.split(" "), - udid, - jsonFormat, - timeOut = timeOut, - raiseOnError = raiseOnError, - isWarning = isWarning + cmd.split(" "), + udid, + jsonFormat, + timeOut = timeOut, + raiseOnError = raiseOnError ) private fun fbsimctl(cmd: List, udid: UDID? = null, jsonFormat: Boolean = true, timeOut: Duration = Duration.ofSeconds(30), - raiseOnError: Boolean = false, isWarning: (stdOut: String) -> Boolean = { false }): String { + raiseOnError: Boolean = false): String { val fbsimctlCommand = buildFbsimctlCommand(jsonFormat, udid, cmd) @@ -141,13 +139,14 @@ class FBSimctl( result = executeCommand(fbsimctlCommand, timeOut) //FIXME: NOT a good place and not a good thing to do } + val errors = filterFailures(result.stdOut) + errors.forEach { logger.warn("fbsimctl warnings: ${it["subject"]}") } + if (raiseOnError) { - val (warnings, errors) = filterWarnings(result.stdOut, isWarning) - warnings.forEach { logger.warn("Ignoring fbsimctl errors: ${it["subject"]}") } ensure(errors.isEmpty()) { FBSimctlError("fbsimctl failed: $errors", null) } if (result.exitCode != 0) { - throw FBSimctlError("Error while running command: ≤${fbsimctlCommand.joinToString(" ")}≥ " + - "Exit code:: [${result.exitCode}], stderr:: [${result.stdErr}] stdout:: [${result.stdOut}]", null) + throw FBSimctlError("Error while running command: ${fbsimctlCommand.joinToString(" ")} " + + "Exit code: [${result.exitCode}], stdout: [${result.stdOut}], stderr: [${result.stdErr}]", null) } } @@ -174,9 +173,5 @@ class FBSimctl( return cmd } - private fun filterWarnings(out: String, isWarning: (stdOut: String) -> Boolean) - = parser.parse(out) - .filter { it["event_name"] == "failure" } - .partition { isWarning(it["subject"] as String) } - //endregion + private fun filterFailures(out: String) = parser.parse(out).filter { it["event_name"] == "failure" } } From 9e0d01fd214a0dc95cf9c8d345477fde81e49b49 Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Wed, 20 Mar 2019 14:25:20 +0000 Subject: [PATCH 075/131] Reformat FBSimctl.kt --- .../deviceserver/ios/fbsimctl/FBSimctl.kt | 20 +++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt index 96895039..777e01b4 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt @@ -17,7 +17,6 @@ class FBSimctl( private val SIMULATOR_SHUTDOWN_TIMEOUT: Duration = Duration.ofSeconds(60) const val FBSIMCTL_BIN = "/usr/local/bin/fbsimctl" const val RESPONSE_FORMAT = "--json" - private val NEW_LINE = System.lineSeparator()!! } private val logger = LoggerFactory.getLogger(javaClass.simpleName) @@ -118,9 +117,13 @@ class FBSimctl( fbsimctl(listOf("uninstall", bundleId), udid, raiseOnError = true) } - private fun fbsimctl(cmd: String, udid: UDID? = null, jsonFormat: Boolean = true, timeOut: Duration = Duration.ofSeconds(30), - raiseOnError: Boolean = true) - = fbsimctl( + private fun fbsimctl( + cmd: String, + udid: UDID? = null, + jsonFormat: Boolean = true, + timeOut: Duration = Duration.ofSeconds(30), + raiseOnError: Boolean = true + ) = fbsimctl( cmd.split(" "), udid, jsonFormat, @@ -128,8 +131,13 @@ class FBSimctl( raiseOnError = raiseOnError ) - private fun fbsimctl(cmd: List, udid: UDID? = null, jsonFormat: Boolean = true, timeOut: Duration = Duration.ofSeconds(30), - raiseOnError: Boolean = false): String { + private fun fbsimctl( + cmd: List, + udid: UDID? = null, + jsonFormat: Boolean = true, + timeOut: Duration = Duration.ofSeconds(30), + raiseOnError: Boolean = false + ): String { val fbsimctlCommand = buildFbsimctlCommand(jsonFormat, udid, cmd) From acd761c9377654c7f143c0f7c67b6d968c6db01e Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Wed, 20 Mar 2019 16:38:03 +0000 Subject: [PATCH 076/131] Retry fbsimctl device listing on empty response We noticed a couple of times device server releasing all devices at once because fbsimctl returned an empty list of connected devices. I don't think all devices could have got disconnected at the same time. Probably it is fbsimctl or USB flake. There are now useful logs as we were ignoring fbsimctl failure events in list method. This was fixed separately and is already in master. Let's add retry for list method until we figure and fix the underlying issue. --- .../deviceserver/ios/device/DeviceSlots.kt | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlots.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlots.kt index 20418be6..86433305 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlots.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceSlots.kt @@ -37,8 +37,27 @@ class DeviceSlots( ) ) + private fun getDevicesWithRetry(): List { + val maxAttempts = 3 + var connectedDevices = emptyList() + + for (attempt in 1..maxAttempts) { + connectedDevices = deviceInfoProvider.list() + + if (connectedDevices.isEmpty()) { + logger.warn("fbsimctl returned an empty list of devices on attempt $attempt/$maxAttempts") + Thread.sleep(500) + continue + } else { + break + } + } + + return connectedDevices + } + fun registerDevices() { - val connectedDevices = deviceInfoProvider.list() + val connectedDevices = getDevicesWithRetry() val knownConnectedDevices = connectedDevices.filter { isWhitelisted(it.udid) } val diff = diff(knownConnectedDevices) From 876fb2fee88c64fb0713d99574e138fedf7b501b Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Fri, 22 Mar 2019 13:22:06 +0000 Subject: [PATCH 077/131] Update Gradle to 5.3 --- device-server/build.gradle | 23 ------------------ .../gradle/wrapper/gradle-wrapper.jar | Bin 54333 -> 55616 bytes .../gradle/wrapper/gradle-wrapper.properties | 3 +-- device-server/gradlew | 18 +++++++++++++- device-server/gradlew.bat | 18 +++++++++++++- 5 files changed, 35 insertions(+), 27 deletions(-) diff --git a/device-server/build.gradle b/device-server/build.gradle index 4a33c2ec..f5083937 100644 --- a/device-server/build.gradle +++ b/device-server/build.gradle @@ -129,26 +129,3 @@ test { kapt { useBuildCache = true } - -task wrapper(type: Wrapper) { - gradleVersion="4.5.1" -} - -// caching dependencies for docker -task resolveDependencies { - doLast { - project.buildscript.configurations.each { configuration -> - println "Downloading dependencies for project.buildscript.configuration => " + configuration - configuration.resolve() - } - - println "Downloading dependencies for project.configuration => :compile" - project.configurations['compile'].resolve() - println "Downloading dependencies for project.configuration => :compileKotlin" - project.configurations['compileKotlin'].resolve() - println "Downloading dependencies for project.configuration => :testCompile" - project.configurations['testCompile'].resolve() - println "Downloading dependencies for project.configuration => :compileTestKotlin" - project.configurations['compileTestKotlin'].resolve() - } -} diff --git a/device-server/gradle/wrapper/gradle-wrapper.jar b/device-server/gradle/wrapper/gradle-wrapper.jar index c44b679acd3f794ddbb3aa5e919244914911014a..5c2d1cf016b3885f6930543d57b744ea8c220a1a 100644 GIT binary patch literal 55616 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
K7|BUm`^7>V&Zk%^_d-%A~k@lFe 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

Qox z0`xkkAPQweAlfOtBr;PCpCyY@I(B}_q2#9zd3W%J|3eWKpVLA(TO z5%Zf>!cM)^YQ?&n@bvEeMq7qf)_Rqe86vho+bO6^&4TNMJrCK9V`zKRuXfd8M5%~s`9IYm95q_DwQl# zw{#U3?nojDov=wtw2sQ^BnoussoxlxR&D21ZG+h=hHHPRxddwfoNLfm=2*#>S;;QV z!b3X2P@Y~tG@ zEsv?a$avqb z!A;+xKmVyOCP2?u_M?6ro!|6p3hE1XWYaW#CmFc3%s^$13Jd-mV|FHKD;5_gD8=oL zv9{Lt);bu_WV&2XT749?b+HvE@zDP45=p1BaTTD|Ujs_}Pptcu-!Z)p9f!fEsGcW0 zNI*A-;X6d73JsXdwnqOVLo}*B?BqJxV>?b(wQd&e?en)d{)G}U1e&OCD|aImZ`3H6ub*NDlQpCW z7Fvb22s61l4U30fGmyZE_9%KpbX?j2jtpKREvCcg;qd6)+bMk%rMajuBY7%4@T_MqDUPcc-On;3{h}TDaHHiD8llM)Y zenv30d7+wIdgsx!>bknt{ArjL-`i3>%>zm7b1aEWPdW0}Dn`+tNiz|#nDU#_Mw2GC zF??~VSmm`iB5JmNJnfW{;S|zFTxex&mW5Oa^r*W%uJM>*pmo=TO24r~ap-AG@Z^z& z@ag%!NpczPaLM}v-G7twO{k8Y@*^M&%;gdP$@biw`0`qQ$SNmi*8mkopTL?V(*&}c zBLjqsFZ6T@g5&L+aa)+Qr61|;9SRLU@j)Cb*v4VnqP&h-Cqz$)nB3x)s@C4u!g%pM zEyb*^R3|r3{4MKBUPH?(D8W81Y2Wi>?d83MZ{MQ=!DaVyWJQG-->ZYzQh6mm-2RAr zwJeG0GKJdfJyLuoeXc_f?Ancb`$9pUO=9Ebr%&VtFna#h@=(gm!2vLt`(x|`>{9<} z;LQAwbHwG{$}BQEX-KrBUk$h+Oe|hb=vXisNt!NgrwZ!qNZKii4fNz~AIrU&Cthe& z52`m1Pr}7=!w75=OcL=4TjSp2n8D(|{FJg?rBNVX+2cqF#nR*srLf3GN^A4tb~jU^ zw^00dk6n`pHdS@eyf=nvnjNK@PwmDHX|tg8hQda*<{Z&cN~6kAkK*PmYn!Yzdc&qo zZRN_;yI>xRqWF|ahf0Yk&#(p9mfqqvcEXjhG7XuCqJKPLZjihSvsrMYmv?GtZtpBC zygaAfZLcR?ncPb{QqRN2JsWmcosmDIY;l(-I{^F9WE4l-zK$g{sJwQ;rCrzj0d1cdA`jz{$1?pXrG=acA{?JbGvy(oh&ivO9cX;@g)xX}$b5Kq948PdDBiJbiYt zR0vER&T`jt{Dj;JtKbTgsy#L^0Zs{7FHT^NL1-580djJX)=Wk;e1aj-1UzILng@P` zgo%F__Zz9(sqT9~vJ}FxsRdQtC%d@`Y#?J>qrJisrL;3PxBXf$=g6%%F_Kn$wT!uy>CK@uaU z0F>zhy{(7o7W{}c*oBRdoE}3X9G68iyzT}{29wew58xymHl3&f zuKG?e$hb&uX*2Ki=|a54*X&bX`B`dyny*-oDJu~g-4!B*9?~JIa+lH+$w8>&CeB|M zHvac;C8+@GF9lftZ_OM3ZT2pD_C|l3H&!SuSWnBsak1EK_1KA#TB#1nPbCna#xZ|L zpr$O$`yj6vKXAO9!cL#;+Jqw2C99vUJ7z+5)ek$x)ON(BhmLXEvqt zE!l_#8jiyN2{>H4nZuoy$hkMW7~ZA(&|1LI{Yc%}K>^G0u+8Mhn>+&O@;9PmZ+CBO zd<`V`uQ_1;u#fK2XLP6rV;~bO>TAn7O zQMZ>EM(ELT)0mClcC7IkY##L4t!cV?uT^+Uv(ezz;AQS!p56^|2ln2^-NffhZ58{8k5t*V zK`^yH?32h(0seh<&w7XO%$Z1y)w53NfD`s^S{ugGPuHN8_N`V=MyaLW6}=7_9keUc zvywH`bHX{CBFadUFYkPsYx=p;Pq^#j9gMo|hCtf!oZMZ6X~|VEMT>W)6bPXLuT2Ap zJ%ZZk@$w9(`$o7^Iy-RnM@|Xu={|tY$Y&YlR*My=zA-==mW?tW$O31Vktg8KK&8c| zt&F3QqchlLNVw7JK-*T|@o?4G%0i>wMA$*6Ho#wB=#~XnqUXjFR}?T@Q0ZC4cK~uy zai|eukdf#KcZjRHEmS(8y5K?=Gy&|vDh_o+kTdxq`%T@zMMso0AuN*p|hGHue ztCRZL7%~=DgK+i8FgEJPi?!01K5?H;fX!C`Y@X$J)=Gca{L9sQqSC)S;ohgSlXA>x zl|!Cx$o0kf70i=VQyK_; z&K^)rtR@yP*;m_RzF|SzbaP7PBWHUc?&b|#+I6n2Hfgbm;0k9HKrS{`Z4Dakb4dY*Nn57C#) z=ECn}*Y1u~%pvL}>{5-!9ou<#23Q+=AWl%|Fh%D`@94AW$~9{*_^6gdOv_vO&i4#0 zi>d7wf0OY^@!GR6z5U_yf%%@H zb_*}SllSF=(a5w$dA9WgP&+VDPtU-lb%--Yg=2F}3b)WP0VEyFbgc;K0!u_p1{4rl zuT+SIC>2yD51g9c>`p3T&p2+oQL(5e|2W(B$-NV`5TnJLPXMj)X95zlFc(T zV;*6TyX^>C`K+kBi4bGJ>i#^BW(A^ z2R?pZE|5he!8_?UlcB|w%_0M@^j3}-P=KiErPlGVW3{%4&fPv#IAO4uW)`Fs%HdX0 z4uXay5=!}E#1_g(zlx6i4*S=UAd|qct{89ztmyBuO26J4`s1zm+aQoAuk}+_iK|wv z)>%rbE^X5#f=rmq8cBx`-;@{04=R@PmRT(5WWZS2n1skDm#0`Jkoy++K0nNb`4v30 znKSlSX6s(oFqg~Iu@@rhE)gMy+y%s!B#=XC5lrSbcUrKR$z_rHy{EXWQk4a zmmK_S-=qaodySWOuo0Yn0BnhzJa^IL{EV%fVr%SpfN3d4*xzu`(i-(9^dQMw_P_=J3AAf)c! zAse)jx9GXO<_2en3`Uh-2z8`DF&5mVd9kgOIN~Y#PHsnmFyg$b8z^Yy(D02 zoKEp6SSnKeg4dW0^j?V;Nn5Msgfom9_Ra|-8Eq(DM2}Po zznRFri~2Y@(7*&=g{uWLz>v=P+NbkQ%-4S*!O-i6?^~ojVUXKfh^9Jb%7Ug488T`; zw%)u^R7wXUN^k!Ch~9-yz2O91qMVV+)k#Se#gDM&Z-nT)& z`UYdx9f?)jAU1d0MkwkmwszZ9x^9G4YoBv2mCTx!u*`eK7){fT)5EE;*$DjXHpwDf z+B>rK9jC1zCQ1Bc10wytMU7r7OkgF~_?uGdw*u+T705iMs*&&Kw3bSnqm-`FrA}vr z!W%guPH=rNWM0$5a=0G^P$m1Q?MNLmXp%Z3rbRtARBplpqpfO+n%Hn7vqA5C%b-Qp z+eQD1+DQj-rcg*QeYitDz0(!Y!KC7r^cItL6*ZnfuNh6R}}T(~1u5O?VNB zazm$B2ZzJRrqkk@@!TD{k*wqsa-1eO`MW5waLvX58*vi*Apt}OUQ@w(Q1@!D(UW>e zcO0zH`fRacvP`=RNHEB@r>%OdxQEbG=|2&qN@3-lQ4o9cuW<6K2YgR3sl()d2)fvc z^ksPGL6UJVNL3_`?cQoV;vZTJcT;DI>_PSo?%u7+8!E%x9~O@p)qhSD8#35D$v7(K zI6H7FIw1XofP_Jo4t<=rHzC9K+?pUdAhr){`9xQE^SUL8+nAY5f+8iU;k}(35!A}5 zm!^M^MqQWaj~5xVnv+C0ya7h81TgadkGbxzefOD);{eG3q$gwNrNF|#Fj-_Od}ULz z8YDP=@sNU0v3OxgT0-}CLj^Eu&V#2(x0Rm<)4@G1UWXF*)%qk{j5g%S*Y$OeJ? zrF-59F#A3AL1aYzc$qfI_b6}LRCM2~8=I9THdQ0E{)ZU}7FdO>e;(H)(3iSoVHkG|S#aj2Tq z13192TLHUM^uIHq{rjM;u=Z28^GTWv3EBa)vBW`cSytEb%bhW8nkXY3-V(wH_O-Kb zkP}(sZUe(T&)sG?G50O_tqA(K)qYg?c>VH6H#`}x6q z^DW3M^$!}RaP~A_2mO^0sqR|=y3Sp>BC03%Qygt*H(XbIm%!HvtsA@`B>Z=aS*)YC zBhe6n2D$h$SNia^wYS>hGET4Ig|KlNT5>U(35bGx_ujl-I|9FIiUn z%A!qX4=Gi_*^Yx@ek2!es9RP$&WoWkyKoO_s3fM*-ZWPXC|6kr#%W@9iJ6;+K=B8_ zgLBgb&2+wc=YH{yfsSfL79Qm*NZAv+`Eg?!%5~Vh$RK}sRimWG^2(=ISXblie3Gsm zkK2$-;pwf)lq+C2v?v$rk~-@{_#m}iJ}PhSt9AF`&k?MvcWSmHaa$jN`&g7=<{wAR zNZ3fLv?YO6KfWer;3IoQUMtDBm|b|oLr4eVAU1OGL+}d=m5|f}Yjo!b6}I*bgVH1ubk21&MUkV)QN7<&uymkUFE>r< zRJC!XLc#MB*=_8uo-W;Fba(JOkRc)8K>If?}tg%gm)QkX(fIQa|paNyJ8fcJnWvT2Uz|@W^8=TE8K%hO4V={C$dIW zk<_T%6h2)427`Bs0W+9r@(4Pvw#;mAk!7(6hSdultQxeDKf*0j9hHq63p&l*E(FHq zl~K*c=h162i{3RX9UFFpLROYIRdmX|o1R3iy^YjVKc=N{?5{iTVIC(6EOWfq@NLSw zX(u)6dvXRcHYKWnVf9zj!?PJ-8WU%! zdEZM6*bp}($=xSOM%u!x2^BAKOZfSc!}MT;t8+GqQSzI5X>Z1-J85T-mVmxY<0e^& z7~XF%qlW1*u9!0frNO=uAfZ7yv-Y6Y*;5X@{vO#^|7xb1f=&>p>&?AtPz(}mu9AG+ zz|9w;ukfOIUX0b>>nJ9vB|CHsz+>vFxdQ5rvAY&;vA40ZJ@E0nI_}!cuNc>j zSfe|EQlVpN8lnf%3D(b?beq9Cc!v}_9kvVOKl6CnmZr&i#72Zag{PpMy*G}v??HyN zO8&AaWQrqa{}nGEUv*xlXQ8qs4naxzP?UxmT=QK4?m>78a}pL0&=Q;c3^)#t!f1&S za(5yxVC4v$X(0N*9uQ{#cWj(`#rCG-Fy;-80sV-kOj z2GWhcO2{(!nHJH6m|ycyyR3e(1*Lpu%Di-DmI<$Ds$;f-TjN3dA?wU(@|vonx3EIX zvO;F{Y?*^0Rg9YWI(pgRlx^)M)8_linWXm9eri4t%5Z%1yno}DEvqY6k$yKOSQ2ZhtlABUwteQ;g#Dy+(+fYbu;gkjV3cE;=xrY2}c4kOd}3t7r&sENjgXy znUD)|0haHPGcN6??4{G-@)Q3IDSjGyXcsp%y_+6S;$Vc0b1NIKkL6@vL;TH&G9EN7 z!BoD~ATT2@UmJydh+b;QsXQ08fM3Lau_Rtxs?@Q(n71U!?Nv#xN`dkTB@}L{v|2f~ zgd>}hv_frR+Ls-@{0!_EqclpDX?LgXu=nMP?v+pj=2soU@eGc2WSy|LF$`+MaHO@1 zhDpSL?PBePnGXhy870Ohpxc%^nZ#OSu?|iPxTCMka)~2?Ex#DWTfP}^Gp|*Or+N($ zQ6$-*5s=d@(4Fi4GY2wjvX^gYIPH`g;WZpM7$N}#q!p%7H-OJ%`!2m`J3J?&cy|* z5T_-Ly24xvz21zOCgLSfhT}vAfoj*h`pQiA69$4zq^jA&u)cD-qqJjDjvT#D=(ROt zD`W%1>hrz84DCcI9d^@6MUhmk8W?HsTx`teYYH#gQ21=SvA-eIHqgLB&GnUAAMu_5 zhMo$13J`_-s2Yn01^OamS(fznfc$a!R1(H;*&bty{za2&E=b0lC_ z%Vjwk`jnU}N?NVHPDWvp(0-JcnKYG6Qh#}3(WtM1l$&EKP}dD(!(@PWm8E$}?9QLS z`NQCgQ-+k0SGzeeYrAE?tH*G^c+~!3-FUc{y4k0MjiyZnpTtjL z381SjY6g#q`z-qOVTxHSg;*tz&@|R@ zbd<#4L`k4$XfR3evmym5l>K0ejVsGDFsJt0>nQEKmyeC%{8MAi_D_t0IFy7QY4g-n z*$FU?>hw$S?UfVN+v&=N-w2r(;tEv2<~B`zshv9{vDDNLdT{+P9!98t*glCKUPD*c zqphqt*%2Vls{*U$`>20h>&v0hlUialwQWKswd1Mh?w@ax?Z#WBTMn)@-DnuW*N>;M zVH~ss-kIoe(1U}Z!hM!y8iL+XL+S6M#faI!ejL(TSO=|o7xF|tkSf|x?e#X0bh(yg z>p(Vw%Re_n;~=SfZFO#@P@mpona|<`%Ski&e!|2jR0Q;6xol8{U8AU#^wb9#&B+7# zFQZX!D6nbNT1;be>MZr)NcW1__de&zjTwb~`!Z-7WkDm4pF{!gn`r3Jap-PQM>E@r zEtY#WVi#wgfC=2Vi2}^BNerB=P)oDU%s;gcZ<2n2jh#PeEkKPh&SCM{xw7IxXc4{r<4&%*uV_Gv8Q+3Qhh%eVQI1h(0MS(iKGBXp@ z6JVyswUL`@^?^OSq*zJitjTufqqxBRw!Q#$?Drtd7;gdU#Nm*4Mi!epVqr>5$U&Oa zDx`Tb==O!0LY8$mGYyNqdv?$sY1`^oAJd?WeZb5M-Rt{QDKQwf%?mHfFM8pjTuNKu z7o8$CEe4$I+wroMqnh}r8MYbh^YK^)m4ZA`8qw`*J*DF{V49W0-o5*5CuTLUw*!4# zr>QGXH0V%>g7BeW@*(i+snwxfE1t_hCK*TkJoJ(gf>UXGAraOGZ{L=Z)JR8}tY#%UPMNjFrCF~oCZ!m7FJr`mg`l^aM7h@ij z`rIV83S-NA9C9XNDn-Ar-F~HH!LY(76AzC39mvBsLOCR7 z)+%U0;re8Yg>L1nrq@oAMq3p_M-?*+HGLz+$oU%8<*UZKYIchR6de_7?}31DT)og`sIzEIud*k%-vx2vN1K0@Qi6W~ z;UFffX2pQKL3I%%fMh_*&1>f}4%qGC$Lhu6icketpd5QtG+F3A4P?SeuaZ7zx=X@~ zCKHk-Uuxd{n%SPr6hL+phIOEJb*hED6U0d^Gf{%Li{Nq2Kunl+&fV_G58vOaEOL3k50-xR_JxGz3#Y-H5vu<;srb1&&Y@gH4W^p5(6H zYqP+udfjjY@l`EIZ?#>cWi#mhN(45K5!Y}hT)iK^XQYGtXo??=q#HAZ5cqwZ{YJyvsQjT;hwxjKG~P+9F4rG?~i9wQJmdgjF*-( zOV#UgMn!x|viNZH7UgcRJ0boAhZ;p{Q=4=5sWK2hbM}=J-}O`hG4d9%%e3P=!DD-b zawq6f5-tv!JEhR=BN=H*?t z_If)wCJljVi(fKcWW$QUpZy|b)mI5IbrJgh@AU!gcp?`)tZ4}QT4zrM1D zE^&Zn$mLu4uCz*((eyPQogGX~UWdVBe7qZ@Ya`khCn;Roe~M+_OpWRE5g|4^@_m%R zoW@0zD(O|NN@dG1jl;ztVf*%)#nsa3AkK;U9}=gw4u*gIDpO$LEZ>?(An6fYs<8;*w~0zLKZkzj`%#s4Dw@oz-@WA&41ie9!O%NmtJ!8VqLle z{mt9ct`*G6U7`ovlEgM8Ob6CoWkqaX=8(?@W_;f1C6g$$(|F=gvb6$D!4Eo{%flDi zPZzsm`D9-lP)A4d(as?3mxOZ~l{f=4^tK^`bYb+wzd?LmA}=+BP|zR`miv6<$Fh&r z$Joi|CNv5Ky4HK?uH!Vp5`qrCGnrFaWeUgeHcuC%b`k05IO$b$@^B|#hAkXP4E;XA zMW{b($tup}Tm3hX)Fhpn={dyv6sk-iZcg68H6cj7Vam|vd>w8yHEuG*(`trkHVm1T z)9zkk@?o&|k7g}yGP<33NU<#eUxH&;{N#hS63$`*1+Tn~oF{l90@*HaB#DNzIVWe| z@JJ1PoU;_C5_5C9f*2zG&{m}nml)P$52s|#S;7qm1Cw`;3+3;d(5wi`QnHhVqN8Ok z_t9SMM2|9G$y31@dG2Td|EfTgi>jt*r$rN;^?Dg-Ru*+ok)@gE{Z#0sykHAfjSv+u z4pk|3&n9`I3^qr07B6ykI$e5T6;OrgXOs;8Z+FX3h)Y$ds5v-RO$bYBZ#Yt1I4*#k zH^?+YK6P6^qM>e}7I*@mxZ+^321%#BmN3qh*v-)hnXoyI&rBxJASagLZ9XcZpD)C$~!S=cnRMT(r0mO1)9 zVyyKv?tkl-542I>%2KL$v(MRi7k^m^OeN8rN3LCV&J8QmOA5E|e6hw)WIf7@NL3PG zJEIg3foR7ew7h}8Y0fD{vxMIxG0ODuM6ro3fM_(4YDVO!EsI?zwsOEDg-C5%L;kE% zd}g+U4Xw|NZQeOE`tHGfhBgUGy%dYKv;2@S=?hsv2}aKWaQ|vK+UVfjCG&nVkQaUO zZGDIVmO)i2-D+Qol?hB@2M2m(^9V2rIXi<}$n759e9{KQL0d|YeBT}|)v{!m9%pyG zQi?(Uh=GKt-kx;C{5-nuuFt#iDTWeJHVP3d67OK~CF~2!0?xdWM_Z8LMe^XPjB_;^ zRjo;3Bu%yeC8`-SPpm%k7JU$l{T7D9_L&Bj!%#gjpSC<>vEW-QI#}@$^|0#L801gX zM21{}j5Re(BI4GxEM!JyX+(JHD!B4T?Kt23U$I1>_oX5+zjw=D6548v=0bx(%5nlR z`G!Su*&opq)w)5Qx>|rd^P9p0B!#I!d)O0^bsXy4MT-h^B&an zT&hJ+4N@_Uy1qvoTuBrSrAubJG<|(Fy+hzB|R5B8)Q{XHddbNgL0yaQ%e3oTLY#+!pzjN}(n7xHrUFzGr0dTGZJVThU%RY3H|s z;hhqPbHCB*&=#2U@o0BexSg$qAXx9Tk^13HJ$?fgy+@(P_ZI17liCVmndH!8+I?#b zI}ST+ZGJd45Pn~gyai!7Rq=1umAa~vlei?>l~POc*dp`u_jn4f9!3009cE>kb_ZC~ zk}edI3O{;BN?r4O+7#uo9fMdz8#x(Wok^tW(s3ON3e!6tu#}Wdvy?paa(IK+80Nd$ zTp{jt>|By+a`m}-4s8Kiq_a>sk*XfzTrrbmcZ;d3XB+~Xhh*Z>kM|q<*!rF&RlR9X z<%wx|5wntIqjvYFi3Z#~v5CFnuR4R!9@h@{%ALLH;&((;6J&c%_>N%vOP4mbjyX>% zAPcXuHr`vl<;pMTR$tI`a;z^N)7Z{*Kzk?)Ym+$iVy?N5YZtWzX5GSkBD@^_m%|l?>l8;#$nbby= z70Hd{fj~Bjk>1*e^F+WldSI)>1)sXdZdfiyZ5CwPf~g;|lO4`59z(I+vlFjPW`F3Y za^V!@dV#rHn%>B*DlymX*?I@Uo?zeK$-i4{-_F$Et4|)a7Q2$+pK>@8`Y|q96rD>#oIDVK*+lpFDe%FLJ{&`C*WK`Dwpi&zd~f zGP*()xIf$tKFlt{L9>&tvpRZy`brL)(|KE&8Zr2QQR<3Rds1t;FT=Jy+!Z zGB)k4(aw6zN`miKm^@M~k+%feU-zDP{<>kR;cA_d0Pu_U13Wyx@b3J}!EX4cAm@MY zk*X~Cyi-Ab5?&gZ60BD0k6IyCnr2NhVhbXia4iYnB9_8jBC`{-Rfj^fz?X?JNthf6 z)ex7+od_%}1WilwVhHywV1y**Nn*LZ7<*^acCG@~!NGtbG228(1K2rbyW!aLG-;mV zd3xyQ0luYOmB~R2f?@E5i$K|yOR^*L{m@#~laJpmozuHgLR=j%ET-96NT@5rt#miKK(YEQbW#J;BlX9pFw&ERcRz=`p~tW>_eq1$YOWBx#9 zN%&zLyK4Q_)OwvdcI*Uw87l0|NAOjTLq}`!a$2-^3JBrAerFf{UA} zSV~|uhRq0VI^@^CF%hqX*l&N=z}y)Xc^G6JEw(>0OO{c^B*CRKC44_78X}njD&;zU zZd@9^$8-dFA;s@Ll%XPFq9}oCVN=_?lR+H7A1?)o=T#YS&3=Yy&|r3vF7JHn2%H$R zLcS8weK~f#;7TmYp-;1)2(&`%c{pSod3}u=MCiykRi*h+&W@GW=koM4v@Qa~$UwqG zsBg1DjpFv&Qb!gcK|%?jofFwrPy(IjACKcmuY3_>r1Amcw9L8LTw>px-L{}K87fV* zqFg2FKsiu-iY;~_=lnH=qvLRk?^6TiheUO*lL2On%gOXv(3!I4Y3t%xT%mg5aUdGdG4GpU1!wY>+`;RSnI86o zn&Uny$$U3ln5%0R16umR-^s_BpH#X?d|9iRFL8QZ zY!)PEdakEjt$w%OpvCk&ium?>ml|dx9vGao6TEN)&O9H zQ?(!L)@p|}xT>8Z=W^&O$Zh^EMxH92H|JiUJfGhZ8J_O4Ff=eJBRxX!BwZjf_XwXF zJt}sNpF2Q;x7)F19F%M`M54yF&bexYwu60E*rTb5K^|F8@I!v|QyC{#@OKg7&R7QGaU(D2C`GEb(UO4cZ*AXwIW7Z(dm!` z%bC5Z{ryOc26$!#7F~wW7OhJtp`c&p(Rfw^n84|Hgca;NSPNyMNY?2G+XnPDHnS%aNeG)n3MPjio~E`y@mAG3_QpCxUm0pk2@F4T>kS0 zkMNE=0&l4MJ>z>?!_&67R!}mRce%|P5No^v`U(2SVB1b~($#_bn_zL@Eo2EL_vWgk zx|A{sV&cwKEVG*c3U^oZZIq!^p zQa){S$s7xy>)Lxo>gmj|jrCZu##a*0GwMRW?Lim%KpU=ARsD`;)4MGIMw?hHpVKbm z)q?1TUIboH@npEjIo(r#3ehHO6r4@mEuw3JGj>;hW+fiqtEf%1bep)SZFsI}9v z0+~%Q@eEAQIDSt*?pOyI{Aydc2;H6`-Y9X!Xn%!D^ype2xdR~GH?f?)yNIn24Thn(7Y?{F`=H!D(JNs=g!Wd zR0k-q7s!&r9NtR(8?)@eY6k4KFjGS(z(eRR^M+y<&HGSnEngi>cwAW7eyN%=249!{ z2GT#zh{1d1`tI~{L!tcUz6F^h``YX~43W08_9Jp$Kwy-q0DCKo;GTN%H=ph?fLBxE zESz;_nFi__#r;Q|PUT`9qMol*kz5Ba2`VND1GR3Z05uO;L%C@?+|IX@n-mPk6yUr( zG3Exrl8;1r*g5Znd}ShqS4gq1YCb@~3C9{;X|Bb5rk8k-*Lsb-#s@0Y&qoWiBEZ-N za%#P9B%GkNnriCBNLO`gGX$wFnL%DW6_-tgf9vebF)eCe0NKBXq>d%tvk-n;Zt?&- zel6=`<$Qk?Ur~nBLSGf68L~ts*Qj|J+ynA`&wbyTj;kB*j&j8o>xPUVvlz-o~`P;bu$04{sM)ybs zjei{pX=tQ6!7tQA;v+@Pr5XxDZIdknp~ExlDFE}g5#Ue@vUEvbp@R2;8Yk|!%?TBc z5%jtSY@{Dk7b1yyre?A|WS)7hu`zu5;rZj0E<6R9p{%T&B%UAt+k4vVyq%!1bTP_; znD<$IRFuSa8s29gnkYWqY}XWQc7%aLA$W{f+Ntmr)eK*!tbPqBQ3*JrqS!Bi>ekmD z-heW0@lN)u9i$YfbdRcv*r6{Z6z@XNR^wyTnOB6 zXEL^?7a4Fsi*V!cOW3ApFxU_3J|v#AD4Nir@87vnYMsWKZ z*`{%!koSx#jm_z`mNbps*RD}&4{Sf^DF^r!$g#~`LE<{cK%z5dbX7Gz}Lkv#VyIC(58=rn|46|`NWI5CJqqK-HiEk8*PI{qzwF_3*TLEM{Bh?mN9h+_K{Bp;)37iNA|PO;G)#!-5QEhjvSUQ0#}s3`f@CaW>Zkz z=8*Zn545hcPzWM0uy>MFy}C6q^_-sL4+@AhuekawJaVnJzkpRCxSzT|7QIh2CwaR} zlz=!37KLyT6&Qs{9;_4kVW*wvYBq$O6hD~LcQHWUM|>vo8WI)jW5s-!<5%M&ZE}g5 zrWq`#wfZ7hRi)K)4CQvLi2P+UT5LL>0Snl!PMsyvPCfp;4AbSxnv@ihOcxQZV%&gWnR5;M3Gz8 z3PeJg682V6)pam0?CMj3u^^~o4v^660+Afd9@%~sB;T!9;#MC`y=yA^a2VP6PRv~^ z>L;sUE2bT~O|M5_O}?b&S;MhD_A`|%Y2{E0`yzdb`{Yms&UUpfH~czuEN`<0Bb6L6 z(cyuHH%rL`Qk;C(p!$swGKGWx5CvTa)C|Zep>0veW!-z`Pr0cyj#QwdlzAK_rhtE` z^VFeAxh;;>e}MdT5&HH=w+a&AQ6d1YpUIj3Erkv^vHQ<5=sPdP&mkZn0M+>b*Khi*7y%G;Dp?tN(SKG#@^&_oIn5MKQ#cVF@Gwb0rx*^{96+KpQwKJ z!E-qR-2SQJzvb%x#(d_ko_q#qSt)255SuNTm;X!fIC${Aj~hI1px{HmNt5Z|B<142{>fK?(i1SO}v2iGX4dS z6W|&7CqTf+;p)cc;Fjizl8S=p@r85bmb1fWPTo^e=bXa zm+2R+1xOoPIynI3L4?gLjra@<01Q%k)_VV!P5mVSNQwK3CZNOR03H5U;|K%1{=Xm) zvDX7+a8v?F35wcS8A;mMSUB1Kx@(Tg&Etyz!tsoNmXbd=9B@a6{}0grPCNC}_I{1K zcdY7A3P4!`TmYay6&%1X_(hY&{$q8&#>*^2yZr*_V`e~ZhQH!LQvVvy+QCuJ((=FA za3*v!FCpMfy#{k8dyTa*D02neux`1wJ8E{|4=U%3tyl@F&1eEBje~ z<|uhCwgA9Cy;MJAcV1S0nX%-#a`xXV|0ik0f1eG$gnyZZ;u)UY^lxqZ5B%?}BwiA| zRFZ!t8n^r#(VyD?Uv%YP!oQS6e}*@*{wMesljWDNFO|TbVS8=<3HHx^hL@NxWo4f+ zN1Xl%^N%Qi|Mq2kDd75y+T{EjsQ+Eg^=0#4ic&rkNxALt}nsf=eTz3|_l`Ul~R zmrO5Z37(mBqJD$v4|CxArAWa`s+ZB=&r}MrzfA@BzS#a*+3h9C%i!8)60?NgCi&xi z{gd3tOO}^WoX;%ANx#kV=a|ly1TQ1#o(UvU|33--SC74nX?mt21gzR#jB$VZy#M>7 z_CNdTWpK+gzDL$?;Qw=|%gcUy84K`C)(lvW{I4JL>q*wj9q4DwzPOF^WS)0PCNf(M*m|Nf9ZL7 zrors`zbV~+^TYh7&HwSb{Ml*p)9dnFtN>vD%?BeZ0SZ_L{S5e{2hstYLKp!2EfCQE E17(ftEC2ui diff --git a/device-server/gradle/wrapper/gradle-wrapper.properties b/device-server/gradle/wrapper/gradle-wrapper.properties index ef20e206..b8a51fe2 100644 --- a/device-server/gradle/wrapper/gradle-wrapper.properties +++ b/device-server/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,5 @@ -#Tue Feb 13 11:16:11 GMT 2018 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-5.3-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.5.1-all.zip diff --git a/device-server/gradlew b/device-server/gradlew index cccdd3d5..b0d6d0ab 100755 --- a/device-server/gradlew +++ b/device-server/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,7 +44,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" diff --git a/device-server/gradlew.bat b/device-server/gradlew.bat index e95643d6..15e1ee37 100644 --- a/device-server/gradlew.bat +++ b/device-server/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem http://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -14,7 +30,7 @@ set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome From 92bbdd70a34f2074ae0655d1485481980f4a2949 Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Wed, 3 Apr 2019 10:13:59 +0100 Subject: [PATCH 078/131] Allow to set custom workerGroupSize and callGroupSize for embedded Netty --- device-server/jar_launcher.sh | 4 ++++ .../main/kotlin/com/badoo/automation/deviceserver/Program.kt | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/device-server/jar_launcher.sh b/device-server/jar_launcher.sh index 5228d9f2..b7b6a59c 100755 --- a/device-server/jar_launcher.sh +++ b/device-server/jar_launcher.sh @@ -9,6 +9,8 @@ declare -r WDA_RUNNER=${DEVICE_SERVER_WDA_SIMULATOR_RUNNER:-'../ios/facebook/sim declare -r WDA_DEVICE_RUNNER=${DEVICE_SERVER_WDA_DEVICE_RUNNER:-'../ios/facebook/devices/WebDriverAgentRunner-Runner.app'} declare -r FBSIMCTL_VERSION=${DEVICE_SERVER_FBSIMCTL_VERSION:-'HEAD-d30c2a73'} declare -r LOG_CONFIG=${DEVICE_SERVER_LOG_CONFIG:-'logback-test.xml'} +declare -r NETTY_WORKER_GROUP_SIZE=${NETTY_WORKER_GROUP_SIZE:-''} +declare -r NETTY_CALL_GROUP_SIZE=${NETTY_CALL_GROUP_SIZE:-''} export JAVA_HOME=$(/usr/libexec/java_home -v 10 -F || /usr/libexec/java_home -v 9 -F) @@ -22,4 +24,6 @@ exec java \ -Dwda.bundle.path=${WDA_RUNNER} \ -Dwda.device.bundle.path=${WDA_DEVICE_RUNNER} \ -Dfbsimctl.version=${FBSIMCTL_VERSION} \ + -Dembedded.netty.workerGroupSize=${NETTY_WORKER_GROUP_SIZE} \ + -Dembedded.netty.callGroupSize=${NETTY_CALL_GROUP_SIZE} \ -jar ${DEVICE_SERVER_JAR} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/Program.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/Program.kt index 1c30b87d..98bd28fb 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/Program.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/Program.kt @@ -5,5 +5,8 @@ import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty fun main(args: Array) { - embeddedServer(Netty, commandLineEnvironment(args)).start(wait = true) + embeddedServer(Netty, commandLineEnvironment(args)) { + workerGroupSize = Integer.getInteger("embedded.netty.workerGroupSize", workerGroupSize) + callGroupSize = Integer.getInteger("embedded.netty.callGroupSize", callGroupSize) + }.start(wait = true) } \ No newline at end of file From 5b9b8be4fb0bf6e088b48d11856f9036b28e1bfd Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Tue, 9 Apr 2019 11:57:44 +0100 Subject: [PATCH 079/131] Cleanup simulator processes while starting Simulator node --- .../automation/deviceserver/NodeConfig.kt | 5 ++++- .../deviceserver/host/HostFactory.kt | 3 ++- .../automation/deviceserver/host/IRemote.kt | 2 +- .../automation/deviceserver/host/Remote.kt | 14 ++++++++++-- .../deviceserver/host/SimulatorsNode.kt | 1 + .../host/management/ISimulatorHostChecker.kt | 22 +++++++++++++++++-- 6 files changed, 40 insertions(+), 7 deletions(-) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/NodeConfig.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/NodeConfig.kt index 5f9c7df0..1493fd85 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/NodeConfig.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/NodeConfig.kt @@ -31,7 +31,10 @@ data class NodeConfig( val uninstallApps: Boolean = false, @JsonProperty("devices") - val knownDevices: List = emptyList() + val knownDevices: List = emptyList(), + + @JsonProperty("shutdown_simulators") + val shutdownSimulators: Boolean = false ) { enum class NodeType { diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt index af0953a1..8e736f1d 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt @@ -39,7 +39,8 @@ class HostFactory( remote, wdaBundle = wdaSimulatorBundle, remoteWdaBundleRoot = REMOTE_WDA_BUNDLE_ROOT, - fbsimctlVersion = fbsimctlVersion + fbsimctlVersion = fbsimctlVersion, + shutdownSimulators = config.shutdownSimulators ), simulatorLimit = config.simulatorLimit, concurrentBoots = config.concurrentBoots, diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt index a71ee594..0bfee96d 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt @@ -36,7 +36,7 @@ interface IRemote { */ fun captureFile(file: File): CommandResult - fun pkill(matchString: String) + fun pkill(matchString: String, force: Boolean) /** * Sends command to FBSimctl and expects JSON back from FBSimctl, diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt index d4237a39..8a453a45 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt @@ -75,8 +75,18 @@ class Remote( ) } - override fun pkill(matchString: String) { - execIgnoringErrors(listOf("pkill", "-9", "-f", matchString)) + private enum class Signal(val signal: Int) { + SIGKILL(9), + SIGTERM(15); + + override fun toString(): String { + return signal.toString() + } + } + + override fun pkill(matchString: String, force: Boolean) { + val signal = if (force) { Signal.SIGKILL } else { Signal.SIGTERM } + execIgnoringErrors(listOf("pkill", "-$signal", "-f", matchString)) } override fun isDirectory(path: String): Boolean { diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt index a830c435..49da7131 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt @@ -46,6 +46,7 @@ class SimulatorsNode( if (!remote.isLocalhost()) { hostChecker.copyWdaBundleToHost() } + hostChecker.cleanup() hostChecker.setupHost() logger.info(logMarker, "Prepared node ${remote.hostName}") diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt index 421c9de2..b8d4ebbb 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt @@ -25,7 +25,8 @@ class SimulatorHostChecker( private val diskCleanupInterval: Duration = Duration.ofMinutes(15), private val wdaBundle: File, private val remoteWdaBundleRoot: File, - private val fbsimctlVersion: String + private val fbsimctlVersion: String, + private val shutdownSimulators: Boolean ) : ISimulatorHostChecker { override val isWdaBundleDeployed: Boolean get() = remote.execIgnoringErrors(listOf("test", "-d", remoteWdaBundleRoot.absolutePath)).isSuccess @@ -76,9 +77,14 @@ class SimulatorHostChecker( } override fun cleanup() { + if (shutdownSimulators) { + cleanupSimulators() + cleanupSimulatorServices() + } + try { logger.info(logMarker, "Will kill abandoned long living fbsimctl processes") - remote.pkill("/usr/local/bin/fbsimctl") + remote.pkill("/usr/local/bin/fbsimctl", true) logger.info(logMarker, "Will shutdown booted simulators") remote.fbsimctl.shutdownAllBooted() logger.info(logMarker, "Done shutting down booted simulators") @@ -116,6 +122,18 @@ class SimulatorHostChecker( TimeUnit.MINUTES) } + private fun cleanupSimulators() { + remote.pkill("Simulator.app", false) // Simulator UI application + remote.pkill("launchd_sim", false) // main process for running simulator + } + + private fun cleanupSimulatorServices() { + val simulatorServices = listOf("CoreSimulatorService", "SimAudioProcessorService", "SimStreamProcessorService") + simulatorServices.forEach { + remote.pkill(it, true) + } + } + override fun setupHost() { // disable node hardware keyboard, i.e. use on-screen one remote.execIgnoringErrors("defaults write com.apple.iphonesimulator ConnectHardwareKeyboard -bool false".split(" ")) From dc563c67db1f175cb1466a989d08e632646d4524 Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Wed, 10 Apr 2019 15:31:28 +0100 Subject: [PATCH 080/131] Add System Log diagnostic Add endpoints to truncate and get content of System Log of a simulator ``` GET /devices/${ref}/system_log - get content of System Log DELETE /devices/${ref}/system_log - truncate System Log ``` --- .../automation/deviceserver/DeviceServer.kt | 12 +++++ .../controllers/DevicesController.kt | 10 ++++ .../deviceserver/data/Diagnostic.kt | 11 +++++ .../deviceserver/data/DiagnosticType.kt | 21 +++++++++ .../deviceserver/host/DevicesNode.kt | 9 ++++ .../deviceserver/host/ISimulatorsNode.kt | 3 ++ .../deviceserver/host/SimulatorsNode.kt | 16 +++++++ .../host/management/DeviceManager.kt | 8 ++++ .../host/management/IDeviceManager.kt | 2 + .../deviceserver/ios/simulator/ISimulator.kt | 2 + .../deviceserver/ios/simulator/Simulator.kt | 3 ++ .../ios/simulator/diagnostic/SystemLog.kt | 46 +++++++++++++++++++ 12 files changed, 143 insertions(+) create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/Diagnostic.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DiagnosticType.kt create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/SystemLog.kt diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt index 1d87d0d0..424338c4 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt @@ -217,6 +217,18 @@ fun Application.module() { call.respond(devicesController.uninstallApplication(ref, bundleId)) } } + route("diagnose/{type}") { + get { + val ref = param(call, "ref") + val type = param(call, "type") + call.respond(devicesController.getDiagnostic(ref, type)) + } + delete { + val ref = param(call, "ref") + val type = param(call, "type") + call.respond(devicesController.resetDiagnostic(ref, type)) + } + } route("video") { get { //FIXME: should be a better way of streaming a file over HTTP. without caching bytes in server's memory. Investigating ByteReadChannel diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt index a16c0d3f..7d4c3f2e 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt @@ -101,6 +101,16 @@ class DevicesController(private val deviceManager: IDeviceManager) { return happy } + fun getDiagnostic(ref: DeviceRef, type: String): Diagnostic { + val diagnosticType = DiagnosticType.fromString(type) + return deviceManager.getDiagnostic(ref, diagnosticType) + } + + fun resetDiagnostic(ref: DeviceRef, type: String) { + val diagnosticType = DiagnosticType.fromString(type) + deviceManager.resetDiagnostic(ref, diagnosticType) + } + fun getDeviceState(ref: DeviceRef): SimulatorStatusDTO { return deviceManager.getDeviceState(ref) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/Diagnostic.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/Diagnostic.kt new file mode 100644 index 00000000..5f1a0a23 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/Diagnostic.kt @@ -0,0 +1,11 @@ +package com.badoo.automation.deviceserver.data + +import com.fasterxml.jackson.annotation.JsonProperty + +data class Diagnostic( + @JsonProperty("type") + val type: DiagnosticType, + + @JsonProperty("content") + val content: String +) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DiagnosticType.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DiagnosticType.kt new file mode 100644 index 00000000..0eaa1b6c --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DiagnosticType.kt @@ -0,0 +1,21 @@ +package com.badoo.automation.deviceserver.data + +import com.fasterxml.jackson.annotation.JsonValue +import java.lang.IllegalArgumentException + +enum class DiagnosticType(@JsonValue val value: String) { + SystemLog("system_log"); + + companion object { + fun fromString(type: String): DiagnosticType { + val rv = DiagnosticType.values().find { it.value == type } + + if (rv == null) { + val msg = "Diagnostic type $type is not one of ${DiagnosticType.values().joinToString(", ")}" + throw IllegalArgumentException(msg) + } + + return rv + } + } +} diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt index f3a81eb7..6aac9d5b 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt @@ -41,6 +41,15 @@ class DevicesNode( get() = remote.isLocalhost() || isWebDriverAgentDeployed private val deviceRegistrationInterval = Duration.ofMinutes(1) + + override fun getDiagnostic(deviceRef: DeviceRef, type: DiagnosticType): Diagnostic { + throw(NotImplementedError("Diagnostic is not supported by physical devices")) + } + + override fun resetDiagnostic(deviceRef: DeviceRef, type: DiagnosticType) { + throw(NotImplementedError("Diagnostic is not supported by physical devices")) + } + override val remoteAddress: String get() = remote.hostName private val deviceInfoProvider = DeviceInfoProvider(remote) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt index dff28d01..56a1296a 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt @@ -26,6 +26,9 @@ interface ISimulatorsNode { fun listFiles(deviceRef: DeviceRef, dataPath: DataPath): List fun pullFile(deviceRef: DeviceRef, dataPath: DataPath): ByteArray + fun getDiagnostic(deviceRef: DeviceRef, type: DiagnosticType): Diagnostic + fun resetDiagnostic(deviceRef: DeviceRef, type: DiagnosticType) + val remoteAddress: String fun isReachable(): Boolean fun prepareNode() diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt index 49da7131..df025c8d 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt @@ -102,6 +102,22 @@ class SimulatorsNode( } } + override fun getDiagnostic(deviceRef: DeviceRef, type: DiagnosticType): Diagnostic { + when (type) { + DiagnosticType.SystemLog -> return Diagnostic( + type = type, + content = getDeviceFor(deviceRef).systemLog.content() + ) + else -> throw RuntimeException("Diagnostic $type is not supported") + } + } + + override fun resetDiagnostic(deviceRef: DeviceRef, type: DiagnosticType) { + when (type) { + DiagnosticType.SystemLog -> getDeviceFor(deviceRef).systemLog.truncate() + } + } + private fun simulatorToDTO(device: ISimulator): DeviceDTO { with(device) { return DeviceDTO( diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt index 954ec304..7851432d 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt @@ -172,4 +172,12 @@ class DeviceManager( override fun setEnvironmentVariables(ref: DeviceRef, envs: Map) { nodeRegistry.activeDevices.getNodeFor(ref).setEnvironmentVariables(ref, envs) } + + override fun getDiagnostic(ref: DeviceRef, type: DiagnosticType): Diagnostic { + return nodeRegistry.activeDevices.getNodeFor(ref).getDiagnostic(ref, type) + } + + override fun resetDiagnostic(ref: DeviceRef, type: DiagnosticType) { + nodeRegistry.activeDevices.getNodeFor(ref).resetDiagnostic(ref, type) + } } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt index dbaf33e8..ccba5ec1 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt @@ -32,4 +32,6 @@ interface IDeviceManager { fun pullFile(ref: DeviceRef, dataPath: DataPath): ByteArray fun uninstallApplication(ref: String, bundleId: String) fun setEnvironmentVariables(ref: DeviceRef, envs: Map) + fun getDiagnostic(ref: DeviceRef, type: DiagnosticType): Diagnostic + fun resetDiagnostic(ref: DeviceRef, type: DiagnosticType) } diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt index bcda57ca..2c355589 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt @@ -2,6 +2,7 @@ package com.badoo.automation.deviceserver.ios.simulator import com.badoo.automation.deviceserver.data.* import com.badoo.automation.deviceserver.ios.simulator.data.DataContainer +import com.badoo.automation.deviceserver.ios.simulator.diagnostic.SystemLog import com.badoo.automation.deviceserver.ios.simulator.video.SimulatorVideoRecorder import java.net.URI import java.net.URL @@ -19,6 +20,7 @@ interface ISimulator { val calabashPort: Int val videoRecorder: SimulatorVideoRecorder val fbsimctlSubject: String + val systemLog: SystemLog fun prepareAsync() fun resetAsync() diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt index 87cd017f..697838ca 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt @@ -12,6 +12,7 @@ import com.badoo.automation.deviceserver.ios.simulator.backup.ISimulatorBackup import com.badoo.automation.deviceserver.ios.simulator.backup.SimulatorBackup import com.badoo.automation.deviceserver.ios.simulator.data.DataContainer import com.badoo.automation.deviceserver.ios.simulator.data.FileSystem +import com.badoo.automation.deviceserver.ios.simulator.diagnostic.SystemLog import com.badoo.automation.deviceserver.ios.simulator.video.SimulatorVideoRecorder import com.badoo.automation.deviceserver.util.executeWithTimeout import com.badoo.automation.deviceserver.util.pollFor @@ -61,6 +62,8 @@ class Simulator ( override val videoRecorder: SimulatorVideoRecorder = SimulatorVideoRecorder(deviceInfo, remote, location = recordingLocation) + override val systemLog = SystemLog(remote, udid) + //region instance state variables private val deviceLock = ReentrantLock() @Volatile private var deviceState: DeviceState = DeviceState.NONE // writing from separate thread diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/SystemLog.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/SystemLog.kt new file mode 100644 index 00000000..21be5187 --- /dev/null +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/SystemLog.kt @@ -0,0 +1,46 @@ +package com.badoo.automation.deviceserver.ios.simulator.diagnostic + +import com.badoo.automation.deviceserver.LogMarkers +import com.badoo.automation.deviceserver.command.ShellUtils +import com.badoo.automation.deviceserver.data.UDID +import com.badoo.automation.deviceserver.host.IRemote +import net.logstash.logback.marker.MapEntriesAppendingMarker +import org.slf4j.LoggerFactory +import org.slf4j.Marker +import java.io.File + +class SystemLog( + private val remote: IRemote, + private val udid: UDID +) { + private val logger = LoggerFactory.getLogger(javaClass.simpleName) + private val logMarker: Marker = MapEntriesAppendingMarker( + mapOf( + LogMarkers.UDID to udid, + LogMarkers.HOSTNAME to remote.hostName + ) + ) + + fun truncate(): Boolean { + val path = remote.fbsimctl.diagnose(udid).sysLogLocation ?: return false + + val rv = remote.shell("echo > ${ShellUtils.escape(path)}", returnOnFailure = true) + + return rv.isSuccess + } + + fun content(): String { + val path = remote.fbsimctl.diagnose(udid).sysLogLocation + ?: throw RuntimeException("Could not determine System Log path") + + val result = remote.captureFile(File(path)) + + if (!result.isSuccess) { + val message = "Could not read System Log: $result" + logger.error(logMarker, message) + throw RuntimeException(message) + } + + return result.stdOut + } +} From 3d4c63d773df8607465d9f4b950daba3fd4e18b0 Mon Sep 17 00:00:00 2001 From: Nikolai Abalov Date: Thu, 11 Apr 2019 09:32:48 +0100 Subject: [PATCH 081/131] Add a comment on system log truncation --- .../deviceserver/ios/simulator/diagnostic/SystemLog.kt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/SystemLog.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/SystemLog.kt index 21be5187..082d2af9 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/SystemLog.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/SystemLog.kt @@ -24,6 +24,11 @@ class SystemLog( fun truncate(): Boolean { val path = remote.fbsimctl.diagnose(udid).sysLogLocation ?: return false + // Following command can be used to correctly rotate syslog. + // newsyslog -R "Log file rotated" $path + // But it requires privilege and we don't actually care about correct rotation + // (as we consider simulators and their logs to be ephemeral). + // All we want is to truncate log. val rv = remote.shell("echo > ${ShellUtils.escape(path)}", returnOnFailure = true) return rv.isSuccess From 579cd02c8935ce89d7472885e8f72ad3f8dfc43b Mon Sep 17 00:00:00 2001 From: Vyacheslav Frolov Date: Thu, 11 Apr 2019 08:54:51 +0100 Subject: [PATCH 082/131] Remove interface IDeviceManager --- .../controllers/DevicesController.kt | 4 +- .../controllers/StatusController.kt | 4 +- .../host/management/DeviceManager.kt | 62 +++++++++---------- .../host/management/IAutoreleaseLooper.kt | 2 +- .../host/management/IDeviceManager.kt | 37 ----------- .../host/management/util/AutoreleaseLooper.kt | 6 +- .../controllers/DevicesControllerTest.kt | 4 +- .../controllers/StatusControllerTest.kt | 4 +- 8 files changed, 43 insertions(+), 80 deletions(-) delete mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt index 7d4c3f2e..a7b03865 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt @@ -3,11 +3,11 @@ package com.badoo.automation.deviceserver.controllers import com.badoo.automation.deviceserver.EmptyMap import com.badoo.automation.deviceserver.JsonMapper import com.badoo.automation.deviceserver.data.* -import com.badoo.automation.deviceserver.host.management.IDeviceManager +import com.badoo.automation.deviceserver.host.management.DeviceManager import com.fasterxml.jackson.databind.JsonNode import io.ktor.auth.UserIdPrincipal -class DevicesController(private val deviceManager: IDeviceManager) { +class DevicesController(private val deviceManager: DeviceManager) { private val happy = emptyMap() fun getDeviceRefs(): List { diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/StatusController.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/StatusController.kt index 4b5af1c1..077edf64 100644 --- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/StatusController.kt +++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/StatusController.kt @@ -1,9 +1,9 @@ package com.badoo.automation.deviceserver.controllers -import com.badoo.automation.deviceserver.host.management.IDeviceManager +import com.badoo.automation.deviceserver.host.management.DeviceManager import io.ktor.routing.Route -class StatusController(private val deviceManager: IDeviceManager) { +class StatusController(private val deviceManager: DeviceManager) { fun welcomeMessage(route: Route?): String { return "Welcome to the device server.

\n" +
                 (if (route == null) "No routes set yet!?" else childHierarchy(route)) + "\n" +
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
index 7851432d..af9d62e9 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
@@ -17,7 +17,7 @@ class DeviceManager(
         nodeFactory: IHostFactory,
         activeDevices: ActiveDevices = ActiveDevices(),
         private val autoreleaseLooper: IAutoreleaseLooper = AutoreleaseLooper()
-) : IDeviceManager {
+) {
     private val logger = LoggerFactory.getLogger(javaClass.simpleName)
     private val deviceTimeoutInSecs: Duration
     private val nodeRegistry = NodeRegistry(activeDevices)
@@ -46,90 +46,90 @@ class DeviceManager(
         autoreleaseLooper.autoreleaseLoop(this)
     }
 
-    override fun getStatus(): Map {
+    fun getStatus(): Map {
         return mapOf(
             "initialized" to nodeRegistry.getInitialRegistrationComplete(),
             "sessions" to listOf(nodeRegistry.activeDevices.getStatus()).toString()
         )
     }
 
-    override fun readyForRelease(): List {
+    fun readyForRelease(): List {
         return nodeRegistry.activeDevices.readyForRelease()
     }
 
-    override fun nextReleaseAtSeconds(): Long {
+    fun nextReleaseAtSeconds(): Long {
         return nodeRegistry.activeDevices.nextReleaseAtSeconds()
     }
 
-    override fun getTotalCapacity(desiredCaps: DesiredCapabilities): Map {
+    fun getTotalCapacity(desiredCaps: DesiredCapabilities): Map {
         return nodeRegistry.capacitiesTotal(desiredCaps)
     }
 
-    override fun getGetDeviceDTO(ref: DeviceRef): DeviceDTO {
+    fun getGetDeviceDTO(ref: DeviceRef): DeviceDTO {
         return nodeRegistry.activeDevices.getNodeFor(ref).getDeviceDTO(ref)
     }
 
-    override fun clearSafariCookies(ref: DeviceRef) {
+    fun clearSafariCookies(ref: DeviceRef) {
         nodeRegistry.activeDevices.getNodeFor(ref).clearSafariCookies(ref)
     }
 
-    override fun resetAsyncDevice(ref: DeviceRef) {
+    fun resetAsyncDevice(ref: DeviceRef) {
         nodeRegistry.activeDevices.getNodeFor(ref).resetAsync(ref)
     }
 
-    override fun  approveAccess(ref: DeviceRef, bundleId: String) {
+    fun  approveAccess(ref: DeviceRef, bundleId: String) {
         nodeRegistry.activeDevices.getNodeFor(ref).approveAccess(ref, bundleId)
     }
 
-    override fun setPermissions(ref: DeviceRef, permissions: AppPermissionsDto) {
+    fun setPermissions(ref: DeviceRef, permissions: AppPermissionsDto) {
         nodeRegistry.activeDevices.getNodeFor(ref).setPermissions(ref, permissions)
     }
 
-    override fun getEndpointFor(ref: DeviceRef, port: Int): URL {
+    fun getEndpointFor(ref: DeviceRef, port: Int): URL {
         return nodeRegistry.activeDevices.getNodeFor(ref).endpointFor(ref, port)
     }
 
-    override fun crashLogs(ref: DeviceRef, pastMinutes: Long?): List {
+    fun crashLogs(ref: DeviceRef, pastMinutes: Long?): List {
         return nodeRegistry.activeDevices.getNodeFor(ref).crashLogs(ref, pastMinutes)
     }
 
-    override fun deleteCrashLogs(ref: DeviceRef): Boolean {
+    fun deleteCrashLogs(ref: DeviceRef): Boolean {
         return nodeRegistry.activeDevices.getNodeFor(ref).deleteCrashLogs(ref)
     }
 
-    override fun getLastCrashLog(ref: DeviceRef): CrashLog {
+    fun getLastCrashLog(ref: DeviceRef): CrashLog {
         return nodeRegistry.activeDevices.getNodeFor(ref).lastCrashLog(ref)
     }
 
-    override fun shake(ref: DeviceRef) {
+    fun shake(ref: DeviceRef) {
         nodeRegistry.activeDevices.getNodeFor(ref).shake(ref)
     }
 
-    override fun startVideo(ref: DeviceRef) {
+    fun startVideo(ref: DeviceRef) {
         nodeRegistry.activeDevices.getNodeFor(ref).videoRecordingStart(ref)
     }
 
-    override fun stopVideo(ref: DeviceRef) {
+    fun stopVideo(ref: DeviceRef) {
         nodeRegistry.activeDevices.getNodeFor(ref).videoRecordingStop(ref)
     }
 
-    override fun getVideo(ref: DeviceRef): ByteArray {
+    fun getVideo(ref: DeviceRef): ByteArray {
         return nodeRegistry.activeDevices.getNodeFor(ref).videoRecordingGet(ref)
     }
 
-    override fun deleteVideo(ref: DeviceRef) {
+    fun deleteVideo(ref: DeviceRef) {
         nodeRegistry.activeDevices.getNodeFor(ref).videoRecordingDelete(ref)
     }
 
-    override fun uninstallApplication(ref: DeviceRef, bundleId: String) {
+    fun uninstallApplication(ref: DeviceRef, bundleId: String) {
         nodeRegistry.activeDevices.getNodeFor(ref).uninstallApplication(ref, bundleId)
     }
 
-    override fun getDeviceState(ref: DeviceRef): SimulatorStatusDTO {
+    fun getDeviceState(ref: DeviceRef): SimulatorStatusDTO {
         return nodeRegistry.activeDevices.getNodeFor(ref).state(ref)
     }
 
-    override fun createDeviceAsync(desiredCaps: DesiredCapabilities, userId: String?): DeviceDTO {
+    fun createDeviceAsync(desiredCaps: DesiredCapabilities, userId: String?): DeviceDTO {
         try {
             return nodeRegistry.createDeviceAsync(desiredCaps, deviceTimeoutInSecs, userId)
         } catch(e: NoNodesRegisteredException) {
@@ -139,7 +139,7 @@ class DeviceManager(
         }
     }
 
-    override fun deleteReleaseDevice(ref: DeviceRef, reason: String) {
+    fun deleteReleaseDevice(ref: DeviceRef, reason: String) {
         try { // using try-catch here not to expose tryGetNodeFor
             nodeRegistry.activeDevices.releaseDevice(ref, reason)
         } catch (e: DeviceNotFoundException) {
@@ -148,36 +148,36 @@ class DeviceManager(
         }
     }
 
-    override fun getDeviceRefs() : List {
+    fun getDeviceRefs() : List {
         return nodeRegistry.activeDevices.deviceList()
     }
 
-    override fun releaseUserDevices(userId: String, reason: String) {
+    fun releaseUserDevices(userId: String, reason: String) {
         val devices = nodeRegistry.activeDevices.getUserDeviceRefs(userId)
         nodeRegistry.activeDevices.releaseDevices(devices, reason)
     }
 
-    override fun isReady(): Boolean {
+    fun isReady(): Boolean {
         return nodeRegistry.getInitialRegistrationComplete()
     }
 
-    override fun listFiles(ref: DeviceRef, dataPath: DataPath): List {
+    fun listFiles(ref: DeviceRef, dataPath: DataPath): List {
         return nodeRegistry.activeDevices.getNodeFor(ref).listFiles(ref, dataPath)
     }
 
-    override fun pullFile(ref: DeviceRef, dataPath: DataPath): ByteArray {
+    fun pullFile(ref: DeviceRef, dataPath: DataPath): ByteArray {
         return nodeRegistry.activeDevices.getNodeFor(ref).pullFile(ref, dataPath)
     }
 
-    override fun setEnvironmentVariables(ref: DeviceRef, envs: Map) {
+    fun setEnvironmentVariables(ref: DeviceRef, envs: Map) {
         nodeRegistry.activeDevices.getNodeFor(ref).setEnvironmentVariables(ref, envs)
     }
 
-    override fun getDiagnostic(ref: DeviceRef, type: DiagnosticType): Diagnostic {
+    fun getDiagnostic(ref: DeviceRef, type: DiagnosticType): Diagnostic {
         return nodeRegistry.activeDevices.getNodeFor(ref).getDiagnostic(ref, type)
     }
 
-    override fun resetDiagnostic(ref: DeviceRef, type: DiagnosticType) {
+    fun resetDiagnostic(ref: DeviceRef, type: DiagnosticType) {
         nodeRegistry.activeDevices.getNodeFor(ref).resetDiagnostic(ref, type)
     }
 }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IAutoreleaseLooper.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IAutoreleaseLooper.kt
index a807739a..7c20a046 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IAutoreleaseLooper.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IAutoreleaseLooper.kt
@@ -1,5 +1,5 @@
 package com.badoo.automation.deviceserver.host.management
 
 interface IAutoreleaseLooper {
-    fun autoreleaseLoop(deviceManager: IDeviceManager)
+    fun autoreleaseLoop(deviceManager: DeviceManager)
 }
\ No newline at end of file
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt
deleted file mode 100644
index ccba5ec1..00000000
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IDeviceManager.kt
+++ /dev/null
@@ -1,37 +0,0 @@
-package com.badoo.automation.deviceserver.host.management
-
-import com.badoo.automation.deviceserver.data.*
-import java.net.URL
-
-interface IDeviceManager {
-    fun getDeviceRefs() : List
-    fun createDeviceAsync(desiredCaps: DesiredCapabilities, userId: String?): DeviceDTO
-    fun deleteReleaseDevice(ref: DeviceRef, reason: String)
-    fun getGetDeviceDTO(ref: DeviceRef): DeviceDTO
-    fun clearSafariCookies(ref: DeviceRef)
-    fun resetAsyncDevice(ref: DeviceRef)
-    fun approveAccess(ref: DeviceRef, bundleId: String)
-    fun setPermissions(ref: DeviceRef, permissions: AppPermissionsDto)
-    fun getEndpointFor(ref: DeviceRef, port: Int): URL
-    fun crashLogs(ref: DeviceRef, pastMinutes: Long?): List
-    fun deleteCrashLogs(ref: DeviceRef): Boolean
-    fun getLastCrashLog(ref: DeviceRef): CrashLog
-    fun shake(ref: DeviceRef)
-    fun startVideo(ref: DeviceRef)
-    fun stopVideo(ref: DeviceRef)
-    fun getVideo(ref: DeviceRef): ByteArray
-    fun deleteVideo(ref: DeviceRef)
-    fun getDeviceState(ref: DeviceRef): SimulatorStatusDTO
-    fun getTotalCapacity(desiredCaps: DesiredCapabilities): Map
-    fun nextReleaseAtSeconds(): Long
-    fun readyForRelease(): List
-    fun getStatus(): Map
-    fun releaseUserDevices(userId: String, reason: String)
-    fun isReady(): Boolean
-    fun listFiles(ref: DeviceRef, dataPath: DataPath): List
-    fun pullFile(ref: DeviceRef, dataPath: DataPath): ByteArray
-    fun uninstallApplication(ref: String, bundleId: String)
-    fun setEnvironmentVariables(ref: DeviceRef, envs: Map)
-    fun getDiagnostic(ref: DeviceRef, type: DiagnosticType): Diagnostic
-    fun resetDiagnostic(ref: DeviceRef, type: DiagnosticType)
-}
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/util/AutoreleaseLooper.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/util/AutoreleaseLooper.kt
index 31dd0fe7..d9b618d9 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/util/AutoreleaseLooper.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/util/AutoreleaseLooper.kt
@@ -1,7 +1,7 @@
 package com.badoo.automation.deviceserver.host.management.util
 
 import com.badoo.automation.deviceserver.host.management.IAutoreleaseLooper
-import com.badoo.automation.deviceserver.host.management.IDeviceManager
+import com.badoo.automation.deviceserver.host.management.DeviceManager
 import com.badoo.automation.deviceserver.util.executeWithTimeout
 import kotlinx.coroutines.experimental.delay
 import kotlinx.coroutines.experimental.launch
@@ -16,7 +16,7 @@ class AutoreleaseLooper : IAutoreleaseLooper {
     private val autoreleaseThreadPool = newFixedThreadPoolContext(1, "AutoreleaseLoop")
     private val logger = LoggerFactory.getLogger(javaClass.simpleName)
 
-    override fun autoreleaseLoop(deviceManager: IDeviceManager) {
+    override fun autoreleaseLoop(deviceManager: DeviceManager) {
         launch(autoreleaseThreadPool) {
             while (isActive) {
                 try {
@@ -33,7 +33,7 @@ class AutoreleaseLooper : IAutoreleaseLooper {
 
     private fun currentTimeSeconds() = System.currentTimeMillis() / 1000
 
-    private fun autoRelease(deviceManager: IDeviceManager) {
+    private fun autoRelease(deviceManager: DeviceManager) {
         val jobs = deviceManager.readyForRelease().map { deviceRef ->
             launch {
                 val message = "Failed to release device $deviceRef"
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/DevicesControllerTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/DevicesControllerTest.kt
index e2431dd2..d5171e1c 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/DevicesControllerTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/DevicesControllerTest.kt
@@ -2,7 +2,7 @@ package com.badoo.automation.deviceserver.controllers
 
 import com.badoo.automation.deviceserver.data.*
 import com.badoo.automation.deviceserver.deviceDTOStub
-import com.badoo.automation.deviceserver.host.management.IDeviceManager
+import com.badoo.automation.deviceserver.host.management.DeviceManager
 import com.badoo.automation.deviceserver.json
 import com.badoo.automation.deviceserver.mockThis
 import com.nhaarman.mockito_kotlin.whenever
@@ -17,7 +17,7 @@ import java.net.URL
 private val happyEmpty: Map = mapOf()
 
 class DevicesControllerTest {
-    private var deviceManager: IDeviceManager = mockThis()
+    private var deviceManager: DeviceManager = mockThis()
     private var expectedArray = ByteArray(3)
 
     private var deviceServer = DevicesController(deviceManager)
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/StatusControllerTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/StatusControllerTest.kt
index f0abf246..6b395382 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/StatusControllerTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/controllers/StatusControllerTest.kt
@@ -1,13 +1,13 @@
 package com.badoo.automation.deviceserver.controllers
 
-import com.badoo.automation.deviceserver.host.management.IDeviceManager
+import com.badoo.automation.deviceserver.host.management.DeviceManager
 import com.badoo.automation.deviceserver.mockThis
 import org.hamcrest.CoreMatchers
 import org.junit.Assert
 import org.junit.Test
 
 class StatusControllerTest {
-    private var deviceManager: IDeviceManager = mockThis()
+    private var deviceManager: DeviceManager = mockThis()
     private var statusController = StatusController(deviceManager)
     @Test
     fun getServerStatus() {

From 9787825528e2517949392f41c478f8d7c2bce4a2 Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Thu, 11 Apr 2019 11:33:54 +0100
Subject: [PATCH 083/131] Distinguish Nodes by hostname

---
 .../automation/deviceserver/DeviceServer.kt    |  2 +-
 .../deviceserver/DeviceServerConfig.kt         |  2 +-
 .../automation/deviceserver/NodeConfig.kt      | 15 +++++++++++++++
 .../deviceserver/host/DevicesNode.kt           | 16 ++++++++++++++++
 .../deviceserver/host/HostFactory.kt           |  4 +++-
 .../deviceserver/host/SimulatorsNode.kt        | 18 +++++++++++++++++-
 .../host/management/NodeRegistrar.kt           |  2 +-
 .../deviceserver/DeviceServerConfigTest.kt     | 17 +++++++++++++++++
 .../deviceserver/host/SimulatorsNodeTest.kt    |  2 ++
 .../deviceserver/ios/DeviceManagerTest.kt      |  2 +-
 10 files changed, 74 insertions(+), 6 deletions(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
index 424338c4..a02e3e8a 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
@@ -76,7 +76,7 @@ private fun serverConfig(): DeviceServerConfig {
     if (appConfiguration.deviceServerConfigPath.isEmpty()) {
         val defaultNodeConfig = NodeConfig()
         logger.info("Using default config: $defaultNodeConfig")
-        return DeviceServerConfig(nodes = listOf(defaultNodeConfig), timeouts = emptyMap())
+        return DeviceServerConfig(nodes = setOf(defaultNodeConfig), timeouts = emptyMap())
     }
 
     val configFile = File(appConfiguration.deviceServerConfigPath)
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServerConfig.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServerConfig.kt
index 64d3e5e3..ed585bc7 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServerConfig.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServerConfig.kt
@@ -5,5 +5,5 @@ import com.fasterxml.jackson.annotation.JsonIgnoreProperties
 @JsonIgnoreProperties( ignoreUnknown = true )
 data class DeviceServerConfig(
     val timeouts: Map,
-    val nodes: List
+    val nodes: Set
 )
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/NodeConfig.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/NodeConfig.kt
index 1493fd85..4aef4282 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/NodeConfig.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/NodeConfig.kt
@@ -44,4 +44,19 @@ data class NodeConfig(
         @JsonProperty("devices")
         Devices
     }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as NodeConfig
+
+        if (publicHost != other.publicHost) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        return publicHost.hashCode()
+    }
 }
\ No newline at end of file
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
index 6aac9d5b..ca380cfd 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
@@ -19,6 +19,7 @@ import java.util.concurrent.TimeUnit
 
 class DevicesNode(
     private val remote: IRemote,
+    private val publicHostName: String,
     portAllocator: PortAllocator = PortAllocator(),
     wdaRunnerXctest: File,
     knownDevices: List,
@@ -353,4 +354,19 @@ class DevicesNode(
     override fun setEnvironmentVariables(deviceRef: DeviceRef, envs: Map) {
         throw(NotImplementedError("Setting environment variables is not supported by physical devices"))
     }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as DevicesNode
+
+        if (publicHostName != other.publicHostName) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        return publicHostName.hashCode()
+    }
 }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt
index 8e736f1d..aea4493c 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt
@@ -35,6 +35,7 @@ class HostFactory(
         return if (config.type == NodeConfig.NodeType.Simulators) {
             SimulatorsNode(
                 remote = remote,
+                publicHostName = publicHostName,
                 hostChecker = SimulatorHostChecker(
                     remote,
                     wdaBundle = wdaSimulatorBundle,
@@ -48,7 +49,8 @@ class HostFactory(
             )
         } else {
             DevicesNode(
-                remote,
+                remote = remote,
+                publicHostName = publicHostName,
                 whitelistedApps = config.whitelistApps,
                 knownDevices = config.knownDevices,
                 uninstallApps = config.uninstallApps,
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
index df025c8d..39245b96 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
@@ -20,6 +20,7 @@ import java.util.concurrent.ConcurrentHashMap
 
 class SimulatorsNode(
         val remote: IRemote,
+        private val publicHostName: String,
         private val hostChecker: ISimulatorHostChecker,
         private val simulatorLimit: Int,
         concurrentBoots: Int,
@@ -29,7 +30,7 @@ class SimulatorsNode(
         private val simulatorFactory: ISimulatorFactory = object : ISimulatorFactory {}
 ) : ISimulatorsNode {
 
-    override val remoteAddress: String get() = remote.hostName
+    override val remoteAddress: String get() = publicHostName
 
     private val logger = LoggerFactory.getLogger(javaClass.simpleName)
     private val logMarker = MapEntriesAppendingMarker(mapOf(
@@ -270,4 +271,19 @@ class SimulatorsNode(
     override fun toString(): String {
         return "${javaClass.simpleName} at $remoteAddress"
     }
+
+    override fun equals(other: Any?): Boolean {
+        if (this === other) return true
+        if (javaClass != other?.javaClass) return false
+
+        other as SimulatorsNode
+
+        if (publicHostName != other.publicHostName) return false
+
+        return true
+    }
+
+    override fun hashCode(): Int {
+        return publicHostName.hashCode()
+    }
 }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt
index 2a7fdff9..0e79c22c 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt
@@ -7,7 +7,7 @@ import java.util.concurrent.Executors
 import java.util.concurrent.Future
 
 class NodeRegistrar(
-    nodesConfig: List,
+    nodesConfig: Set,
     nodeFactory: IHostFactory,
     private val nodeRegistry: NodeRegistry,
     private val registrationInterval: Duration = DEFAULT_REGISTRATION_INTERVAL
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/DeviceServerConfigTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/DeviceServerConfigTest.kt
index 4bf3bb7e..552119d1 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/DeviceServerConfigTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/DeviceServerConfigTest.kt
@@ -20,4 +20,21 @@ class DeviceServerConfigTest {
         Assert.assertEquals(600, config.timeouts["device"]?.toInt())
         Assert.assertEquals(NodeConfig(), config.nodes.first())
     }
+
+    @Test
+    fun shouldIgnoreDuplicatingNodes() {
+        val configWithDuplicatingNodes = """
+        {
+            "timeouts": {
+                "device": 600
+            },
+            "nodes": [
+                {"user":"zz","host":"node1.co.uk","simulator_limit":1},
+                {"user":"zz","host":"node1.co.uk","simulator_limit":1}
+            ]
+        }
+        """.trimMargin()
+        val config = JsonMapper().fromJson(configWithDuplicatingNodes)
+        Assert.assertEquals(1, config.nodes.size)
+    }
 }
\ No newline at end of file
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt
index 16778c79..f67c80dc 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt
@@ -56,8 +56,10 @@ class SimulatorsNodeTest {
     private val configuredSimulatorLimit = 3
 
     private val simulatorFactory: ISimulatorFactory = mockThis()
+    private val publicHostName = "hostname"
     private val simulatorsNode1 = SimulatorsNode(
             iRemote,
+            publicHostName,
             hostChecker,
             configuredSimulatorLimit,
             2,
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/DeviceManagerTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/DeviceManagerTest.kt
index 31be6ba8..8cdb5697 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/DeviceManagerTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/DeviceManagerTest.kt
@@ -58,7 +58,7 @@ class DeviceManagerTest {
     private val deviceManager = DeviceManager(
             DeviceServerConfig(
                     emptyMap(),
-                    listOf()
+                    setOf()
             ),
             hostFactory,
             activeDevices,

From ee4e47ddd51e6682903b92c29a1aac48a4418b2b Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Thu, 21 Mar 2019 20:29:42 +0000
Subject: [PATCH 084/131] Use lightweight way to check simulator status. Make
 'get simulator status' idempotent.

---
 .../deviceserver/data/SimulatorStatus.kt      |   5 +-
 .../deviceserver/ios/device/Device.kt         |   2 -
 .../deviceserver/ios/simulator/Simulator.kt   | 104 ++++++------------
 3 files changed, 38 insertions(+), 73 deletions(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SimulatorStatus.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SimulatorStatus.kt
index 1c493046..47b3be23 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SimulatorStatus.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/SimulatorStatus.kt
@@ -1,9 +1,10 @@
 package com.badoo.automation.deviceserver.data
 
 data class SimulatorStatus(
-        var isReady: Boolean = false,
         var wdaStatus: Boolean = false,
         var fbsimctlStatus: Boolean = false,
         @Volatile var wdaStatusRetries: Int = 0,
         @Volatile var fbsimctlStatusRetries: Int = 0
-)
+) {
+    val isReady: Boolean get() = wdaStatus && fbsimctlStatus
+}
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt
index 17743645..ae4c9c62 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt
@@ -96,7 +96,6 @@ class Device(
     private fun refreshStatus() {
         val previousFbSimctlStatus = status.fbsimctlStatus
 
-        status.isReady = false
         status.fbsimctlStatus = false
         status.wdaStatus = false
 
@@ -138,7 +137,6 @@ class Device(
             lastException = RuntimeException(message)
         }
 
-        status.isReady = isBooted && wdaStatus && fbsimctlStatus
         status.fbsimctlStatus = fbsimctlStatus
         status.wdaStatus = wdaStatus
     }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
index 697838ca..f894579c 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
@@ -73,7 +73,6 @@ class Simulator (
     private val fbsimctlProc: FbsimctlProc = FbsimctlProc(remote, deviceInfo.udid, fbsimctlEndpoint, headless)
     private val wdaProc = SimulatorWebDriverAgent(remote, wdaRunnerXctest, deviceInfo.udid, wdaEndpoint)
     private val backup: ISimulatorBackup = SimulatorBackup(remote, udid, deviceSetPath)
-    private val simulatorStatus = SimulatorStatus()
     private val logger = LoggerFactory.getLogger(javaClass.simpleName)
     private val commonLogMarkerDetails = mapOf(
             LogMarkers.DEVICE_REF to deviceRef,
@@ -94,9 +93,21 @@ class Simulator (
 
     //region prepareAsync
     override fun prepareAsync() {
+        executeCritical {
+            deviceState = DeviceState.CREATING
+        }
+
         executeCriticalAsync {
             val elapsed = measureTimeMillis {
-                prepare(clean = true)
+                try {
+                    prepare(clean = true)
+                } catch (e: Exception) { // catching most wide exception
+                    executeCritical {
+                        deviceState = DeviceState.FAILED
+                    }
+                    logger.error(logMarker, "Failed to prepare device ${this@Simulator}", e)
+                    throw e
+                }
             }
             logger.info(logMarker, "Device ${this@Simulator} ready in ${elapsed / 1000} seconds")
         }
@@ -311,7 +322,15 @@ class Simulator (
             // FIXME: check for it.isActive to help to cancel long running tasks
             val elapsed = measureTimeMillis {
                 resetFromBackup()
-                prepare(clean = false) // simulator is already clean as it was restored from backup in resetFromBackup
+                try {
+                    prepare(clean = false) // simulator is already clean as it was restored from backup in resetFromBackup
+                } catch (e: Exception) { // catching most wide exception
+                    executeCritical {
+                        deviceState = DeviceState.FAILED
+                    }
+                    logger.error(logMarker, "Failed to reset and prepare device ${this@Simulator}", e)
+                    throw e
+                }
             }
             logger.info(logMarker, "Device ${this@Simulator} reset and ready in ${elapsed / 1000} seconds")
         }
@@ -382,11 +401,20 @@ class Simulator (
 
     //region simulator status
     override fun status(): SimulatorStatusDTO {
-        refreshStatus()
+        var isFbsimctlReady = false
+        var isWdaReady = false
+
+        if (deviceState == DeviceState.CREATED) {
+            isFbsimctlReady = fbsimctlProc.isHealthy()
+            isWdaReady = (if (useWda) { wdaProc.isHealthy() } else true)
+        }
+
+        val isSimulatorReady = deviceState == DeviceState.CREATED && isFbsimctlReady && isWdaReady
+
         return SimulatorStatusDTO(
-                ready = deviceState == DeviceState.CREATED && simulatorStatus.isReady,
-                wda_status = simulatorStatus.wdaStatus,
-                fbsimctl_status = simulatorStatus.fbsimctlStatus,
+                ready = isSimulatorReady,
+                wda_status = isWdaReady,
+                fbsimctl_status = isFbsimctlReady,
                 state = deviceState.value,
                 last_error = lastException?.toDTO()
         )
@@ -400,68 +428,6 @@ class Simulator (
             stackTrace = stackTrace.map { it.toString() }
         )
     }
-
-    private fun refreshStatus() {
-        simulatorStatus.isReady = false
-        simulatorStatus.fbsimctlStatus = false
-        simulatorStatus.wdaStatus = false
-
-        if (deviceState != DeviceState.CREATED) {
-            return
-        }
-
-        val device = remote.fbsimctl.listDevice(udid) ?: return
-
-        if (device.state != FBSimctlDeviceState.BOOTED.value) {
-            return
-        }
-
-        runBlocking {
-            val isFbsimctlHealthyTask = async { fbsimctlProc.isHealthy() }
-            val isWdaHealthyTask = async { if (useWda) wdaProc.isHealthy() else true }
-
-            val isFbsimctlHealthy: Boolean = isFbsimctlHealthyTask.await()
-            val isWdaHealthy: Boolean = isWdaHealthyTask.await()
-
-            if (isFbsimctlHealthy) {
-                simulatorStatus.fbsimctlStatusRetries = 0
-            } else {
-                simulatorStatus.fbsimctlStatusRetries += 1
-
-                if (simulatorStatus.fbsimctlStatusRetries > 3) {
-                    executeCritical {
-                        deviceState = DeviceState.FAILED
-                    }
-
-                    val message = "${this@Simulator} Fbsimctl: simulator is not healthy, probably crashed."
-                    logger.error(logMarker, message)
-                    lastException = RuntimeException(message)
-                    return@runBlocking
-                }
-            }
-
-            if (isWdaHealthy) {
-                simulatorStatus.wdaStatusRetries = 0
-            } else {
-                simulatorStatus.wdaStatusRetries += 1
-
-                if (simulatorStatus.wdaStatusRetries > 3) {
-                    executeCritical {
-                        deviceState = DeviceState.FAILED
-                    }
-
-                    val message = "${this@Simulator} WebDriverAgent is not healthy, probably crashed."
-                    logger.error(logMarker, message)
-                    lastException = RuntimeException(message)
-                    return@runBlocking
-                }
-            }
-
-            simulatorStatus.isReady = isWdaHealthy && isFbsimctlHealthy
-            simulatorStatus.fbsimctlStatus = isFbsimctlHealthy
-            simulatorStatus.wdaStatus = isWdaHealthy
-        }
-    }
     //endregion
 
     override fun endpointFor(port: Int): URL {

From 969a096c235837b978b9a8b7844c27dace4e0cbe Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Fri, 22 Mar 2019 10:28:48 +0000
Subject: [PATCH 085/131] Have longer retry intervals to check if simulator
 shut down not to DDoS fbsimctl.

---
 .../deviceserver/ios/simulator/Simulator.kt   | 20 +++++++++++--------
 1 file changed, 12 insertions(+), 8 deletions(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
index f894579c..a9b9c158 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
@@ -196,14 +196,18 @@ class Simulator (
 
     private fun shutdown() {
         logger.info(logMarker, "Shutting down ${this@Simulator}")
-        ignoringErrors({ fbsimctlProc.kill() })
-
-        if (remote.fbsimctl.listDevice(udid)?.state != FBSimctlDeviceState.SHUTDOWN.value) {
-            remote.fbsimctl.shutdown(udid)
-            pollFor(Duration.ofSeconds(50), "${this@Simulator} to shutdown", logger = logger, marker = logMarker) {
-                val fbSimctlDevice = remote.fbsimctl.listDevice(udid)
-                FBSimctlDeviceState.SHUTDOWN.value == fbSimctlDevice?.state
-            }
+        remote.fbsimctl.shutdown(udid)
+        ignoringErrors { fbsimctlProc.kill() }
+
+        pollFor(
+            timeOut = Duration.ofSeconds(60),
+            reasonName = "${this@Simulator} to shutdown",
+            retryInterval = Duration.ofSeconds(10),
+            logger = logger,
+            marker = logMarker
+        ) {
+            val fbSimctlDevice = remote.fbsimctl.listDevice(udid)
+            FBSimctlDeviceState.SHUTDOWN.value == fbSimctlDevice?.state
         }
 
         logger.info(logMarker, "Successfully shut down ${this@Simulator}")

From ab41c1cc87ea02785629dc7dbf1da78cf76ddf35 Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Tue, 16 Apr 2019 06:36:28 +0100
Subject: [PATCH 086/131] Add simulator health check

---
 .../deviceserver/ios/proc/WebDriverAgent.kt   |  2 +-
 .../deviceserver/ios/simulator/Simulator.kt   | 50 +++++++++++++++++++
 2 files changed, 51 insertions(+), 1 deletion(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt
index ea9466ec..1f0ba91c 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt
@@ -36,7 +36,7 @@ open class WebDriverAgent(
             "--",
             "listen"
     )
-    private val uri: URI = uriWithPath(wdaEndpoint, "wda/healthcheck")
+    private val uri: URI = uriWithPath(wdaEndpoint, "status")
 
     override fun toString(): String = "<$udid at ${remote.hostName}:${wdaEndpoint.port}>"
 
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
index a9b9c158..293fbd0c 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
@@ -82,6 +82,7 @@ class Simulator (
     private val logMarker: Marker = MapEntriesAppendingMarker(commonLogMarkerDetails)
     private val fileSystem = FileSystem(remote, udid)
     private val simulatorProcess = SimulatorProcess(remote, udid)
+    @Volatile private var healthChecker: Job? = null
     //endregion
 
     //region properties from ruby with backing mutable field
@@ -93,6 +94,7 @@ class Simulator (
 
     //region prepareAsync
     override fun prepareAsync() {
+        stopPeriodicHealthCheck()
         executeCritical {
             deviceState = DeviceState.CREATING
         }
@@ -140,9 +142,55 @@ class Simulator (
 
             logger.info(logMarker, "Finished preparing $this")
             deviceState = DeviceState.CREATED
+            startPeriodicHealthCheck()
         }
     }
 
+    private fun startPeriodicHealthCheck() {
+        stopPeriodicHealthCheck()
+
+        var fbsimctlFailCount = 0
+        var wdaFailCount = 0
+        val maxFailCount = 3
+        val healthCheckInterval = Duration.ofSeconds(10).toMillis()
+
+        healthChecker = launch {
+            while (isActive) {
+                if (fbsimctlProc.isHealthy()) {
+                    fbsimctlFailCount = 0
+                } else {
+                    fbsimctlFailCount += 1
+
+                    if (fbsimctlFailCount >= maxFailCount) {
+                        deviceState = DeviceState.FAILED
+                        val message = "Fbsimctl health check failed $fbsimctlFailCount times. Setting device state to $deviceState"
+                        logger.error(logMarker, message)
+                        throw RuntimeException("${this@Simulator} $message. Stopping health check")
+                    }
+                }
+
+                if (wdaProc.isHealthy()) {
+                    wdaFailCount = 0
+                } else {
+                    wdaFailCount += 1
+
+                    if (wdaFailCount >= maxFailCount) {
+                        deviceState = DeviceState.FAILED
+                        val message = "WebDriverAgent health check failed $wdaFailCount times. Setting device state to $deviceState"
+                        logger.error(logMarker, message)
+                        throw RuntimeException("${this@Simulator} $message. Stopping health check")
+                    }
+                }
+
+                delay(healthCheckInterval)
+            }
+        }
+    }
+
+    private fun stopPeriodicHealthCheck() {
+        healthChecker?.cancel()
+    }
+
     private fun startWdaWithRetry(pollTimeout: Duration = Duration.ofSeconds(30), retryInterval: Duration = Duration.ofSeconds(3)) {
         val maxRetries = 3
 
@@ -318,6 +366,7 @@ class Simulator (
 
     //region reset async
     override fun resetAsync() {
+        stopPeriodicHealthCheck()
         executeCritical {
             deviceState = DeviceState.RESETTING
         }
@@ -464,6 +513,7 @@ class Simulator (
 
     //region release
     override fun release(reason: String) {
+        stopPeriodicHealthCheck()
         logger.info(logMarker, "Releasing device $this because $reason")
 
         // FIXME: add background thread to clear up junk we failed to delete

From 99e701bbecb6ec36c472d29b5e0e8bed3f879ca4 Mon Sep 17 00:00:00 2001
From: Nikolai Abalov 
Date: Wed, 17 Apr 2019 09:59:14 +0100
Subject: [PATCH 087/131] Add OS Log diagnostic for simulators

---
 .../automation/deviceserver/DeviceServer.kt   |  8 ++-
 .../controllers/DevicesController.kt          |  4 +-
 .../deviceserver/data/DiagnosticQuery.kt      |  8 +++
 .../deviceserver/data/DiagnosticType.kt       |  1 +
 .../deviceserver/host/DevicesNode.kt          |  2 +-
 .../deviceserver/host/ISimulatorsNode.kt      |  2 +-
 .../deviceserver/host/SimulatorsNode.kt       | 12 ++--
 .../host/management/DeviceManager.kt          |  4 +-
 .../deviceserver/ios/simulator/ISimulator.kt  |  2 +
 .../deviceserver/ios/simulator/Simulator.kt   |  2 +
 .../ios/simulator/diagnostic/OsLog.kt         | 58 +++++++++++++++++++
 11 files changed, 92 insertions(+), 11 deletions(-)
 create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DiagnosticQuery.kt
 create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/OsLog.kt

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
index a02e3e8a..e8096444 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
@@ -221,7 +221,13 @@ fun Application.module() {
                     get {
                         val ref = param(call, "ref")
                         val type = param(call, "type")
-                        call.respond(devicesController.getDiagnostic(ref, type))
+                        call.respond(devicesController.getDiagnostic(ref, type, DiagnosticQuery()))
+                    }
+                    post {
+                        val ref = param(call, "ref")
+                        val type = param(call, "type")
+                        val query = jsonContent(call)
+                        call.respond(devicesController.getDiagnostic(ref, type, query))
                     }
                     delete {
                         val ref = param(call, "ref")
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt
index a7b03865..e968fd5c 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt
@@ -101,9 +101,9 @@ class DevicesController(private val deviceManager: DeviceManager) {
         return happy
     }
 
-    fun getDiagnostic(ref: DeviceRef, type: String): Diagnostic {
+    fun getDiagnostic(ref: DeviceRef, type: String, query: DiagnosticQuery): Diagnostic {
         val diagnosticType = DiagnosticType.fromString(type)
-        return deviceManager.getDiagnostic(ref, diagnosticType)
+        return deviceManager.getDiagnostic(ref, diagnosticType, query)
     }
 
     fun resetDiagnostic(ref: DeviceRef, type: String) {
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DiagnosticQuery.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DiagnosticQuery.kt
new file mode 100644
index 00000000..ea64e5d9
--- /dev/null
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DiagnosticQuery.kt
@@ -0,0 +1,8 @@
+package com.badoo.automation.deviceserver.data
+
+import com.fasterxml.jackson.annotation.JsonProperty
+
+data class DiagnosticQuery(
+    @JsonProperty("process")
+    val process: String? = null
+)
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DiagnosticType.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DiagnosticType.kt
index 0eaa1b6c..2539c7bf 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DiagnosticType.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DiagnosticType.kt
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.annotation.JsonValue
 import java.lang.IllegalArgumentException
 
 enum class DiagnosticType(@JsonValue val value: String) {
+    OsLog("os_log"),
     SystemLog("system_log");
 
     companion object {
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
index ca380cfd..0d49515b 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
@@ -43,7 +43,7 @@ class DevicesNode(
 
     private val deviceRegistrationInterval = Duration.ofMinutes(1)
 
-    override fun getDiagnostic(deviceRef: DeviceRef, type: DiagnosticType): Diagnostic {
+    override fun getDiagnostic(deviceRef: DeviceRef, type: DiagnosticType, query: DiagnosticQuery): Diagnostic {
         throw(NotImplementedError("Diagnostic is not supported by physical devices"))
     }
 
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
index 56a1296a..0f80671a 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
@@ -26,7 +26,7 @@ interface ISimulatorsNode {
     fun listFiles(deviceRef: DeviceRef, dataPath: DataPath): List
     fun pullFile(deviceRef: DeviceRef, dataPath: DataPath): ByteArray
 
-    fun getDiagnostic(deviceRef: DeviceRef, type: DiagnosticType): Diagnostic
+    fun getDiagnostic(deviceRef: DeviceRef, type: DiagnosticType, query: DiagnosticQuery): Diagnostic
     fun resetDiagnostic(deviceRef: DeviceRef, type: DiagnosticType)
 
     val remoteAddress: String
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
index 39245b96..c32af8ab 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
@@ -103,19 +103,23 @@ class SimulatorsNode(
         }
     }
 
-    override fun getDiagnostic(deviceRef: DeviceRef, type: DiagnosticType): Diagnostic {
-        when (type) {
-            DiagnosticType.SystemLog -> return Diagnostic(
+    override fun getDiagnostic(deviceRef: DeviceRef, type: DiagnosticType, query: DiagnosticQuery): Diagnostic {
+        return when (type) {
+            DiagnosticType.SystemLog -> Diagnostic(
                 type = type,
                 content = getDeviceFor(deviceRef).systemLog.content()
             )
-            else -> throw  RuntimeException("Diagnostic $type is not supported")
+            DiagnosticType.OsLog -> Diagnostic(
+                type = type,
+                content = getDeviceFor(deviceRef).osLog.content(query.process)
+            )
         }
     }
 
     override fun resetDiagnostic(deviceRef: DeviceRef, type: DiagnosticType) {
         when (type) {
             DiagnosticType.SystemLog -> getDeviceFor(deviceRef).systemLog.truncate()
+            DiagnosticType.OsLog -> getDeviceFor(deviceRef).osLog.truncate()
         }
     }
 
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
index af9d62e9..ea6ff831 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
@@ -173,8 +173,8 @@ class DeviceManager(
         nodeRegistry.activeDevices.getNodeFor(ref).setEnvironmentVariables(ref, envs)
     }
 
-    fun getDiagnostic(ref: DeviceRef, type: DiagnosticType): Diagnostic {
-        return nodeRegistry.activeDevices.getNodeFor(ref).getDiagnostic(ref, type)
+    fun getDiagnostic(ref: DeviceRef, type: DiagnosticType, query: DiagnosticQuery): Diagnostic {
+        return nodeRegistry.activeDevices.getNodeFor(ref).getDiagnostic(ref, type, query)
     }
 
     fun resetDiagnostic(ref: DeviceRef, type: DiagnosticType) {
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt
index 2c355589..4954cdac 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt
@@ -2,6 +2,7 @@ package com.badoo.automation.deviceserver.ios.simulator
 
 import com.badoo.automation.deviceserver.data.*
 import com.badoo.automation.deviceserver.ios.simulator.data.DataContainer
+import com.badoo.automation.deviceserver.ios.simulator.diagnostic.OsLog
 import com.badoo.automation.deviceserver.ios.simulator.diagnostic.SystemLog
 import com.badoo.automation.deviceserver.ios.simulator.video.SimulatorVideoRecorder
 import java.net.URI
@@ -21,6 +22,7 @@ interface ISimulator {
     val videoRecorder: SimulatorVideoRecorder
     val fbsimctlSubject: String
     val systemLog: SystemLog
+    val osLog: OsLog
 
     fun prepareAsync()
     fun resetAsync()
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
index 293fbd0c..234893e1 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
@@ -12,6 +12,7 @@ import com.badoo.automation.deviceserver.ios.simulator.backup.ISimulatorBackup
 import com.badoo.automation.deviceserver.ios.simulator.backup.SimulatorBackup
 import com.badoo.automation.deviceserver.ios.simulator.data.DataContainer
 import com.badoo.automation.deviceserver.ios.simulator.data.FileSystem
+import com.badoo.automation.deviceserver.ios.simulator.diagnostic.OsLog
 import com.badoo.automation.deviceserver.ios.simulator.diagnostic.SystemLog
 import com.badoo.automation.deviceserver.ios.simulator.video.SimulatorVideoRecorder
 import com.badoo.automation.deviceserver.util.executeWithTimeout
@@ -63,6 +64,7 @@ class Simulator (
     override val videoRecorder: SimulatorVideoRecorder = SimulatorVideoRecorder(deviceInfo, remote, location = recordingLocation)
 
     override val systemLog = SystemLog(remote, udid)
+    override val osLog = OsLog(remote, udid)
 
     //region instance state variables
     private val deviceLock = ReentrantLock()
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/OsLog.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/OsLog.kt
new file mode 100644
index 00000000..410018e6
--- /dev/null
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/OsLog.kt
@@ -0,0 +1,58 @@
+package com.badoo.automation.deviceserver.ios.simulator.diagnostic
+
+import com.badoo.automation.deviceserver.LogMarkers
+import com.badoo.automation.deviceserver.command.ShellUtils
+import com.badoo.automation.deviceserver.data.UDID
+import com.badoo.automation.deviceserver.host.IRemote
+import io.ktor.util.chomp
+import net.logstash.logback.marker.MapEntriesAppendingMarker
+import org.slf4j.LoggerFactory
+import org.slf4j.Marker
+import java.io.File
+
+class OsLog(
+    private val remote: IRemote,
+    private val udid: UDID
+) {
+    private val logger = LoggerFactory.getLogger(javaClass.simpleName)
+    private val logMarker: Marker = MapEntriesAppendingMarker(
+        mapOf(
+            LogMarkers.UDID to udid,
+            LogMarkers.HOSTNAME to remote.hostName
+        )
+    )
+
+    private var timestamp: String? = null
+
+    fun truncate(): Boolean {
+        val date = remote.execIgnoringErrors(listOf("date", "+%s"))
+
+        if (date.isSuccess) {
+            timestamp = date.stdOut.lines().first()
+        }
+
+        return date.isSuccess
+    }
+
+    fun content(process: String?): String {
+        val cmd = mutableListOf("xcrun", "simctl", "spawn", udid, "log", "show", "--style", "syslog")
+
+        if (timestamp != null) {
+            cmd.addAll(listOf("--start", "@$timestamp"))
+        }
+
+        if (process != null) {
+            cmd.addAll(listOf("--predicate", "process==\"$process\""))
+        }
+
+        val result = remote.execIgnoringErrors(cmd)
+
+        if (!result.isSuccess) {
+            val message = "Could not read OS Log: $result"
+            logger.error(logMarker, message)
+            throw RuntimeException(message)
+        }
+
+        return result.stdOut
+    }
+}

From 39c1b11bd67dcef3bcd71a95cf2c51da4dbe0b4e Mon Sep 17 00:00:00 2001
From: Nikolai Abalov 
Date: Wed, 17 Apr 2019 17:03:12 +0100
Subject: [PATCH 088/131] Fix OS Log predicate to work over ssh

---
 .../kotlin/com/badoo/automation/deviceserver/host/IRemote.kt    | 2 ++
 .../kotlin/com/badoo/automation/deviceserver/host/Remote.kt     | 2 ++
 .../automation/deviceserver/ios/simulator/diagnostic/OsLog.kt   | 2 +-
 3 files changed, 5 insertions(+), 1 deletion(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt
index 0bfee96d..caf26f4e 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt
@@ -30,6 +30,8 @@ interface IRemote {
 
     fun shell(command: String, returnOnFailure: Boolean = true) : CommandResult
 
+    fun escape(value: String) : String
+
     /**
      * Returns [CommandResult] file contents would be in [CommandResult.stdOutBytes]
      * //FIXME: should be a better way of streaming a file over HTTP. without caching bytes in server's memory. Investigate ByteReadChannel
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt
index 8a453a45..b2674dd5 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt
@@ -57,6 +57,8 @@ class Remote(
         return remoteExecutor.exec(command, env, returnFailure = returnFailure, timeOut = Duration.ofSeconds(timeOutSeconds))
     }
 
+    override fun escape(value: String) = remoteExecutor.escape(value)
+
     override fun shell(command: String, returnOnFailure: Boolean): CommandResult {
         val cmd = when {
             isLocalhost() -> listOf("bash", "-c", command)
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/OsLog.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/OsLog.kt
index 410018e6..c6a6f035 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/OsLog.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/OsLog.kt
@@ -42,7 +42,7 @@ class OsLog(
         }
 
         if (process != null) {
-            cmd.addAll(listOf("--predicate", "process==\"$process\""))
+            cmd.addAll(listOf("--predicate", remote.escape("process==\"$process\"")))
         }
 
         val result = remote.execIgnoringErrors(cmd)

From cf242f04c7418f45ad24a7f3f2a7be2f95b5f3d5 Mon Sep 17 00:00:00 2001
From: Nikolai Abalov 
Date: Thu, 18 Apr 2019 10:05:51 +0100
Subject: [PATCH 089/131] Log command result on failure to set permissions

---
 .../deviceserver/ios/simulator/SimulatorPermissions.kt | 10 ++++++----
 1 file changed, 6 insertions(+), 4 deletions(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt
index 1d6f2f68..15fbc059 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt
@@ -43,8 +43,9 @@ class SimulatorPermissions(
 
         val delete = "$sqlCmd \"DELETE FROM access WHERE service = '$key' AND client = '$bundleId' AND client_type = 0;\""
 
-        if (!remote.shell(delete).isSuccess) {
-            throw(SimulatorError("Failed to unset type $type for $this "))
+        val deleteResult = remote.shell(delete)
+        if (!deleteResult.isSuccess) {
+            throw(SimulatorError("Could not unset $type permission: $deleteResult"))
         }
 
         if (allowed == PermissionAllowed.Unset) {
@@ -60,8 +61,9 @@ class SimulatorPermissions(
         val replace =
             "$sqlCmd \"REPLACE INTO access (service, client, client_type, allowed, prompt_count) VALUES ('$key','$bundleId',0,$value,1);\""
 
-        if (!remote.shell(replace).isSuccess) {
-            throw(SimulatorError("Failed to update type $type for $this"))
+        val replaceResult = remote.shell(replace)
+        if (!replaceResult.isSuccess) {
+            throw(SimulatorError("Could not update $type permission: $replaceResult"))
         }
     }
 

From 67889b91ad9a5cfa8a7f53ca42aba0d780479b8b Mon Sep 17 00:00:00 2001
From: Nikolai Abalov 
Date: Thu, 18 Apr 2019 10:09:49 +0100
Subject: [PATCH 090/131] Improve error message for unknown diagnostic type

---
 .../com/badoo/automation/deviceserver/data/DiagnosticType.kt   | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DiagnosticType.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DiagnosticType.kt
index 2539c7bf..d754a2b6 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DiagnosticType.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DiagnosticType.kt
@@ -12,7 +12,8 @@ enum class DiagnosticType(@JsonValue val value: String) {
             val rv = DiagnosticType.values().find { it.value == type }
 
             if (rv == null) {
-                val msg = "Diagnostic type $type is not one of ${DiagnosticType.values().joinToString(", ")}"
+                val availableTypes = DiagnosticType.values().map { it.value }.sorted()
+                val msg = "Diagnostic type $type is not one of ${availableTypes.joinToString(", ")}"
                 throw IllegalArgumentException(msg)
             }
 

From 20da4acc2c7caf7049305ac9c690025884484d02 Mon Sep 17 00:00:00 2001
From: Nikolai Abalov 
Date: Tue, 23 Apr 2019 14:33:52 +0100
Subject: [PATCH 091/131] Refactor setting of service permissions

Reduce the number of SQLite process executions by batching all service
permission changes from the permission set into a single SQLite call.

This significantly reduces response time for this operation, especially
when a device server runs using ssh, making it basically constant.
---
 .../deviceserver/ios/simulator/Simulator.kt   | 18 ++++---
 .../ios/simulator/SimulatorPermissions.kt     | 54 ++++++++++++-------
 2 files changed, 45 insertions(+), 27 deletions(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
index 234893e1..27d0fed6 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
@@ -497,18 +497,22 @@ class Simulator (
     override fun approveAccess(bundleId: String) {
         val permissions = SimulatorPermissions(remote, deviceSetPath, this)
 
-        permissions.setServicePermission(bundleId, PermissionType.Camera, PermissionAllowed.Yes)
-        permissions.setServicePermission(bundleId, PermissionType.Microphone, PermissionAllowed.Yes)
-        permissions.setServicePermission(bundleId, PermissionType.Photos, PermissionAllowed.Yes)
-        permissions.setServicePermission(bundleId, PermissionType.Contacts, PermissionAllowed.Yes)
+        val set = PermissionSet()
+
+        set.putAll(mapOf(
+            PermissionType.Camera to PermissionAllowed.Yes,
+            PermissionType.Microphone to PermissionAllowed.Yes,
+            PermissionType.Photos to PermissionAllowed.Yes,
+            PermissionType.Contacts to PermissionAllowed.Yes
+        ))
+
+        permissions.setPermissions(bundleId, set)
     }
 
     override fun setPermissions(bundleId: String, permissions: PermissionSet) {
         val manager = SimulatorPermissions(remote, deviceSetPath, this)
 
-        permissions.forEach { type, allowed ->
-            manager.setPermission(bundleId, type, allowed)
-        }
+        manager.setPermissions(bundleId, permissions)
     }
 
     //endregion
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt
index 15fbc059..da66a046 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt
@@ -1,6 +1,7 @@
 package com.badoo.automation.deviceserver.ios.simulator
 
 import com.badoo.automation.deviceserver.data.PermissionAllowed
+import com.badoo.automation.deviceserver.data.PermissionSet
 import com.badoo.automation.deviceserver.data.PermissionType
 import com.badoo.automation.deviceserver.host.IRemote
 import java.io.File
@@ -26,30 +27,47 @@ class SimulatorPermissions(
         PermissionType.Speech to "kTCCServiceSpeechRecognition"
     )
 
-    fun setPermission(bundleId: String, type: PermissionType, allowed: PermissionAllowed) {
-        when (type) {
-            PermissionType.Location -> setLocationPermission(bundleId, allowed)
-            PermissionType.Notifications -> setNotificationsPermission(bundleId, allowed)
-            else -> setServicePermission(bundleId, type, allowed)
+    fun setPermissions(bundleId: String, permissions: PermissionSet) {
+        val servicePermissions = PermissionSet()
+
+        permissions.forEach { type, allowed ->
+            when (type) {
+                PermissionType.Location -> setLocationPermission(bundleId, allowed)
+                PermissionType.Notifications -> setNotificationsPermission(bundleId, allowed)
+                else -> servicePermissions[type] = allowed
+            }
         }
+
+        setServicePermissions(bundleId, servicePermissions)
     }
 
-    fun setServicePermission(bundleId: String, type: PermissionType, allowed: PermissionAllowed) {
-        val key = serviceKeys[type]
-                ?: throw(IllegalArgumentException("Permission $type is not a service type"))
+    private fun setServicePermissions(bundleId: String, servicePermissions: PermissionSet) {
+        val sql = StringBuilder()
+
+        servicePermissions.forEach { type, allowed ->
+            sql.append(sqlForPermission(bundleId, type, allowed))
+        }
 
         val path = File(deviceSetPath, simulator.udid)
-        val sqlCmd = "sqlite3 ${path.absolutePath}/data/Library/TCC/TCC.db"
+        val sqlCmd = "sqlite3 ${path.absolutePath}/data/Library/TCC/TCC.db \"$sql\""
 
-        val delete = "$sqlCmd \"DELETE FROM access WHERE service = '$key' AND client = '$bundleId' AND client_type = 0;\""
+        val result = remote.shell(sqlCmd)
 
-        val deleteResult = remote.shell(delete)
-        if (!deleteResult.isSuccess) {
-            throw(SimulatorError("Could not unset $type permission: $deleteResult"))
+        if (!result.isSuccess) {
+            throw(SimulatorError("Could not set permissions: $result"))
         }
+    }
+
+    private fun sqlForPermission(bundleId: String, type: PermissionType, allowed: PermissionAllowed): String? {
+        val sql = StringBuilder()
+
+        val key = serviceKeys[type]
+                ?: throw(IllegalArgumentException("Permission $type is not a service type"))
+
+        sql.append("DELETE FROM access WHERE service = '$key' AND client = '$bundleId' AND client_type = 0;")
 
         if (allowed == PermissionAllowed.Unset) {
-            return
+            return sql.toString()
         }
 
         val value = when (allowed) {
@@ -58,13 +76,9 @@ class SimulatorPermissions(
             else -> throw IllegalArgumentException("Unsupported value $allowed for type $type")
         }
 
-        val replace =
-            "$sqlCmd \"REPLACE INTO access (service, client, client_type, allowed, prompt_count) VALUES ('$key','$bundleId',0,$value,1);\""
+        sql.append("REPLACE INTO access (service, client, client_type, allowed, prompt_count) VALUES ('$key','$bundleId',0,$value,1);")
 
-        val replaceResult = remote.shell(replace)
-        if (!replaceResult.isSuccess) {
-            throw(SimulatorError("Could not update $type permission: $replaceResult"))
-        }
+        return sql.toString()
     }
 
     private val appleSimUtils = "/usr/local/bin/applesimutils"

From 9f468be6c51e6f635011ecd32c63ef624524f13c Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Thu, 25 Apr 2019 09:15:12 +0100
Subject: [PATCH 092/131] Print only stderr for OS log error message

---
 .../automation/deviceserver/ios/simulator/diagnostic/OsLog.kt   | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/OsLog.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/OsLog.kt
index c6a6f035..9a16eeed 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/OsLog.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/OsLog.kt
@@ -48,7 +48,7 @@ class OsLog(
         val result = remote.execIgnoringErrors(cmd)
 
         if (!result.isSuccess) {
-            val message = "Could not read OS Log: $result"
+            val message = "Could not read OS Log. Result stdErr: ${result.stdErr}"
             logger.error(logMarker, message)
             throw RuntimeException(message)
         }

From 3526bbcb0510339895809ca1288251dae357f6e8 Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Thu, 25 Apr 2019 09:22:46 +0100
Subject: [PATCH 093/131] Exclude irrelevant fields from CommandResult toString

---
 .../deviceserver/command/CommandResult.kt     | 20 +++++++++++++++++-
 .../deviceserver/command/CommandResultTest.kt | 21 +++++++++++++++++++
 2 files changed, 40 insertions(+), 1 deletion(-)
 create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/CommandResultTest.kt

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/CommandResult.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/CommandResult.kt
index 7b6a6b0a..62c55df9 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/CommandResult.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/CommandResult.kt
@@ -1,5 +1,9 @@
 package com.badoo.automation.deviceserver.command
 
+import java.lang.StringBuilder
+
+private const val MAX_CHARS = 1024
+
 data class CommandResult(
     // FIXME: Separate binary and string results for "capture" and "process open" executors
     val stdOut: String,
@@ -8,4 +12,18 @@ data class CommandResult(
     val exitCode: Int,
     val isSuccess: Boolean = exitCode == 0,
     val cmd: List = listOf()
-)
+) {
+    override fun toString(): String {
+        val sb = StringBuilder(javaClass.simpleName)
+        sb.append("(")
+        sb.append("cmd=$cmd")
+        sb.append(", ")
+        sb.append("exitCode=$exitCode")
+        sb.append(", ")
+        sb.append("stdOut=${stdOut.take(MAX_CHARS)}")
+        sb.append(", ")
+        sb.append("stdErr=${stdErr.take(MAX_CHARS)}")
+        sb.append(")")
+        return sb.toString()
+    }
+}
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/CommandResultTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/CommandResultTest.kt
new file mode 100644
index 00000000..07b0bc83
--- /dev/null
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/CommandResultTest.kt
@@ -0,0 +1,21 @@
+package com.badoo.automation.deviceserver.command
+
+import org.junit.Assert
+import org.junit.Test
+
+class CommandResultTest {
+    @Test
+    fun commandResultContainsRelevantFields() {
+        val expectedResult = "CommandResult(cmd=[rm, -rf, /], exitCode=0, stdOut=out, stdErr=err)"
+
+        val actualResult = CommandResult(
+                stdOut = "out",
+                stdErr = "err",
+                stdOutBytes = "out".toByteArray(),
+                exitCode = 0,
+                cmd = listOf("rm", "-rf", "/")
+        ).toString()
+
+        Assert.assertEquals("Wrong result string", expectedResult, actualResult)
+    }
+}
\ No newline at end of file

From 37602fa3a9de3b6f7baf5eeb364cba73b50dda37 Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Thu, 25 Apr 2019 10:42:24 +0100
Subject: [PATCH 094/131] Unify error messages when logging command result

---
 .../automation/deviceserver/command/RemoteShellCommand.kt   | 2 +-
 .../kotlin/com/badoo/automation/deviceserver/host/Remote.kt | 2 +-
 .../deviceserver/host/management/ISimulatorHostChecker.kt   | 2 +-
 .../badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt  | 3 +--
 .../badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt  | 2 +-
 .../deviceserver/ios/simulator/SimulatorProcess.kt          | 6 ++----
 .../ios/simulator/video/SimulatorVideoRecorder.kt           | 2 +-
 7 files changed, 8 insertions(+), 11 deletions(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt
index 05c85192..c78e1dd2 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt
@@ -72,7 +72,7 @@ class RemoteShellCommand(
 
         if (result.exitCode == SSH_ERROR) {
             // FIXME: Check stdout and stderr, if they are empty – ssh timeout, otherwise, it is likely to be command error
-            val message = "Probably SSH could not connect to node $remoteHost while running command [$command]. See stdout: <${result.stdOut}> stderr: <${result.stdErr}>"
+            val message = "Probably SSH could not connect to node $remoteHost. Result: $result"
             logger.error(logMarker, message)
             throw SshConnectionException(message)
         }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt
index b2674dd5..ea274300 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt
@@ -115,7 +115,7 @@ class Remote(
         }
 
         ensure(result.isSuccess) {
-            logger.error(logMarker, "Executing rsync command failed. Result: [$result]")
+            logger.error(logMarker, "Executing rsync command failed. Result: $result")
             RuntimeException("Remote $cmd failed with $result")
         }
     }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt
index b8d4ebbb..4770d387 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt
@@ -107,7 +107,7 @@ class SimulatorHostChecker(
                     try {
                         val r = remote.shell("find $it -maxdepth 0 -mmin +60 -exec rm -rf {} \\;", returnOnFailure = true) // find returns non zero if nothing found
                         if (!r.isSuccess || r.stdErr.isNotEmpty() || r.stdOut.isNotEmpty()) {
-                            logger.debug(logMarker, "[disc cleaner] $this returned non-empty. ERR: [${r.stdErr}], OUT: [${r.stdOut}]")
+                            logger.debug(logMarker, "[disc cleaner] $this returned non-empty. Result stdErr: ${r.stdErr}")
                         }
                     } catch (e: RuntimeException) {
                         logger.debug(logMarker, "[disc cleaner] $this got exception while cleaning caches: ${e.message}", e)
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt
index 777e01b4..6a95f5a1 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt
@@ -153,8 +153,7 @@ class FBSimctl(
         if (raiseOnError) {
             ensure(errors.isEmpty()) { FBSimctlError("fbsimctl failed: $errors", null) }
             if (result.exitCode != 0) {
-                throw FBSimctlError("Error while running command: ${fbsimctlCommand.joinToString(" ")} " +
-                        "Exit code: [${result.exitCode}], stdout: [${result.stdOut}], stderr: [${result.stdErr}]", null)
+                throw FBSimctlError("Error while running command. Result: $result", null)
             }
         }
 
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt
index ee93652d..95e8ff87 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt
@@ -48,7 +48,7 @@ open class FbsimctlProc(
             return if (result.isSuccess) {
                 true
             } else {
-                logger.debug(logMarker, "Failed fbsimctl health check. Response: $result")
+                logger.debug(logMarker, "Failed fbsimctl health check. Result: $result")
                 false
             }
         } catch (e: RuntimeException) {
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcess.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcess.kt
index ecac6671..5540d425 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcess.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcess.kt
@@ -13,8 +13,7 @@ class SimulatorProcess(
             val result = remote.execIgnoringErrors(command)
 
             check(result.isSuccess) {
-                "No launchd_sim process is found for simulator with udid: $udid. " +
-                        "Found: stdout: [${result.stdOut}], stderr: [${result.stdErr}]."
+                "No launchd_sim process is found for simulator with udid: $udid. Result: $result"
             }
 
             val processList = result
@@ -23,8 +22,7 @@ class SimulatorProcess(
                 .filter { it.contains(udid) }
 
             check(processList.isNotEmpty()) {
-                "No launchd_sim process is found for simulator with udid: $udid. " +
-                        "Found: stdout: [${result.stdOut}], stderr: [${result.stdErr}]."
+                "No launchd_sim process is found for simulator with udid: $udid. Result: $result"
             }
 
             return processList
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt
index e4ddc6ec..cf758e11 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt
@@ -118,7 +118,7 @@ class SimulatorVideoRecorder(
         // once we have proper deployment solution for our macOS machines
         val result = remote.captureFile(videoFile)
         if (!result.isSuccess) {
-            val message = "Could not read video file $result"
+            val message = "Could not read video file. Result stdErr: ${result.stdErr}"
             logger.error(message)
             throw SimulatorVideoRecordingException(message)
         }

From 03989238d2aa1084ff3b431814f31d1fa7a839bb Mon Sep 17 00:00:00 2001
From: Nikolai Abalov 
Date: Tue, 30 Apr 2019 14:20:03 +0100
Subject: [PATCH 095/131] Add method to reset Media folder

---
 .../automation/deviceserver/DeviceServer.kt   |  6 +++++
 .../controllers/DevicesController.kt          |  6 +++++
 .../deviceserver/host/DevicesNode.kt          |  4 +++
 .../deviceserver/host/ISimulatorsNode.kt      |  2 ++
 .../deviceserver/host/SimulatorsNode.kt       |  4 +++
 .../host/management/DeviceManager.kt          |  4 +++
 .../deviceserver/ios/simulator/ISimulator.kt  |  2 ++
 .../deviceserver/ios/simulator/Simulator.kt   |  3 +++
 .../deviceserver/ios/simulator/data/Media.kt  | 27 +++++++++++++++++++
 9 files changed, 58 insertions(+)
 create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/Media.kt

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
index e8096444..c0c19caf 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
@@ -217,6 +217,12 @@ fun Application.module() {
                         call.respond(devicesController.uninstallApplication(ref, bundleId))
                     }
                 }
+                route("media") {
+                    delete {
+                        val ref = param(call, "ref")
+                        call.respond(devicesController.resetMedia(ref))
+                    }
+                }
                 route("diagnose/{type}") {
                     get {
                         val ref = param(call, "ref")
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt
index e968fd5c..9a11e026 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt
@@ -101,6 +101,12 @@ class DevicesController(private val deviceManager: DeviceManager) {
         return happy
     }
 
+    fun resetMedia(ref: DeviceRef): EmptyMap {
+        deviceManager.resetMedia(ref)
+
+        return happy
+    }
+
     fun getDiagnostic(ref: DeviceRef, type: String, query: DiagnosticQuery): Diagnostic {
         val diagnosticType = DiagnosticType.fromString(type)
         return deviceManager.getDiagnostic(ref, diagnosticType, query)
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
index 0d49515b..deff7155 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
@@ -43,6 +43,10 @@ class DevicesNode(
 
     private val deviceRegistrationInterval = Duration.ofMinutes(1)
 
+    override fun resetMedia(deviceRef: DeviceRef) {
+        throw(NotImplementedError("Resetting media is not supported by physical devices"))
+    }
+
     override fun getDiagnostic(deviceRef: DeviceRef, type: DiagnosticType, query: DiagnosticQuery): Diagnostic {
         throw(NotImplementedError("Diagnostic is not supported by physical devices"))
     }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
index 0f80671a..d75f06d8 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
@@ -26,6 +26,8 @@ interface ISimulatorsNode {
     fun listFiles(deviceRef: DeviceRef, dataPath: DataPath): List
     fun pullFile(deviceRef: DeviceRef, dataPath: DataPath): ByteArray
 
+    fun resetMedia(deviceRef: DeviceRef)
+
     fun getDiagnostic(deviceRef: DeviceRef, type: DiagnosticType, query: DiagnosticQuery): Diagnostic
     fun resetDiagnostic(deviceRef: DeviceRef, type: DiagnosticType)
 
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
index c32af8ab..d860e9ac 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
@@ -103,6 +103,10 @@ class SimulatorsNode(
         }
     }
 
+    override fun resetMedia(deviceRef: DeviceRef) {
+        getDeviceFor(deviceRef).media.reset()
+    }
+
     override fun getDiagnostic(deviceRef: DeviceRef, type: DiagnosticType, query: DiagnosticQuery): Diagnostic {
         return when (type) {
             DiagnosticType.SystemLog -> Diagnostic(
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
index ea6ff831..e77f45d2 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
@@ -173,6 +173,10 @@ class DeviceManager(
         nodeRegistry.activeDevices.getNodeFor(ref).setEnvironmentVariables(ref, envs)
     }
 
+    fun resetMedia(ref: DeviceRef) {
+        nodeRegistry.activeDevices.getNodeFor(ref).resetMedia(ref)
+    }
+
     fun getDiagnostic(ref: DeviceRef, type: DiagnosticType, query: DiagnosticQuery): Diagnostic {
         return nodeRegistry.activeDevices.getNodeFor(ref).getDiagnostic(ref, type, query)
     }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt
index 4954cdac..0f72399d 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt
@@ -2,6 +2,7 @@ package com.badoo.automation.deviceserver.ios.simulator
 
 import com.badoo.automation.deviceserver.data.*
 import com.badoo.automation.deviceserver.ios.simulator.data.DataContainer
+import com.badoo.automation.deviceserver.ios.simulator.data.Media
 import com.badoo.automation.deviceserver.ios.simulator.diagnostic.OsLog
 import com.badoo.automation.deviceserver.ios.simulator.diagnostic.SystemLog
 import com.badoo.automation.deviceserver.ios.simulator.video.SimulatorVideoRecorder
@@ -23,6 +24,7 @@ interface ISimulator {
     val fbsimctlSubject: String
     val systemLog: SystemLog
     val osLog: OsLog
+    val media: Media
 
     fun prepareAsync()
     fun resetAsync()
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
index 27d0fed6..348f9fe1 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
@@ -12,6 +12,7 @@ import com.badoo.automation.deviceserver.ios.simulator.backup.ISimulatorBackup
 import com.badoo.automation.deviceserver.ios.simulator.backup.SimulatorBackup
 import com.badoo.automation.deviceserver.ios.simulator.data.DataContainer
 import com.badoo.automation.deviceserver.ios.simulator.data.FileSystem
+import com.badoo.automation.deviceserver.ios.simulator.data.Media
 import com.badoo.automation.deviceserver.ios.simulator.diagnostic.OsLog
 import com.badoo.automation.deviceserver.ios.simulator.diagnostic.SystemLog
 import com.badoo.automation.deviceserver.ios.simulator.video.SimulatorVideoRecorder
@@ -87,6 +88,8 @@ class Simulator (
     @Volatile private var healthChecker: Job? = null
     //endregion
 
+    override val media: Media = Media(remote, udid, deviceSetPath)
+
     //region properties from ruby with backing mutable field
     override val state get() = deviceState
     override val lastError get() = lastException
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/Media.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/Media.kt
new file mode 100644
index 00000000..bfc9135e
--- /dev/null
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/Media.kt
@@ -0,0 +1,27 @@
+package com.badoo.automation.deviceserver.ios.simulator.data
+
+import com.badoo.automation.deviceserver.data.UDID
+import com.badoo.automation.deviceserver.host.IRemote
+import java.lang.RuntimeException
+import java.nio.file.Paths
+
+class Media(
+    private val remote: IRemote,
+    udid: UDID,
+    deviceSetPath: String
+) {
+    private val mediaPath = Paths.get(deviceSetPath, udid, "data", "Media")
+
+    fun reset() {
+        val imagesPath = mediaPath.resolve("DCIM").toString()
+        val photoDataPath = mediaPath.resolve("PhotoData/Photos.sqlite").toString()
+
+        val shellCmd = "rm $imagesPath/**/* $photoDataPath*; touch $photoDataPath"
+
+        val result = remote.shell(shellCmd)
+
+        if (!result.isSuccess) {
+            throw RuntimeException("Could not reset Media: $result")
+        }
+    }
+}

From 48d2f13325ab1f17b0aab2b8c8dfb34d8091d6be Mon Sep 17 00:00:00 2001
From: Nikolai Abalov 
Date: Tue, 30 Apr 2019 15:43:09 +0100
Subject: [PATCH 096/131] Restart com.apple.assetsd on Media reset

Uploading media files using fbsimctl after reset might fail unless
com.apple.assetsd is restarted explicitly
---
 .../deviceserver/ios/simulator/data/Media.kt  | 22 +++++++++++++++----
 1 file changed, 18 insertions(+), 4 deletions(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/Media.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/Media.kt
index bfc9135e..4aaf7d52 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/Media.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/Media.kt
@@ -2,12 +2,11 @@ package com.badoo.automation.deviceserver.ios.simulator.data
 
 import com.badoo.automation.deviceserver.data.UDID
 import com.badoo.automation.deviceserver.host.IRemote
-import java.lang.RuntimeException
 import java.nio.file.Paths
 
 class Media(
     private val remote: IRemote,
-    udid: UDID,
+    private val udid: UDID,
     deviceSetPath: String
 ) {
     private val mediaPath = Paths.get(deviceSetPath, udid, "data", "Media")
@@ -16,12 +15,27 @@ class Media(
         val imagesPath = mediaPath.resolve("DCIM").toString()
         val photoDataPath = mediaPath.resolve("PhotoData/Photos.sqlite").toString()
 
-        val shellCmd = "rm $imagesPath/**/* $photoDataPath*; touch $photoDataPath"
+        val removeCmd = "rm -f $imagesPath/**/* $photoDataPath*; touch $photoDataPath"
 
-        val result = remote.shell(shellCmd)
+        val result = remote.shell(removeCmd)
 
         if (!result.isSuccess) {
             throw RuntimeException("Could not reset Media: $result")
         }
+
+        // restart assetsd to prevent fbsimctl upload failing with Error Domain=NSCocoaErrorDomain Code=-1 \"(null)\"
+        restartAssetsd()
+    }
+
+    private fun restartAssetsd() {
+        val restartCmd = listOf(
+            "xcrun", "simctl", "spawn", udid, "launchctl", "kickstart", "-k", "-p", "system/com.apple.assetsd"
+        )
+
+        val result = remote.execIgnoringErrors(restartCmd)
+
+        if (!result.isSuccess) {
+            throw RuntimeException("Could not restart assetsd service: $result")
+        }
     }
 }

From 85ade3c738308995443ee787386733caa48c26f2 Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Tue, 30 Apr 2019 14:42:49 +0100
Subject: [PATCH 097/131] Install WebDriverAgent to Simulators using xcrun
 simctl

---
 .../deviceserver/ios/proc/SimulatorWebDriverAgent.kt      | 8 +++++++-
 .../automation/deviceserver/ios/simulator/Simulator.kt    | 4 ++--
 2 files changed, 9 insertions(+), 3 deletions(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt
index 18c6f3b9..771986ba 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt
@@ -25,7 +25,13 @@ class SimulatorWebDriverAgent(
     }
 
     private fun installHostApp() {
-        remote.fbsimctl.installApp(udid, File(hostApp))
+        val result = remote.execIgnoringErrors(listOf("xcrun", "simctl", "install", udid, hostApp))
+
+        if (!result.isSuccess) {
+            val errorMessage = "Failed to install WebDriverAgent $hostApp to simulator $udid. Result: $result"
+            logger.error(logMarker, errorMessage)
+            throw RuntimeException(errorMessage)
+        }
 
         pollFor(
             Duration.ofSeconds(20),
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
index 348f9fe1..24e44d20 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
@@ -223,8 +223,8 @@ class Simulator (
 
                 break
             }
-            catch (e: WaitTimeoutError) {
-                logger.warn(logMarker, "Attempt $attempt to start WebDriverAgent for ${this@Simulator} timed out: $e")
+            catch (e: RuntimeException) {
+                logger.warn(logMarker, "Attempt $attempt to start WebDriverAgent for ${this@Simulator} failed: $e")
                 if (attempt == maxRetries) {
                     throw e
                 }

From 2d6225169509b23af842035fdc0bcfbc2b940c1f Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Fri, 3 May 2019 15:33:45 +0100
Subject: [PATCH 098/131] Terminate iOS application on simulator before
 uninstall

---
 .../deviceserver/ios/simulator/Simulator.kt     | 17 ++++++++++++++++-
 1 file changed, 16 insertions(+), 1 deletion(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
index 24e44d20..0f827189 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
@@ -670,7 +670,22 @@ class Simulator (
     //endregion
     override fun uninstallApplication(bundleId: String) {
         logger.debug(logMarker, "Uninstalling application $bundleId from Simulator $this")
-        remote.execIgnoringErrors(listOf("xcrun", "simctl", "uninstall", udid, bundleId))
+
+        terminateApplication(bundleId)
+
+        val uninstallResult = remote.execIgnoringErrors(listOf("xcrun", "simctl", "uninstall", udid, bundleId))
+
+        if (!uninstallResult.isSuccess) {
+            logger.error(logMarker, "Uninstall application $bundleId was unsuccessful. Result $uninstallResult")
+        }
+    }
+
+    private fun terminateApplication(bundleId: String) {
+        val terminateResult = remote.execIgnoringErrors(listOf("xcrun", "simctl", "terminate", udid, bundleId))
+
+        if (!terminateResult.isSuccess) {
+            logger.error(logMarker, "Terminating application $bundleId was unsuccessful. Result $terminateResult")
+        }
     }
 
     override fun setEnvironmentVariables(envs: Map) {

From 2f54516e0ef1a8cb9dc66dd98633e88ead9ea98e Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Wed, 8 May 2019 17:24:20 +0100
Subject: [PATCH 099/131] Fix issue when WebDriverAgent gets deleted on remote
 host

---
 .../deviceserver/ApplicationConfiguration.kt         |  4 ++++
 .../badoo/automation/deviceserver/DeviceServer.kt    |  2 ++
 .../automation/deviceserver/host/HostFactory.kt      | 12 ++++++------
 3 files changed, 12 insertions(+), 6 deletions(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt
index 32e06c3d..f7ed945d 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt
@@ -16,4 +16,8 @@ class ApplicationConfiguration {
             ?: throw RuntimeException("Must set system property: -D$deviceServerConfigPathProperty=./config/.device_config")
 
     val fbsimctlVersion: String = System.getProperty("fbsimctl.version", "HEAD-d30c2a73")
+
+    val remoteWdaSimulatorBundleRoot = System.getProperty("remote.wda.simulator.bundle.path", "/usr/local/opt/web_driver_agent_simulator")
+
+    val remoteWdaDeviceBundleRoot = System.getProperty("remote.wda.device.bundle.path", "/usr/local/opt/web_driver_agent_device")
 }
\ No newline at end of file
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
index c0c19caf..f2fc4c45 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
@@ -100,7 +100,9 @@ fun Application.module() {
     val config = serverConfig()
     val hostFactory = HostFactory(
         wdaSimulatorBundle = File(appConfiguration.wdaSimulatorBundlePath).canonicalFile,
+        remoteWdaSimulatorBundleRoot = File(appConfiguration.remoteWdaSimulatorBundleRoot).canonicalFile,
         wdaDeviceBundle = File(appConfiguration.wdaDeviceBundlePath).canonicalFile,
+        remoteWdaDeviceBundleRoot = File(appConfiguration.remoteWdaDeviceBundleRoot).canonicalFile,
         fbsimctlVersion = appConfiguration.fbsimctlVersion
     )
     val deviceManager = DeviceManager(config, hostFactory)
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt
index aea4493c..3c7d1f2d 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/HostFactory.kt
@@ -9,13 +9,13 @@ import java.io.File
 class HostFactory(
     private val remoteProvider: (hostName: String, userName: String, publicHost: String) -> IRemote = { hostName, userName, publicHostName -> Remote(hostName, userName, publicHostName) },
     private val wdaSimulatorBundle: File,
+    private val remoteWdaSimulatorBundleRoot: File,
     private val wdaDeviceBundle: File,
+    private val remoteWdaDeviceBundleRoot: File,
     private val fbsimctlVersion: String
 ) : IHostFactory {
     companion object {
         val WDA_XCTEST = File("PlugIns/WebDriverAgentRunner.xctest")
-        private val REMOTE_WDA_BUNDLE_ROOT = File("/tmp/web_driver_agent/")
-        private val REMOTE_WDA_DEVICE_BUNDLE_ROOT = File("/tmp/web_driver_agent_devices/")
     }
 
     private val logger = LoggerFactory.getLogger(javaClass.simpleName)
@@ -39,13 +39,13 @@ class HostFactory(
                 hostChecker = SimulatorHostChecker(
                     remote,
                     wdaBundle = wdaSimulatorBundle,
-                    remoteWdaBundleRoot = REMOTE_WDA_BUNDLE_ROOT,
+                    remoteWdaBundleRoot = remoteWdaSimulatorBundleRoot,
                     fbsimctlVersion = fbsimctlVersion,
                     shutdownSimulators = config.shutdownSimulators
                 ),
                 simulatorLimit = config.simulatorLimit,
                 concurrentBoots = config.concurrentBoots,
-                wdaRunnerXctest = getWdaRunnerXctest(remote.isLocalhost(), wdaSimulatorBundle, REMOTE_WDA_BUNDLE_ROOT)
+                wdaRunnerXctest = getWdaRunnerXctest(remote.isLocalhost(), wdaSimulatorBundle, remoteWdaSimulatorBundleRoot)
             )
         } else {
             DevicesNode(
@@ -55,8 +55,8 @@ class HostFactory(
                 knownDevices = config.knownDevices,
                 uninstallApps = config.uninstallApps,
                 wdaBundlePath = wdaDeviceBundle,
-                remoteWdaBundleRoot = REMOTE_WDA_DEVICE_BUNDLE_ROOT,
-                wdaRunnerXctest = getWdaRunnerXctest(remote.isLocalhost(), wdaDeviceBundle, REMOTE_WDA_DEVICE_BUNDLE_ROOT),
+                remoteWdaBundleRoot = remoteWdaDeviceBundleRoot,
+                wdaRunnerXctest = getWdaRunnerXctest(remote.isLocalhost(), wdaDeviceBundle, remoteWdaDeviceBundleRoot),
                 fbsimctlVersion = fbsimctlVersion
             )
         }

From 9a511cc2b0e0bd31b99f7d212e1b6b50cfd92a9b Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Wed, 8 May 2019 20:53:32 +0100
Subject: [PATCH 100/131] Use JVM time source for measuring elapsed time

---
 .../command/RemoteShellCommand.kt             | 10 +++++----
 .../host/management/util/AutoreleaseLooper.kt |  4 +---
 .../deviceserver/ios/ActiveDevices.kt         | 21 +++++++++----------
 .../automation/deviceserver/util/Support.kt   |  6 +++---
 4 files changed, 20 insertions(+), 21 deletions(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt
index c78e1dd2..63bf457d 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt
@@ -6,6 +6,7 @@ import com.zaxxer.nuprocess.NuProcessBuilder
 import net.logstash.logback.marker.MapEntriesAppendingMarker
 import org.slf4j.Marker
 import java.time.Duration
+import java.util.concurrent.TimeUnit
 
 class RemoteShellCommand(
     private val remoteHost: String,
@@ -58,17 +59,18 @@ class RemoteShellCommand(
                       returnFailure: Boolean, logMarker: Marker?,
                       processListener: IShellCommandListener): CommandResult {
         val cmd = getCommandWithSSHPrefix(command)
-        val start = System.currentTimeMillis()
+        val startTime = System.nanoTime()
         val result = super.exec(cmd, getEnvironmentForSSH(), timeOut, returnFailure, logMarker, processListener)
-        val elapsed = System.currentTimeMillis() - start
+        val elapsedTime = System.nanoTime() - startTime
+        val elapsedMillis = TimeUnit.NANOSECONDS.toMillis(elapsedTime)
         val marker = MapEntriesAppendingMarker(
             mapOf(
                 LogMarkers.HOSTNAME to remoteHost,
-                LogMarkers.SSH_PROFILING_MS to elapsed
+                LogMarkers.SSH_PROFILING_MS to elapsedMillis
             )
         )
         logger.debug(
-            marker, "Execution of SSH command took $elapsed ms. Command: $cmd")
+            marker, "Execution of SSH command took $elapsedMillis ms. Command: $cmd")
 
         if (result.exitCode == SSH_ERROR) {
             // FIXME: Check stdout and stderr, if they are empty – ssh timeout, otherwise, it is likely to be command error
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/util/AutoreleaseLooper.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/util/AutoreleaseLooper.kt
index d9b618d9..a368f8ed 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/util/AutoreleaseLooper.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/util/AutoreleaseLooper.kt
@@ -25,14 +25,12 @@ class AutoreleaseLooper : IAutoreleaseLooper {
                     logger.warn("Autorelease thread ignored: $t")
                 }
 
-                val seconds = max(1, (deviceManager.nextReleaseAtSeconds() - currentTimeSeconds()))
+                val seconds = max(1, (deviceManager.nextReleaseAtSeconds() - TimeUnit.NANOSECONDS.toSeconds(System.nanoTime())))
                 delay(seconds, TimeUnit.SECONDS)
             }
         }
     }
 
-    private fun currentTimeSeconds() = System.currentTimeMillis() / 1000
-
     private fun autoRelease(deviceManager: DeviceManager) {
         val jobs = deviceManager.readyForRelease().map { deviceRef ->
             launch {
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt
index 0cad0390..17f413aa 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt
@@ -7,6 +7,7 @@ import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundEx
 import org.slf4j.LoggerFactory
 import java.time.Duration
 import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.TimeUnit
 
 data class SessionEntry(
         val ref: DeviceRef,
@@ -25,7 +26,7 @@ class ActiveDevices(
 
     companion object {
         private val DEFAULT_RELEASE_TIMEOUT: Duration = Duration.ofSeconds(600)
-        fun currentTimeSecondsProvider(): Long = System.currentTimeMillis() / 1000
+        fun currentTimeSecondsProvider(): Long = TimeUnit.NANOSECONDS.toSeconds(System.nanoTime())
     }
 
     fun deviceRefs(): Set {
@@ -94,21 +95,20 @@ class ActiveDevices(
 
     fun readyForRelease(): List {
         val secondsNow = currentTimeSeconds()
-        return devices.filter { with(it.value) { releaseTimeout.seconds + updatedAtSeconds <= secondsNow } }
-                .map { it.key }
+        return devices.filter { (_, session) -> secondsNow - session.updatedAtSeconds >= session.releaseTimeout.seconds }
+                .map { (deviceRef, _) -> deviceRef }
                 .also { logger.info("Ready to release $it"); }
     }
 
     fun nextReleaseAtSeconds(): Long {
-        val sessionEntry = devices.minBy {
-            it.value.updatedAtSeconds + it.value.releaseTimeout.seconds
-        }
+        val sessionEntry = devices
+            .map { it.value }
+            .minBy { it.updatedAtSeconds + it.releaseTimeout.seconds }
 
-        val nextReleaseAtSeconds: Long
-        if (sessionEntry != null) {
-            nextReleaseAtSeconds = sessionEntry.value.updatedAtSeconds + sessionEntry.value.releaseTimeout.seconds
+        val nextReleaseAtSeconds = if (sessionEntry == null) {
+            currentTimeSeconds() + DEFAULT_RELEASE_TIMEOUT.seconds
         } else {
-            nextReleaseAtSeconds = currentTimeSeconds() + DEFAULT_RELEASE_TIMEOUT.seconds
+            sessionEntry.updatedAtSeconds + sessionEntry.releaseTimeout.seconds
         }
 
         logger.info("nextReleaseAtSeconds = $nextReleaseAtSeconds seconds")
@@ -138,5 +138,4 @@ class ActiveDevices(
     private fun sessionByRef(ref: String): SessionEntry {
         return devices[ref] ?: throw DeviceNotFoundException("Device [$ref] not found in active devices")
     }
-
 }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/Support.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/Support.kt
index ee41d22a..093d1fbe 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/Support.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/Support.kt
@@ -36,8 +36,8 @@ fun  executeWithTimeout(timeout: Duration, name: String, action: () -> T): T
 fun pollFor(timeOut: Duration, reasonName: String, shouldReturnOnTimeout: Boolean = false,
             retryInterval: Duration = Duration.ofSeconds(2), logger: Logger, marker: Marker, action: () -> Boolean) {
     var isSuccess: Boolean
-    val startMillis = System.currentTimeMillis()
-    val stopMillis = startMillis + timeOut.toMillis()
+    val timeOutNanos = timeOut.toNanos()
+    val startTime = System.nanoTime()
 
     logger.trace(marker, "Awaiting for: $reasonName...")
     do {
@@ -49,7 +49,7 @@ fun pollFor(timeOut: Duration, reasonName: String, shouldReturnOnTimeout: Boolea
         } else {
             Thread.sleep(retryInterval.toMillis())
         }
-    } while (!isSuccess && stopMillis > System.currentTimeMillis())
+    } while (!isSuccess && System.nanoTime() - startTime < timeOutNanos)
 
     if (!isSuccess && !shouldReturnOnTimeout) {
         val message = "$reasonName failed after waiting ${timeOut.seconds} seconds"

From a8e78e2f2a8ef410509b0d5e322f2f809616c4d4 Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Wed, 1 May 2019 15:39:32 +0100
Subject: [PATCH 101/131] API to perform graceful node restart

---
 .../automation/deviceserver/DeviceServer.kt   | 22 +++++-
 .../automation/deviceserver/data/DeviceRef.kt |  1 +
 .../deviceserver/host/DevicesNode.kt          |  2 +-
 .../deviceserver/host/ISimulatorsNode.kt      |  1 +
 .../deviceserver/host/SimulatorsNode.kt       |  2 +-
 .../host/management/DeviceManager.kt          |  4 +
 .../host/management/NodeRegistrar.kt          | 34 ++++++++-
 .../host/management/NodeRegistry.kt           |  3 +-
 .../host/management/NodeRestarter.kt          | 76 +++++++++++++++++++
 .../host/management/NodeWrapper.kt            | 14 +++-
 .../deviceserver/ios/ActiveDevices.kt         |  5 ++
 .../deviceserver/host/NodeRegistryTest.kt     | 40 ++++++++++
 12 files changed, 192 insertions(+), 12 deletions(-)
 create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRestarter.kt

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
index f2fc4c45..4ece5e98 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
@@ -5,10 +5,7 @@ import com.badoo.automation.deviceserver.controllers.StatusController
 import com.badoo.automation.deviceserver.data.*
 import com.badoo.automation.deviceserver.host.HostFactory
 import com.badoo.automation.deviceserver.host.management.DeviceManager
-import com.badoo.automation.deviceserver.host.management.errors.DeviceCreationException
-import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException
-import com.badoo.automation.deviceserver.host.management.errors.NoAliveNodesException
-import com.badoo.automation.deviceserver.host.management.errors.OverCapacityException
+import com.badoo.automation.deviceserver.host.management.errors.*
 import com.fasterxml.jackson.databind.JsonNode
 import com.fasterxml.jackson.databind.SerializationFeature
 import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule
@@ -35,6 +32,7 @@ import io.ktor.server.engine.ApplicationEngineEnvironmentReloading
 import io.ktor.server.engine.ShutDownUrl
 import org.slf4j.LoggerFactory
 import java.io.File
+import java.lang.IllegalStateException
 import java.net.NetworkInterface
 import java.util.*
 
@@ -151,6 +149,21 @@ fun Application.module() {
                 call.respond(config)
             }
         }
+
+        route("nodes") {
+            post("restart_gracefully") {
+                val params = jsonContent(call)
+                val isParallelRestart = params["parallel"]?.asBoolean() ?: false
+                val restartScheduled = deviceManager.restartNodesGracefully(isParallelRestart)
+
+                if (restartScheduled) {
+                    call.respond(HttpStatusCode.Accepted, mapOf("status" to "Scheduled graceful restart of nodes"))
+                } else {
+                    call.respond(HttpStatusCode.TooManyRequests, mapOf("status" to "Nodes restart is already in progress"))
+                }
+            }
+        }
+
         route("devices") {
             get {
                 call.respond(devicesController.getDeviceRefs())
@@ -280,6 +293,7 @@ fun Application.module() {
         exception { exception: Throwable ->
             val statusCode = when (exception) {
                 is IllegalArgumentException -> HttpStatusCode(422, "Unprocessable Entity")
+                is IllegalStateException -> HttpStatusCode.Conflict
                 is DeviceNotFoundException -> HttpStatusCode.NotFound
                 is NoAliveNodesException -> HttpStatusCode.TooManyRequests
                 is OverCapacityException -> HttpStatusCode.TooManyRequests
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceRef.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceRef.kt
index 9401bd05..4add7c27 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceRef.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceRef.kt
@@ -1,3 +1,4 @@
 package com.badoo.automation.deviceserver.data
 
 typealias DeviceRef = String
+typealias NodeRef = String
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
index deff7155..3dadf2bc 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
@@ -19,7 +19,7 @@ import java.util.concurrent.TimeUnit
 
 class DevicesNode(
     private val remote: IRemote,
-    private val publicHostName: String,
+    override val publicHostName: String,
     portAllocator: PortAllocator = PortAllocator(),
     wdaRunnerXctest: File,
     knownDevices: List,
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
index d75f06d8..551c7dc4 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
@@ -45,4 +45,5 @@ interface ISimulatorsNode {
     fun dispose()
     fun uninstallApplication(deviceRef: DeviceRef, bundleId: String)
     fun setEnvironmentVariables(deviceRef: DeviceRef, envs: Map)
+    val publicHostName: String
 }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
index d860e9ac..1ef68b09 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
@@ -20,7 +20,7 @@ import java.util.concurrent.ConcurrentHashMap
 
 class SimulatorsNode(
         val remote: IRemote,
-        private val publicHostName: String,
+        override val publicHostName: String,
         private val hostChecker: ISimulatorHostChecker,
         private val simulatorLimit: Int,
         concurrentBoots: Int,
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
index e77f45d2..d56882ba 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
@@ -42,6 +42,10 @@ class DeviceManager(
         autoRegistrar.startAutoRegistering()
     }
 
+    fun restartNodesGracefully(isParallelRestart: Boolean): Boolean {
+        return autoRegistrar.restartNodesGracefully(isParallelRestart, INFINITE_DEVICE_TIMEOUT)
+    }
+
     fun launchAutoReleaseLoop() {
         autoreleaseLooper.autoreleaseLoop(this)
     }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt
index 0e79c22c..7b612477 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt
@@ -18,6 +18,10 @@ class NodeRegistrar(
 
     private val logger = LoggerFactory.getLogger(javaClass.simpleName)
     private var autoRegisteringJob: Future<*>? = null
+    @Volatile
+    private var restartingJob: Future<*>? = null
+    private val nodeRestarter = NodeRestarter(nodeRegistry)
+
     val nodeWrappers: List = nodesConfig.map {
         NodeWrapper(it, nodeFactory, nodeRegistry)
     }
@@ -49,22 +53,44 @@ class NodeRegistrar(
         logger.debug("Going to auto register ${unregistered.map(NodeWrapper::toString)}")
         val executor = Executors.newFixedThreadPool(unregistered.size)
         val results: List> = unregistered.map { nodeWrapper ->
-            executor.submit({
+            executor.submit {
                 nodeWrapper.stop()
                 if (nodeWrapper.start()) {
                     nodeRegistry.add(nodeWrapper)
+                    nodeWrapper.enable()
                     nodeWrapper.startPeriodicHealthCheck()
                 }
-            })
+            }
         }
         executor.shutdown()
         results.forEach { result ->
             try {
                 result.get()
             } catch (e: Throwable) {
-                logger.debug("Error while starting node", e)
+                logger.error("Error while starting node", e)
             }
         }
         nodeRegistry.setInitialRegistrationComplete()
     }
-}
\ No newline at end of file
+
+    @Synchronized
+    fun restartNodesGracefully(isParallel: Boolean, infiniteDeviceTimeout: Duration): Boolean {
+        val job = restartingJob
+
+        if (job == null || job.isDone) {
+            val executor = Executors.newSingleThreadExecutor()
+            restartingJob = executor.submit {
+                nodeRestarter.restartNodeWrappers(
+                    nodeRegistry.getAll(),
+                    isParallel,
+                    infiniteDeviceTimeout
+                )
+            }
+            executor.shutdown()
+
+            return true
+        } else {
+            return false
+        }
+    }
+}
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt
index 96d84514..14a49df6 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt
@@ -51,6 +51,7 @@ class NodeRegistry(val activeDevices: ActiveDevices = ActiveDevices()) {
 
     private fun getAlive(): Set {
         val filteredStream: Stream = nodeWrappers
+            .filter { it.isEnabled }
             .parallelStream()
             .filter { it.isAlive() }
         return filteredStream.collect(toSet())
@@ -96,4 +97,4 @@ class NodeRegistry(val activeDevices: ActiveDevices = ActiveDevices()) {
         runBlocking { list.forEach { it.join() } }
         nodeWrappers.clear()
     }
-}
\ No newline at end of file
+}
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRestarter.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRestarter.kt
new file mode 100644
index 00000000..0e6a974e
--- /dev/null
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRestarter.kt
@@ -0,0 +1,76 @@
+package com.badoo.automation.deviceserver.host.management
+
+import com.badoo.automation.deviceserver.host.ISimulatorsNode
+import com.badoo.automation.deviceserver.ios.SessionEntry
+import org.slf4j.LoggerFactory
+import java.time.Duration
+
+class NodeRestarter(
+    private val nodeRegistry: NodeRegistry
+) {
+    private val logger = LoggerFactory.getLogger(javaClass.simpleName)
+    private val activeSessionsCheckInterval = Duration.ofSeconds(30)
+
+    fun restartNodeWrappers(
+        nodes: Set,
+        isParallel: Boolean,
+        infiniteDeviceTimeout: Duration
+    ) {
+        val nodesToRestart = if (isParallel) {
+            logger.info("Going to restart nodes in parallel.")
+            nodes.parallelStream()
+        } else {
+            logger.info("Going to restart nodes sequentially.")
+            nodes.stream()
+        }
+
+        nodesToRestart.forEach { nodeWrapper ->
+            nodeWrapper.disable()
+
+            if (!waitForActiveSessionsReleased(nodeWrapper.node, infiniteDeviceTimeout)) {
+                logger.error("Failed to re-start node $nodeWrapper as it has active sessions with infinite timeout")
+                nodeWrapper.enable()
+                return@forEach
+            }
+
+            nodeWrapper.stop()
+
+            if (nodeWrapper.start()) {
+                nodeWrapper.startPeriodicHealthCheck()
+                nodeWrapper.enable()
+            } else {
+                logger.error("Failed to re-start node $nodeWrapper")
+                nodeRegistry.removeIfPresent(nodeWrapper)
+            }
+        }
+    }
+
+    private fun activeSessions(node: ISimulatorsNode): Collection {
+        return nodeRegistry.activeDevices.activeDevicesByNode(node.publicHostName).values
+    }
+
+    private fun hasInfiniteTimeout(sessions: Collection, infiniteDeviceTimeout: Duration): Boolean {
+        return sessions.any { it.releaseTimeout == infiniteDeviceTimeout }
+    }
+
+    private fun waitForActiveSessionsReleased(node: ISimulatorsNode, infiniteDeviceTimeout: Duration): Boolean {
+        while (!Thread.currentThread().isInterrupted) {
+            val sessions = activeSessions(node)
+
+            if (sessions.isEmpty()) {
+                logger.trace("Node $node has no active sessions")
+                return true
+            } else {
+                if (hasInfiniteTimeout(sessions, infiniteDeviceTimeout)) {
+                    logger.error("Some sessions have infinite device timeout. Sessions: $sessions")
+                    return false
+                }
+
+                logger.debug("Node $node still has active sessions: $sessions")
+                Thread.sleep(activeSessionsCheckInterval.toMillis())
+            }
+        }
+
+        return false
+    }
+}
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeWrapper.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeWrapper.kt
index aff30bfc..7236d396 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeWrapper.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeWrapper.kt
@@ -33,10 +33,12 @@ class NodeWrapper(
     val node: ISimulatorsNode = hostFactory.getHostFromConfig(config)
 
     var lastError: Exception? = null
+    @Volatile var isEnabled: Boolean = true
+        private set
 
     fun isAlive(): Boolean = isStarted && node.isReachable() && node.isNodePrepared
 
-    override fun toString(): String = "NodeWrapper for ${config.host}"
+    override fun toString(): String = "NodeWrapper for ${config.publicHost}"
 
     fun start(): Boolean {
         lock.withLock {
@@ -79,6 +81,16 @@ class NodeWrapper(
         }
     }
 
+    fun disable() {
+        isEnabled = false
+        logger.info(logMarker, "Disabled $this")
+    }
+
+    fun enable() {
+        isEnabled = true
+        logger.info(logMarker, "Enabled $this")
+    }
+
     fun startPeriodicHealthCheck() {
         if (!isStarted) {
             throw RuntimeException("Can not start polling stopped node. Call start() on node first")
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt
index 17f413aa..a59776af 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt
@@ -2,6 +2,7 @@ package com.badoo.automation.deviceserver.ios
 
 import com.badoo.automation.deviceserver.data.DeviceDTO
 import com.badoo.automation.deviceserver.data.DeviceRef
+import com.badoo.automation.deviceserver.data.NodeRef
 import com.badoo.automation.deviceserver.host.ISimulatorsNode
 import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException
 import org.slf4j.LoggerFactory
@@ -138,4 +139,8 @@ class ActiveDevices(
     private fun sessionByRef(ref: String): SessionEntry {
         return devices[ref] ?: throw DeviceNotFoundException("Device [$ref] not found in active devices")
     }
+
+    fun activeDevicesByNode(ref: NodeRef): Map {
+        return devices.filter { it.value.node.publicHostName == ref }
+    }
 }
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeRegistryTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeRegistryTest.kt
index f5e59b5c..d4bd13fa 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeRegistryTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeRegistryTest.kt
@@ -4,6 +4,7 @@ import com.badoo.automation.deviceserver.data.DesiredCapabilities
 import com.badoo.automation.deviceserver.deviceDTOStub
 import com.badoo.automation.deviceserver.host.management.NodeRegistry
 import com.badoo.automation.deviceserver.host.management.NodeWrapper
+import com.badoo.automation.deviceserver.host.management.errors.NoAliveNodesException
 import com.badoo.automation.deviceserver.ios.ActiveDevices
 import com.badoo.automation.deviceserver.mockThis
 import com.nhaarman.mockito_kotlin.any
@@ -12,9 +13,12 @@ import com.nhaarman.mockito_kotlin.verify
 import com.nhaarman.mockito_kotlin.whenever
 import org.hamcrest.CoreMatchers.equalTo
 import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Test
 import java.time.Duration
+import kotlin.test.assertFailsWith
+import kotlin.test.assertNotNull
 
 class NodeRegistryTest {
     private val activeDevices: ActiveDevices = mockThis()
@@ -32,9 +36,13 @@ class NodeRegistryTest {
     fun setUp() {
         whenever(wrappedNode1.remoteAddress).thenReturn("n1")
         whenever(wrappedNode2.remoteAddress).thenReturn("n2")
+        whenever(wrappedNode1.publicHostName).thenReturn("n1")
+        whenever(wrappedNode2.publicHostName).thenReturn("n2")
 
         whenever(nodeWrapper1.node).thenReturn(wrappedNode1)
         whenever(nodeWrapper2.node).thenReturn(wrappedNode2)
+        whenever(nodeWrapper1.isEnabled).thenReturn(true)
+        whenever(nodeWrapper2.isEnabled).thenReturn(true)
 
         nodeRegistry.add(nodeWrapper1)
         nodeRegistry.add(nodeWrapper2)
@@ -69,6 +77,7 @@ class NodeRegistryTest {
 
     @Test
     fun createSimulatorByCapacity() {
+        // arrange
         val deviceTimeout = Duration.ofSeconds(0)
         whenever(nodeWrapper1.isAlive()).thenReturn(true)
         whenever(nodeWrapper2.isAlive()).thenReturn(true)
@@ -84,5 +93,36 @@ class NodeRegistryTest {
         // assert
         verify(activeDevices).registerDevice("", wrappedNode2, deviceTimeout, null)
     }
+
+    @Test
+    fun createDeviceIgnoresDisabledNodes() {
+        // arrange
+        whenever(nodeWrapper1.isAlive()).thenReturn(true)
+        whenever(wrappedNode1.createDeviceAsync(desiredCapabilities)).then { deviceDTOStub("") }
+        whenever(wrappedNode1.capacityRemaining(desiredCapabilities)).thenReturn(capacityNotBusy)
+        assertNotNull(nodeRegistry.createDeviceAsync(desiredCapabilities, Duration.ZERO, null))
+
+        // act
+        whenever(nodeWrapper1.isEnabled).thenReturn(false)
+
+        // assert
+        assertFailsWith {
+            nodeRegistry.createDeviceAsync(desiredCapabilities, Duration.ZERO, null)
+        }
+    }
+
+    @Test
+    fun capacityIgnoresDisabledNodes() {
+        // arrange
+        whenever(wrappedNode1.totalCapacity(any())).thenReturn(1)
+        whenever(nodeWrapper1.isAlive()).thenReturn(true)
+        assertEquals(mapOf("total" to 1), nodeRegistry.capacitiesTotal(desiredCapabilities))
+
+        // act
+        whenever(nodeWrapper1.isEnabled).thenReturn(false)
+
+        // assert
+        assertEquals(mapOf("total" to 0), nodeRegistry.capacitiesTotal(desiredCapabilities))
+    }
 }
 

From cad1325c83a71dda4245d337798034910f65c104 Mon Sep 17 00:00:00 2001
From: Nikolai Abalov 
Date: Tue, 14 May 2019 14:02:13 +0100
Subject: [PATCH 102/131] Add busy_timeout to handle TCC database being busy

---
 .../deviceserver/ios/simulator/SimulatorPermissions.kt          | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt
index da66a046..9358fd27 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt
@@ -49,7 +49,7 @@ class SimulatorPermissions(
         }
 
         val path = File(deviceSetPath, simulator.udid)
-        val sqlCmd = "sqlite3 ${path.absolutePath}/data/Library/TCC/TCC.db \"$sql\""
+        val sqlCmd = "sqlite3 ${path.absolutePath}/data/Library/TCC/TCC.db \"pragma busy_timeout=1000; $sql\""
 
         val result = remote.shell(sqlCmd)
 

From 13d375c4dc120f9eb4e7dba3d0a153e81d9f85de Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Fri, 14 Jun 2019 20:25:38 +0100
Subject: [PATCH 103/131] Add TrustStore to Simulator on creation

---
 .../deviceserver/ApplicationConfiguration.kt  |  2 ++
 .../deviceserver/ios/simulator/Simulator.kt   | 22 ++++++++++++++++++-
 2 files changed, 23 insertions(+), 1 deletion(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt
index f7ed945d..0f6e6ed4 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt
@@ -20,4 +20,6 @@ class ApplicationConfiguration {
     val remoteWdaSimulatorBundleRoot = System.getProperty("remote.wda.simulator.bundle.path", "/usr/local/opt/web_driver_agent_simulator")
 
     val remoteWdaDeviceBundleRoot = System.getProperty("remote.wda.device.bundle.path", "/usr/local/opt/web_driver_agent_device")
+
+    val trustStorePath: String = System.getProperty("trust.store.path", "")
 }
\ No newline at end of file
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
index 0f827189..0fdf734e 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
@@ -1,5 +1,6 @@
 package com.badoo.automation.deviceserver.ios.simulator
 
+import com.badoo.automation.deviceserver.ApplicationConfiguration
 import com.badoo.automation.deviceserver.LogMarkers
 import com.badoo.automation.deviceserver.WaitTimeoutError
 import com.badoo.automation.deviceserver.command.ShellUtils
@@ -43,7 +44,8 @@ class Simulator (
         private val concurrentBootsPool: ThreadPoolDispatcher,
         headless: Boolean,
         private val useWda: Boolean,
-        override val fbsimctlSubject: String
+        override val fbsimctlSubject: String,
+        private val trustStoreFile: String = ApplicationConfiguration().trustStorePath
 ) : ISimulator
 {
     private companion object {
@@ -238,6 +240,10 @@ class Simulator (
         logger.info(logMarker, "Erasing simulator ${this@Simulator} before creating a backup")
         remote.fbsimctl.eraseSimulator(udid)
 
+        if (trustStoreFile.isNotEmpty()) {
+            copyTrustStore()
+        }
+
         logger.info(logMarker, "Booting ${this@Simulator} before creating a backup")
         logTiming("initial boot") { boot() }
 
@@ -247,6 +253,20 @@ class Simulator (
         backup.create()
     }
 
+    private fun copyTrustStore() {
+        logger.debug(logMarker, "Copying trust store to ${this@Simulator}")
+        val keyChainLocation = Paths.get(deviceSetPath, udid, "data", "Library", "Keychains").toFile().absolutePath
+        remote.shell("mkdir -p $keyChainLocation", returnOnFailure = false)
+
+        if (remote.isLocalhost()) {
+            remote.shell("cp $trustStoreFile $keyChainLocation", returnOnFailure = false)
+        } else {
+            remote.rsync(trustStoreFile, keyChainLocation, setOf("--delete"))
+        }
+
+        logger.info(logMarker, "Copied trust store to ${this@Simulator}")
+    }
+
     private fun shutdown() {
         logger.info(logMarker, "Shutting down ${this@Simulator}")
         remote.fbsimctl.shutdown(udid)

From 12766d0d9939c896f8aae22f59442944c5cd1f21 Mon Sep 17 00:00:00 2001
From: Sergey Plevako 
Date: Thu, 11 Jul 2019 12:06:21 +0100
Subject: [PATCH 104/131] Add "Open url" command for iOS Simulators

---
 .../com/badoo/automation/deviceserver/DeviceServer.kt     | 7 +++++++
 .../deviceserver/controllers/DevicesController.kt         | 4 ++++
 .../com/badoo/automation/deviceserver/data/UrlDto.kt      | 8 ++++++++
 .../com/badoo/automation/deviceserver/host/DevicesNode.kt | 4 ++++
 .../badoo/automation/deviceserver/host/ISimulatorsNode.kt | 1 +
 .../badoo/automation/deviceserver/host/SimulatorsNode.kt  | 4 ++++
 .../deviceserver/host/management/DeviceManager.kt         | 4 ++++
 .../automation/deviceserver/ios/simulator/ISimulator.kt   | 1 +
 .../automation/deviceserver/ios/simulator/Simulator.kt    | 8 ++++++++
 9 files changed, 41 insertions(+)
 create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/UrlDto.kt

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
index 4ece5e98..6584e862 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
@@ -256,6 +256,13 @@ fun Application.module() {
                         call.respond(devicesController.resetDiagnostic(ref, type))
                     }
                 }
+                route("openurl") {
+                    post {
+                        val ref = param(call, "ref")
+                        val url = jsonContent(call).url
+                        call.respond(devicesController.openUrl(ref, url))
+                    }
+                }
                 route("video") {
                     get {
                         //FIXME: should be a better way of streaming a file over HTTP. without caching bytes in server's memory. Investigating ByteReadChannel
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt
index 9a11e026..8bcc5f6c 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt
@@ -133,6 +133,10 @@ class DevicesController(private val deviceManager: DeviceManager) {
         return deviceManager.pullFile(ref, dataPath)
     }
 
+    fun openUrl(ref: DeviceRef, url: String) {
+        return deviceManager.openUrl(ref, url)
+    }
+
     fun uninstallApplication(ref: DeviceRef, bundleId: String): EmptyMap {
         deviceManager.uninstallApplication(ref, bundleId)
         return happy
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/UrlDto.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/UrlDto.kt
new file mode 100644
index 00000000..bb268817
--- /dev/null
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/UrlDto.kt
@@ -0,0 +1,8 @@
+package com.badoo.automation.deviceserver.data
+
+import com.fasterxml.jackson.annotation.JsonProperty
+import java.nio.file.Path
+
+class UrlDto(
+    @JsonProperty("url")
+    val url: String)
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
index 3dadf2bc..7789939f 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
@@ -95,6 +95,10 @@ class DevicesNode(
         throw(NotImplementedError("Shake gesture is not supported by physical devices"))
     }
 
+    override fun openUrl(deviceRef: DeviceRef, url: String) {
+        throw(NotImplementedError("Opening URL is not supported by physical devices"))
+    }
+
     override fun endpointFor(deviceRef: DeviceRef, port: Int): URL {
         val device = slotByExternalRef(deviceRef).device
         return device.endpointFor(port)
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
index 551c7dc4..6952dd1e 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
@@ -12,6 +12,7 @@ interface ISimulatorsNode {
     fun setPermissions(deviceRef: DeviceRef, appPermissions: AppPermissionsDto)
     fun clearSafariCookies(deviceRef: DeviceRef)
     fun shake(deviceRef: DeviceRef)
+    fun openUrl(deviceRef: DeviceRef, url: String)
     fun endpointFor(deviceRef: DeviceRef, port: Int): URL
     fun lastCrashLog(deviceRef: DeviceRef): CrashLog
     fun crashLogs(deviceRef: DeviceRef, pastMinutes: Long?): List
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
index 1ef68b09..60206a06 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
@@ -268,6 +268,10 @@ class SimulatorsNode(
         return getDeviceFor(deviceRef).dataContainer(dataPath.bundleId).readFile(dataPath.path)
     }
 
+    override fun openUrl(deviceRef: DeviceRef, url: String) {
+        getDeviceFor(deviceRef).openUrl(url)
+    }
+
     override fun uninstallApplication(deviceRef: DeviceRef, bundleId: String) {
         getDeviceFor(deviceRef).uninstallApplication(bundleId)
     }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
index d56882ba..7d2c55cd 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
@@ -109,6 +109,10 @@ class DeviceManager(
         nodeRegistry.activeDevices.getNodeFor(ref).shake(ref)
     }
 
+    fun openUrl(ref: DeviceRef, url: String) {
+        nodeRegistry.activeDevices.getNodeFor(ref).openUrl(ref, url)
+    }
+
     fun startVideo(ref: DeviceRef) {
         nodeRegistry.activeDevices.getNodeFor(ref).videoRecordingStart(ref)
     }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt
index 0f72399d..510aff06 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt
@@ -35,6 +35,7 @@ interface ISimulator {
     fun release(reason: String)
     fun clearSafariCookies(): Map
     fun shake(): Boolean
+    fun openUrl(url: String): Boolean
     fun lastCrashLog(): CrashLog
     fun crashLogs(pastMinutes: Long?): List
     fun dataContainer(bundleId: String): DataContainer
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
index 0fdf734e..628a930e 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
@@ -615,6 +615,14 @@ class Simulator (
         return result.isSuccess
     }
 
+    override fun openUrl(url: String) : Boolean {
+        val urlString = if (remote.isLocalhost()) { url } else { "\"$url\"" }
+
+        val command = listOf("xcrun", "simctl", "openurl", udid, urlString)
+        val result = remote.execIgnoringErrors(command)
+        return result.isSuccess
+    }
+
     //region last crash log
 
     @Deprecated("Will be removed in favor of crashLogs. Note that crashLogs does not delete old crashes")

From 32437cd563b890fecee1e25bad6f8326a86a11a4 Mon Sep 17 00:00:00 2001
From: Sergey Plevako 
Date: Thu, 18 Jul 2019 16:11:33 +0100
Subject: [PATCH 105/131] Add Deferrer helper class

---
 .../automation/deviceserver/util/Deferrer.kt  | 43 +++++++++++++++++++
 1 file changed, 43 insertions(+)
 create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/Deferrer.kt

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/Deferrer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/Deferrer.kt
new file mode 100644
index 00000000..0ce81f24
--- /dev/null
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/Deferrer.kt
@@ -0,0 +1,43 @@
+package com.badoo.automation.deviceserver.util
+
+import org.slf4j.Logger
+
+/**
+ * Deferrer tries to mimic Defer behaviour of Golang (see https://golang.org/ref/spec#Defer_statements)
+ * with slightly changed behaviour - all deferred blocks are executed even if first one throws exception
+ */
+class Deferrer() {
+    private val deferredActions = arrayListOf<() -> Unit>()
+
+    /**
+     * Adds a function or a block to a list of deferred functions which are executed in reverse order.
+     * @param block function or code block
+     */
+    fun defer(block: () -> Unit) {
+        deferredActions.add(block)
+    }
+
+    fun done(logger: Logger) {
+        deferredActions.reversed().forEach { deferredAction ->
+            try {
+                deferredAction()
+            } catch (e: Exception) {
+                logger.error("Deferred action has caused an exception:", e)
+            }
+        }
+    }
+}
+
+/**
+ * Executes enclosing code in context of Deferrer tries to mimic Defer behaviour of Golang.
+ * All deferred blocks are executed even if first one throws exception.
+ * @see Deferrer
+ */
+inline fun  withDefers(logger: Logger, body: Deferrer.() -> T): T {
+    val deferrer = Deferrer()
+    return try {
+        deferrer.body()
+    } finally {
+        deferrer.done(logger)
+    }
+}

From cb27043f34334b8fe826d33df044c618a1a4f464 Mon Sep 17 00:00:00 2001
From: Sergey Plevako 
Date: Mon, 8 Jul 2019 18:01:30 +0100
Subject: [PATCH 106/131] Add media to iOS Simulator using xcrun simctl
 addmedia command

---
 .../automation/deviceserver/DeviceServer.kt   |  5 +++
 .../controllers/DevicesController.kt          |  5 +++
 .../automation/deviceserver/data/FileDto.kt   | 11 +++++++
 .../deviceserver/host/DevicesNode.kt          |  4 +++
 .../deviceserver/host/ISimulatorsNode.kt      |  1 +
 .../deviceserver/host/SimulatorsNode.kt       |  4 +++
 .../host/management/DeviceManager.kt          |  4 +++
 .../deviceserver/ios/simulator/data/Media.kt  | 32 ++++++++++++++++---
 8 files changed, 62 insertions(+), 4 deletions(-)
 create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/FileDto.kt

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
index 6584e862..d711fa8d 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
@@ -237,6 +237,11 @@ fun Application.module() {
                         val ref = param(call, "ref")
                         call.respond(devicesController.resetMedia(ref))
                     }
+                    post {
+                        val ref = param(call, "ref")
+                        val dataPath = jsonContent(call)
+                        call.respond(devicesController.addMedia(ref, dataPath.file_name, dataPath.data))
+                    }
                 }
                 route("diagnose/{type}") {
                     get {
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt
index 8bcc5f6c..1b4d6351 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt
@@ -107,6 +107,11 @@ class DevicesController(private val deviceManager: DeviceManager) {
         return happy
     }
 
+    fun addMedia(ref: DeviceRef, fileName: String, data: ByteArray): EmptyMap {
+        deviceManager.addMedia(ref, fileName, data)
+        return happy
+    }
+
     fun getDiagnostic(ref: DeviceRef, type: String, query: DiagnosticQuery): Diagnostic {
         val diagnosticType = DiagnosticType.fromString(type)
         return deviceManager.getDiagnostic(ref, diagnosticType, query)
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/FileDto.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/FileDto.kt
new file mode 100644
index 00000000..2d8cc466
--- /dev/null
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/FileDto.kt
@@ -0,0 +1,11 @@
+package com.badoo.automation.deviceserver.data
+
+import com.fasterxml.jackson.annotation.JsonProperty
+
+data class FileDto(
+    @JsonProperty("file_name")
+    val file_name: String,
+
+    @JsonProperty("data")
+    val data: ByteArray
+)
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
index 7789939f..2fabbdaf 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
@@ -47,6 +47,10 @@ class DevicesNode(
         throw(NotImplementedError("Resetting media is not supported by physical devices"))
     }
 
+    override fun addMedia(deviceRef: DeviceRef, fileName: String, data: ByteArray) {
+        throw(NotImplementedError("Adding media is not supported by physical devices"))
+    }
+
     override fun getDiagnostic(deviceRef: DeviceRef, type: DiagnosticType, query: DiagnosticQuery): Diagnostic {
         throw(NotImplementedError("Diagnostic is not supported by physical devices"))
     }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
index 6952dd1e..70af222f 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
@@ -27,6 +27,7 @@ interface ISimulatorsNode {
     fun listFiles(deviceRef: DeviceRef, dataPath: DataPath): List
     fun pullFile(deviceRef: DeviceRef, dataPath: DataPath): ByteArray
 
+    fun addMedia(deviceRef: DeviceRef, fileName: String, data: ByteArray)
     fun resetMedia(deviceRef: DeviceRef)
 
     fun getDiagnostic(deviceRef: DeviceRef, type: DiagnosticType, query: DiagnosticQuery): Diagnostic
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
index 60206a06..0e53367d 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
@@ -107,6 +107,10 @@ class SimulatorsNode(
         getDeviceFor(deviceRef).media.reset()
     }
 
+    override fun addMedia(deviceRef: DeviceRef, fileName: String, data: ByteArray) {
+        getDeviceFor(deviceRef).media.addMedia(File(fileName), data)
+    }
+
     override fun getDiagnostic(deviceRef: DeviceRef, type: DiagnosticType, query: DiagnosticQuery): Diagnostic {
         return when (type) {
             DiagnosticType.SystemLog -> Diagnostic(
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
index 7d2c55cd..050667d3 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
@@ -185,6 +185,10 @@ class DeviceManager(
         nodeRegistry.activeDevices.getNodeFor(ref).resetMedia(ref)
     }
 
+    fun addMedia(ref: DeviceRef, fileName: String, data: ByteArray) {
+        nodeRegistry.activeDevices.getNodeFor(ref).addMedia(ref, fileName, data)
+    }
+
     fun getDiagnostic(ref: DeviceRef, type: DiagnosticType, query: DiagnosticQuery): Diagnostic {
         return nodeRegistry.activeDevices.getNodeFor(ref).getDiagnostic(ref, type, query)
     }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/Media.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/Media.kt
index 4aaf7d52..28aa1cb3 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/Media.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/Media.kt
@@ -2,6 +2,9 @@ package com.badoo.automation.deviceserver.ios.simulator.data
 
 import com.badoo.automation.deviceserver.data.UDID
 import com.badoo.automation.deviceserver.host.IRemote
+import com.badoo.automation.deviceserver.util.withDefers
+import org.slf4j.LoggerFactory
+import java.io.File
 import java.nio.file.Paths
 
 class Media(
@@ -10,12 +13,10 @@ class Media(
     deviceSetPath: String
 ) {
     private val mediaPath = Paths.get(deviceSetPath, udid, "data", "Media")
+    private val logger = LoggerFactory.getLogger(javaClass.simpleName)
 
     fun reset() {
-        val imagesPath = mediaPath.resolve("DCIM").toString()
-        val photoDataPath = mediaPath.resolve("PhotoData/Photos.sqlite").toString()
-
-        val removeCmd = "rm -f $imagesPath/**/* $photoDataPath*; touch $photoDataPath"
+        val removeCmd = "rm -rf $mediaPath"
 
         val result = remote.shell(removeCmd)
 
@@ -27,6 +28,29 @@ class Media(
         restartAssetsd()
     }
 
+    fun addMedia(file: File, data: ByteArray) {
+        withDefers(logger) {
+            val tmpFile = File.createTempFile(file.nameWithoutExtension, ".${file.extension}")
+            defer { tmpFile.delete() }
+            tmpFile.writeBytes(data)
+
+            val mediaPath: String = if (remote.isLocalhost()) {
+                tmpFile.absolutePath
+            } else {
+                val remoteMediaDir = remote.execIgnoringErrors(listOf("mktemp", "-d")).stdOut.trim()
+                defer { remote.execIgnoringErrors(listOf("rm", "-rf", remoteMediaDir)) }
+                remote.rsync(tmpFile.absolutePath, remoteMediaDir, setOf("-r", "--delete"))
+                File(remoteMediaDir, tmpFile.name).absolutePath
+            }
+
+            val result = remote.execIgnoringErrors(listOf("xcrun", "simctl", "addmedia", udid, mediaPath))
+
+            if (!result.isSuccess) {
+                throw RuntimeException("Could not add Media to device: $result")
+            }
+        }
+    }
+
     private fun restartAssetsd() {
         val restartCmd = listOf(
             "xcrun", "simctl", "spawn", udid, "launchctl", "kickstart", "-k", "-p", "system/com.apple.assetsd"

From 9e18b6a878942a353faf9b6f46db9797be0aa9de Mon Sep 17 00:00:00 2001
From: Sergey Plevako 
Date: Tue, 23 Jul 2019 18:26:44 +0100
Subject: [PATCH 107/131] Add media to iOS Simulator upon creation

---
 .../deviceserver/ApplicationConfiguration.kt   |  1 +
 .../deviceserver/ios/simulator/Simulator.kt    | 18 +++++++++++++++++-
 2 files changed, 18 insertions(+), 1 deletion(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt
index 0f6e6ed4..c5a5c197 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt
@@ -22,4 +22,5 @@ class ApplicationConfiguration {
     val remoteWdaDeviceBundleRoot = System.getProperty("remote.wda.device.bundle.path", "/usr/local/opt/web_driver_agent_device")
 
     val trustStorePath: String = System.getProperty("trust.store.path", "")
+    val assetsPath: String = System.getProperty("media.assets.path", "")
 }
\ No newline at end of file
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
index 628a930e..2425094d 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
@@ -28,6 +28,7 @@ import java.io.FileOutputStream
 import java.io.IOException
 import java.net.URI
 import java.net.URL
+import java.nio.file.Files
 import java.nio.file.Paths
 import java.time.Duration
 import java.util.concurrent.locks.ReentrantLock
@@ -45,7 +46,8 @@ class Simulator (
         headless: Boolean,
         private val useWda: Boolean,
         override val fbsimctlSubject: String,
-        private val trustStoreFile: String = ApplicationConfiguration().trustStorePath
+        private val trustStoreFile: String = ApplicationConfiguration().trustStorePath,
+        private val assetsPath: String = ApplicationConfiguration().assetsPath
 ) : ISimulator
 {
     private companion object {
@@ -247,6 +249,10 @@ class Simulator (
         logger.info(logMarker, "Booting ${this@Simulator} before creating a backup")
         logTiming("initial boot") { boot() }
 
+        if (assetsPath.isNotEmpty()) {
+            copyMediaAssets()
+        }
+
         logger.info(logMarker, "Shutting down ${this@Simulator} before creating a backup")
         shutdown()
 
@@ -267,6 +273,16 @@ class Simulator (
         logger.info(logMarker, "Copied trust store to ${this@Simulator}")
     }
 
+    private fun copyMediaAssets() {
+        logger.debug(logMarker, "Copying assets to ${this@Simulator}")
+        media.reset()
+        File(assetsPath).walk().filter { it.isFile }.forEach {
+           media.addMedia(it, it.readBytes())
+        }
+
+        logger.info(logMarker, "Copied assets to ${this@Simulator}")
+    }
+
     private fun shutdown() {
         logger.info(logMarker, "Shutting down ${this@Simulator}")
         remote.fbsimctl.shutdown(udid)

From 7f70446fd5162fa5ab980e26fb7854fe5ded83ef Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Mon, 29 Jul 2019 23:37:43 +0100
Subject: [PATCH 108/131] Collect crash reports from real devices

---
 .../automation/deviceserver/DeviceServer.kt   |  5 +++-
 .../controllers/DevicesController.kt          | 10 ++++++-
 .../deviceserver/host/DevicesNode.kt          |  5 ++++
 .../deviceserver/host/ISimulatorsNode.kt      |  1 +
 .../deviceserver/host/SimulatorsNode.kt       |  4 +++
 .../host/management/DeviceManager.kt          |  5 ++++
 .../deviceserver/ios/device/Device.kt         | 29 ++++++++++++++++++-
 7 files changed, 56 insertions(+), 3 deletions(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
index d711fa8d..620b93ee 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
@@ -202,9 +202,12 @@ fun Application.module() {
                 route("crashes") {
                     get {
                         val pastMinutes = call.request.queryParameters["pastMinutes"]?.toLong()
-
                         call.respond(devicesController.crashLogs(param(call, "ref"), pastMinutes))
                     }
+                    get("app") {
+                        val appName: String? = call.request.queryParameters["appName"]
+                        call.respond(devicesController.crashLogs(param(call, "ref"), appName))
+                    }
                     delete {
                         val rv = devicesController.deleteCrashLogs(param(call, "ref"))
                         call.respond(mapOf("result" to rv))
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt
index 1b4d6351..a45c3cd8 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt
@@ -63,7 +63,15 @@ class DevicesController(private val deviceManager: DeviceManager) {
     }
 
     fun crashLogs(ref: DeviceRef, pastMinutes: Long?): List> {
-        var logs = deviceManager.crashLogs(ref, pastMinutes)
+        val logs = deviceManager.crashLogs(ref, pastMinutes)
+
+        return logs.map {
+            mapOf("filename" to it.filename, "content" to it.content)
+        }
+    }
+
+    fun crashLogs(ref: DeviceRef, appName: String?): List> {
+        val logs = deviceManager.crashLogs(ref, appName)
 
         return logs.map {
             mapOf("filename" to it.filename, "content" to it.content)
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
index 2fabbdaf..ecd11be9 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
@@ -260,6 +260,11 @@ class DevicesNode(
         throw NotImplementedError()
     }
 
+    override fun crashLogs(deviceRef: DeviceRef, appName: String?): List {
+        val device = slotByExternalRef(deviceRef).device
+        return device.crashLogs(appName)
+    }
+
     override fun deleteCrashLogs(deviceRef: DeviceRef): Boolean {
         return false // crash logs are not supported on devices yet
     }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
index 70af222f..7ac1e3a0 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
@@ -16,6 +16,7 @@ interface ISimulatorsNode {
     fun endpointFor(deviceRef: DeviceRef, port: Int): URL
     fun lastCrashLog(deviceRef: DeviceRef): CrashLog
     fun crashLogs(deviceRef: DeviceRef, pastMinutes: Long?): List
+    fun crashLogs(deviceRef: DeviceRef, appName: String?): List
     fun deleteCrashLogs(deviceRef: DeviceRef): Boolean
     fun state(deviceRef: DeviceRef): SimulatorStatusDTO
 
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
index 0e53367d..1cafa659 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
@@ -213,6 +213,10 @@ class SimulatorsNode(
         return getDeviceFor(deviceRef).crashLogs(pastMinutes)
     }
 
+    override fun crashLogs(deviceRef: DeviceRef, appName: String?): List {
+        throw NotImplementedError()
+    }
+
     override fun deleteCrashLogs(deviceRef: DeviceRef): Boolean {
         return getDeviceFor(deviceRef).deleteCrashLogs()
     }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
index 050667d3..e4f8bf3d 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
@@ -97,6 +97,11 @@ class DeviceManager(
         return nodeRegistry.activeDevices.getNodeFor(ref).crashLogs(ref, pastMinutes)
     }
 
+    fun crashLogs(ref: DeviceRef, appName: String?): List {
+        val node = nodeRegistry.activeDevices.getNodeFor(ref)
+        return node.crashLogs(ref, appName)
+    }
+
     fun deleteCrashLogs(ref: DeviceRef): Boolean {
         return nodeRegistry.activeDevices.getNodeFor(ref).deleteCrashLogs(ref)
     }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt
index ae4c9c62..53060c7c 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt
@@ -13,12 +13,13 @@ import com.badoo.automation.deviceserver.util.pollFor
 import net.logstash.logback.marker.MapEntriesAppendingMarker
 import org.slf4j.LoggerFactory
 import org.slf4j.Marker
+import java.io.File
 import java.net.URI
 import java.net.URL
+import java.nio.file.Files
 import java.time.Duration
 import java.util.concurrent.Executors
 import java.util.concurrent.Future
-import java.io.File
 
 class Device(
     private val remote: IRemote,
@@ -176,6 +177,32 @@ class Device(
         return null
     }
 
+    fun crashLogs(appName: String?): List {
+        val crashReportsPath = Files.createTempDirectory("crashReports")
+        val filter = appName?.let { "--filter \"$appName\"" } ?: ""
+        val command = "/usr/local/bin/idevicecrashreport --udid $udid $filter ${crashReportsPath.toAbsolutePath()}"
+
+        try {
+            val result = remote.shell(command, returnOnFailure = true)
+
+            if (!result.isSuccess) {
+                logger.error(logMarker, "Failed to collect crash reports. Result stdErr: ${result.stdErr}")
+            }
+
+            return File(crashReportsPath.toUri())
+                .walk()
+                .filter { it.isFile }
+                .map { CrashLog(it.name, String(Files.readAllBytes(it.toPath()))) }
+                .toList()
+
+        } finally {
+            Files.walk(crashReportsPath)
+                .sorted(Comparator.reverseOrder())
+                .map { it.toFile() }
+                .forEach { it.delete() }
+        }
+    }
+
     private fun executeAsync(action: () -> Unit?): Future<*>? {
         val executor = Executors.newSingleThreadExecutor()
         val future =  executor.submit(action)

From 57bb9eb69421dedf5f92bb1d41bb46ca10f11768 Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Fri, 24 Jan 2020 22:21:22 +0000
Subject: [PATCH 109/131] Update OkHttpClient to 4.2.0

---
 device-server/build.gradle                        |  2 +-
 .../automation/deviceserver/ios/WdaClient.kt      | 15 +++++++++------
 .../deviceserver/util/CustomHttpClient.kt         |  8 ++++----
 .../deviceserver/util/CustomHttpClientTest.kt     | 15 +++++++++++++--
 4 files changed, 27 insertions(+), 13 deletions(-)

diff --git a/device-server/build.gradle b/device-server/build.gradle
index f5083937..da8d4110 100644
--- a/device-server/build.gradle
+++ b/device-server/build.gradle
@@ -34,7 +34,7 @@ dependencies {
     compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-io', version: kotlinx_version
     compile group: 'org.jetbrains.kotlinx', name: 'kotlinx-coroutines-jdk8', version: kotlinx_version
 
-    compile group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.9.1'
+    compile group: 'com.squareup.okhttp3', name: 'okhttp', version: '4.2.0'
 
     //region ktor dependencies
     compile("io.ktor:ktor-server-netty:$ktor_version") {
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/WdaClient.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/WdaClient.kt
index bf6402d0..1b5af499 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/WdaClient.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/WdaClient.kt
@@ -1,8 +1,11 @@
 package com.badoo.automation.deviceserver.ios
 
 import com.badoo.automation.deviceserver.JsonMapper
+import com.badoo.automation.deviceserver.util.CustomHttpClient
 import com.fasterxml.jackson.databind.JsonNode
 import okhttp3.*
+import okhttp3.MediaType.Companion.toMediaTypeOrNull
+import okhttp3.RequestBody.Companion.toRequestBody
 import java.net.URL
 import java.time.Duration
 import java.util.concurrent.TimeUnit
@@ -16,7 +19,7 @@ class WdaClient(
 ) {
     class WdaException(message: String): RuntimeException(message)
 
-    private val client: OkHttpClient = OkHttpClient.Builder()
+    private val client: OkHttpClient = CustomHttpClient().client.newBuilder()
         .connectTimeout(openTimeout.toMillis(), TimeUnit.MILLISECONDS)
         .readTimeout(readTimeout.toMillis(), TimeUnit.MILLISECONDS)
         .build()
@@ -69,7 +72,7 @@ class WdaClient(
 
         raiseForHttpStatus(response)
 
-        return JsonMapper().readTree(response.body()!!.byteStream())
+        return JsonMapper().readTree(response.body!!.byteStream())
     }
 
     private fun post(path: String, params: Map): JsonNode {
@@ -78,7 +81,7 @@ class WdaClient(
 
         val request = Request.Builder()
             .addHeader("Content-Type", "application/json")
-            .post(RequestBody.create(mediaType, payload))
+            .post(payload.toRequestBody(mediaType))
             .url(url)
             .build()
 
@@ -86,11 +89,11 @@ class WdaClient(
 
         raiseForHttpStatus(response)
 
-        return JsonMapper().readTree(response.body()!!.byteStream())
+        return JsonMapper().readTree(response.body!!.byteStream())
 
     }
 
-    private val mediaType = MediaType.parse("application/json; charset=utf-8")
+    private val mediaType: MediaType? = "application/json; charset=utf-8".toMediaTypeOrNull()
 
     private fun raiseIfNoSession() {
         if (sessionId == null) {
@@ -102,7 +105,7 @@ class WdaClient(
     private fun raiseForHttpStatus(response: Response) {
         if (response.isSuccessful) return
 
-        throw WdaException("WDA error ${response.code()}: ${response.body()!!.string()}")
+        throw WdaException("WDA error ${response.code}: ${response.body!!.string()}")
     }
 
     private fun raiseForWdStatus(json: JsonNode) {
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClient.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClient.kt
index 748fc2f3..c08ef1ac 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClient.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClient.kt
@@ -6,9 +6,9 @@ import okhttp3.Request
 import java.net.*
 import java.util.concurrent.TimeUnit
 
-class CustomHttpClient {
+class CustomHttpClient(val client: OkHttpClient = defaultHttpClient) {
     companion object {
-        private val client: OkHttpClient = OkHttpClient.Builder()
+        private val defaultHttpClient: OkHttpClient = OkHttpClient.Builder()
                 .connectTimeout(10, TimeUnit.SECONDS)
                 .writeTimeout(10, TimeUnit.SECONDS)
                 .readTimeout(10, TimeUnit.SECONDS)
@@ -24,8 +24,8 @@ class CustomHttpClient {
             val result = client.newCall(request).execute()
 
             return HttpResult(
-                    responseBody = result.body()?.string() ?: "",
-                    httpCode = result.code()
+                    responseBody = result.body?.string() ?: "",
+                    httpCode = result.code
             )
         } catch (e: SocketTimeoutException) {
             return HttpResult(ConnectionTimedOut.code)
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClientTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClientTest.kt
index 34a24d57..1f848488 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClientTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/util/CustomHttpClientTest.kt
@@ -1,19 +1,30 @@
 package com.badoo.automation.deviceserver.util
 
+import com.badoo.automation.deviceserver.mockThis
 import com.badoo.automation.deviceserver.util.HttpCodes.*
+import com.nhaarman.mockito_kotlin.any
+import com.nhaarman.mockito_kotlin.doAnswer
+import com.nhaarman.mockito_kotlin.whenever
+import okhttp3.OkHttpClient
 import org.junit.Assert.assertEquals
 import org.junit.Test
+import java.io.IOException
 import java.net.URL
+import java.net.UnknownHostException
 
 class CustomHttpClientTest {
     @Test fun unknownHost() {
-        val client = CustomHttpClient()
+        val httpClient = mockThis()
+        whenever(httpClient.newCall(any())).doAnswer { throw UnknownHostException() }
+        val client = CustomHttpClient(client = httpClient)
         val result = client.get(URL("http://1922.168.1.6"))
         assertEquals("Wrong code", OriginIsUnreachable.code, result.httpCode)
     }
 
     @Test fun connectionRefused() {
-        val client = CustomHttpClient()
+        val httpClient = mockThis()
+        whenever(httpClient.newCall(any())).doAnswer { throw IOException() }
+        val client = CustomHttpClient(client = httpClient)
         val result = client.get(URL("http://localhost:1"))
         assertEquals("Wrong code", WebServerIsDown.code, result.httpCode)
     }

From 63cc36c143034f2227452ad0db3de865903fa650 Mon Sep 17 00:00:00 2001
From: Sergey Plevako 
Date: Tue, 24 Sep 2019 18:11:59 +0100
Subject: [PATCH 110/131] Add method to list media for iOS Simulator

---
 .../com/badoo/automation/deviceserver/DeviceServer.kt      | 4 ++++
 .../deviceserver/controllers/DevicesController.kt          | 4 ++++
 .../com/badoo/automation/deviceserver/data/MediaDto.kt     | 7 +++++++
 .../com/badoo/automation/deviceserver/host/DevicesNode.kt  | 4 ++++
 .../badoo/automation/deviceserver/host/ISimulatorsNode.kt  | 1 +
 .../badoo/automation/deviceserver/host/SimulatorsNode.kt   | 4 ++++
 .../deviceserver/host/management/DeviceManager.kt          | 4 ++++
 .../automation/deviceserver/ios/simulator/data/Media.kt    | 6 ++++++
 8 files changed, 34 insertions(+)
 create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/MediaDto.kt

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
index 620b93ee..3ad0c7cf 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
@@ -236,6 +236,10 @@ fun Application.module() {
                     }
                 }
                 route("media") {
+                    get {
+                        val ref = param(call, "ref")
+                        call.respond(JsonMapper().toJson(MediaDto(devicesController.listMedia(ref))))
+                    }
                     delete {
                         val ref = param(call, "ref")
                         call.respond(devicesController.resetMedia(ref))
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt
index a45c3cd8..57a4796e 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt
@@ -109,6 +109,10 @@ class DevicesController(private val deviceManager: DeviceManager) {
         return happy
     }
 
+    fun listMedia(ref: DeviceRef): List {
+        return deviceManager.listMedia(ref)
+    }
+
     fun resetMedia(ref: DeviceRef): EmptyMap {
         deviceManager.resetMedia(ref)
 
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/MediaDto.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/MediaDto.kt
new file mode 100644
index 00000000..25612e80
--- /dev/null
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/MediaDto.kt
@@ -0,0 +1,7 @@
+package com.badoo.automation.deviceserver.data
+
+import com.fasterxml.jackson.annotation.JsonProperty
+
+class MediaDto(
+    @JsonProperty("media")
+    val media: List)
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
index ecd11be9..2fed89c8 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
@@ -47,6 +47,10 @@ class DevicesNode(
         throw(NotImplementedError("Resetting media is not supported by physical devices"))
     }
 
+    override fun listMedia(deviceRef: DeviceRef) : List {
+        throw(NotImplementedError("Listing media is not supported by physical devices"))
+    }
+
     override fun addMedia(deviceRef: DeviceRef, fileName: String, data: ByteArray) {
         throw(NotImplementedError("Adding media is not supported by physical devices"))
     }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
index 7ac1e3a0..eba2a301 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
@@ -30,6 +30,7 @@ interface ISimulatorsNode {
 
     fun addMedia(deviceRef: DeviceRef, fileName: String, data: ByteArray)
     fun resetMedia(deviceRef: DeviceRef)
+    fun listMedia(deviceRef: DeviceRef): List
 
     fun getDiagnostic(deviceRef: DeviceRef, type: DiagnosticType, query: DiagnosticQuery): Diagnostic
     fun resetDiagnostic(deviceRef: DeviceRef, type: DiagnosticType)
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
index 1cafa659..41a304b8 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
@@ -107,6 +107,10 @@ class SimulatorsNode(
         getDeviceFor(deviceRef).media.reset()
     }
 
+    override fun listMedia(deviceRef: DeviceRef) : List {
+        return getDeviceFor(deviceRef).media.list()
+    }
+
     override fun addMedia(deviceRef: DeviceRef, fileName: String, data: ByteArray) {
         getDeviceFor(deviceRef).media.addMedia(File(fileName), data)
     }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
index e4f8bf3d..e3a76965 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
@@ -190,6 +190,10 @@ class DeviceManager(
         nodeRegistry.activeDevices.getNodeFor(ref).resetMedia(ref)
     }
 
+    fun listMedia(ref: DeviceRef): List {
+        return nodeRegistry.activeDevices.getNodeFor(ref).listMedia(ref)
+    }
+
     fun addMedia(ref: DeviceRef, fileName: String, data: ByteArray) {
         nodeRegistry.activeDevices.getNodeFor(ref).addMedia(ref, fileName, data)
     }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/Media.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/Media.kt
index 28aa1cb3..a312f9ed 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/Media.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/Media.kt
@@ -1,5 +1,6 @@
 package com.badoo.automation.deviceserver.ios.simulator.data
 
+import com.badoo.automation.deviceserver.command.CommandResult
 import com.badoo.automation.deviceserver.data.UDID
 import com.badoo.automation.deviceserver.host.IRemote
 import com.badoo.automation.deviceserver.util.withDefers
@@ -28,6 +29,11 @@ class Media(
         restartAssetsd()
     }
 
+    fun list() : List {
+        val listCmd = listOf("ls", "-1", "$mediaPath/DCIM/100APPLE")
+        return remote.execIgnoringErrors(listCmd).stdOut.trim().lines()
+    }
+
     fun addMedia(file: File, data: ByteArray) {
         withDefers(logger) {
             val tmpFile = File.createTempFile(file.nameWithoutExtension, ".${file.extension}")

From 2f8b83ecbe711d844520c177dd8cd55d68005e2c Mon Sep 17 00:00:00 2001
From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com>
Date: Fri, 24 Jan 2020 22:29:02 +0000
Subject: [PATCH 111/131] Bump rubyzip from 1.2.2 to 1.3.0 in /client/rb

Bumps [rubyzip](https://github.com/rubyzip/rubyzip) from 1.2.2 to 1.3.0.
- [Release notes](https://github.com/rubyzip/rubyzip/releases)
- [Changelog](https://github.com/rubyzip/rubyzip/blob/master/Changelog.md)
- [Commits](https://github.com/rubyzip/rubyzip/compare/v1.2.2...v1.3.0)

Signed-off-by: dependabot[bot] 
---
 client/rb/Gemfile.lock | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/client/rb/Gemfile.lock b/client/rb/Gemfile.lock
index ea41bcbc..cbcd9988 100644
--- a/client/rb/Gemfile.lock
+++ b/client/rb/Gemfile.lock
@@ -4,7 +4,7 @@ GEM
     childprocess (0.9.0)
       ffi (~> 1.0, >= 1.0.11)
     ffi (1.9.25)
-    rubyzip (1.2.2)
+    rubyzip (1.3.0)
     selenium-webdriver (3.12.0)
       childprocess (~> 0.5)
       rubyzip (~> 1.2)

From 9fc8cd44d0d6537fd8ccc686fc35482fcaeebbbd Mon Sep 17 00:00:00 2001
From: Sergey Plevako 
Date: Wed, 4 Dec 2019 14:53:53 +0000
Subject: [PATCH 112/131] Added locationd service diagnostic

---
 .../ios/simulator/SimulatorPermissions.kt        | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt
index 9358fd27..681d1a52 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorPermissions.kt
@@ -5,6 +5,7 @@ import com.badoo.automation.deviceserver.data.PermissionSet
 import com.badoo.automation.deviceserver.data.PermissionType
 import com.badoo.automation.deviceserver.host.IRemote
 import java.io.File
+import java.nio.file.Paths
 
 class SimulatorPermissions(
     private val remote: IRemote,
@@ -82,6 +83,7 @@ class SimulatorPermissions(
     }
 
     private val appleSimUtils = "/usr/local/bin/applesimutils"
+    private val plUtil = "/usr/bin/plutil"
 
     @Suppress("UNUSED_PARAMETER")
     private fun setNotificationsPermission(bundleId: String, allowed: PermissionAllowed) {
@@ -109,5 +111,19 @@ class SimulatorPermissions(
         if (!rv.isSuccess){
             throw RuntimeException("Could not set location permission: $rv")
         }
+
+        val plistPath = Paths.get(deviceSetPath, simulator.udid, "data", "Library", "Caches", "locationd", "clients.plist").toFile().absolutePath
+        val printCmd = listOf(plUtil, "-p", plistPath)
+        val result = remote.execIgnoringErrors(printCmd)
+
+        when (allowed) {
+            PermissionAllowed.Unset -> if (result.stdOut.contains(bundleId)) {
+                throw RuntimeException("Resetting location permissions did not work. The $bundleId is present in $plistPath")
+            }
+            else -> if (!result.stdOut.contains(bundleId)) {
+                throw RuntimeException("Setting location permissions did not work. The $bundleId is not present in $plistPath")
+            }
+        }
+
     }
 }
\ No newline at end of file

From 4ba6abf1e84478ff03a98ad037924d84f5f83ccd Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Mon, 27 Jan 2020 13:45:31 +0000
Subject: [PATCH 113/131] Capture remote files using scp utility

---
 .../automation/deviceserver/host/IRemote.kt   |  8 ++-
 .../automation/deviceserver/host/Remote.kt    | 57 +++++++++++++++++--
 .../ios/simulator/data/DataContainer.kt       |  9 +--
 .../simulator/data/DataContainerException.kt  |  5 +-
 .../ios/simulator/diagnostic/SystemLog.kt     | 13 ++---
 .../simulator/video/SimulatorVideoRecorder.kt | 11 ++--
 .../ios/simulator/data/DataContainerTest.kt   |  7 +--
 7 files changed, 80 insertions(+), 30 deletions(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt
index caf26f4e..26fb2a36 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt
@@ -3,6 +3,7 @@ package com.badoo.automation.deviceserver.host
 import com.badoo.automation.deviceserver.command.CommandResult
 import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl
 import java.io.File
+import java.time.Duration
 
 interface IRemote {
     companion object {
@@ -33,10 +34,10 @@ interface IRemote {
     fun escape(value: String) : String
 
     /**
-     * Returns [CommandResult] file contents would be in [CommandResult.stdOutBytes]
+     * Returns [ByteArray] file contents
      * //FIXME: should be a better way of streaming a file over HTTP. without caching bytes in server's memory. Investigate ByteReadChannel
      */
-    fun captureFile(file: File): CommandResult
+    fun captureFile(file: File): ByteArray
 
     fun pkill(matchString: String, force: Boolean)
 
@@ -48,4 +49,7 @@ interface IRemote {
     val fbsimctl: FBSimctl
     fun isDirectory(path: String): Boolean
     fun rsync(from: String, to: String, flags: Set)
+    fun scpToRemoteHost(from: String, to: String, timeOut: Duration)
+    fun rm(path: String, timeOut: Duration)
+    fun scpFromRemoteHost(from: String, to: String, timeOut: Duration)
 }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt
index ea274300..4b1ff7a0 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt
@@ -9,6 +9,7 @@ import com.badoo.automation.deviceserver.util.ensure
 import net.logstash.logback.marker.MapEntriesAppendingMarker
 import org.slf4j.LoggerFactory
 import java.io.File
+import java.io.FileNotFoundException
 import java.time.Duration
 
 class Remote(
@@ -69,12 +70,21 @@ class Remote(
     }
 
     //FIXME: should be a better way of streaming a file over HTTP. without caching bytes in server's memory. Investigating ByteReadChannel
-    override fun captureFile(file: File): CommandResult {
-        return remoteExecutor.exec(
-            listOf("cat", file.absolutePath),
-            returnFailure = true,
-            processListener = ShellCommandListener(INITIAL_BUFFER_SIZE) //FIXME: probably better to send just size, not the listener
-        )
+    override fun captureFile(file: File): ByteArray {
+        if (isLocalhost()) {
+            if (!file.exists()) {
+                throw FileNotFoundException("File $file is not found.")
+            }
+            return file.readBytes()
+        }
+
+        val tempFile = File.createTempFile("remoteFile", ".bin")
+        try {
+            scpFromRemoteHost(file.absolutePath, tempFile.absolutePath, Duration.ofMinutes(2));
+            return tempFile.readBytes()
+        } finally {
+            tempFile.delete()
+        }
     }
 
     private enum class Signal(val signal: Int) {
@@ -120,6 +130,41 @@ class Remote(
         }
     }
 
+    override fun scpToRemoteHost(from: String, to: String, timeOut: Duration) {
+        val result = localExecutor.exec(listOf("/usr/bin/scp", "-r", from, "$userAtHost:$to"), timeOut = timeOut, returnFailure = true)
+
+        ensure(result.isSuccess) {
+            val message = "Copying files to remote host failed with ${result.stdErr}"
+            logger.error(logMarker, message)
+            RuntimeException(message)
+        }
+    }
+
+    override fun scpFromRemoteHost(from: String, to: String, timeOut: Duration) {
+        val result = localExecutor.exec(listOf("/usr/bin/scp", "-r", "$userAtHost:$from", to), timeOut = timeOut, returnFailure = true)
+
+        if (!result.isSuccess) {
+            val message = "Copying files from remote host failed with ${result.stdErr}"
+            logger.error(logMarker, message)
+
+            throw if (result.stdErr.contains("No such file or directory")) {
+                FileNotFoundException(message)
+            } else {
+                RuntimeException(message)
+            }
+        }
+    }
+
+    override fun rm(path: String, timeOut: Duration) {
+        val result = remoteExecutor.exec(listOf("/bin/rm", "-rf", path), timeOut = timeOut, returnFailure = true)
+
+        ensure(result.isSuccess) {
+            val message = "Failed to delete remote files. Stderr: ${result.stdErr}"
+            logger.error(logMarker, message)
+            RuntimeException(message)
+        }
+    }
+
     private fun environmentForRsync(): MutableMap {
         val env = mutableMapOf()
         val sshAuthSocket = System.getenv(SSH_AUTH_SOCK)
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainer.kt
index 37882754..11b30397 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainer.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainer.kt
@@ -3,6 +3,7 @@ package com.badoo.automation.deviceserver.ios.simulator.data
 import com.badoo.automation.deviceserver.command.ShellUtils
 import com.badoo.automation.deviceserver.host.IRemote
 import java.io.File
+import java.lang.RuntimeException
 import java.nio.file.Path
 
 class DataContainer(
@@ -25,11 +26,11 @@ class DataContainer(
     fun readFile(path: Path): ByteArray {
         val expandedPath = sshNoEscapingWorkaround(expandPath(path).toString())
 
-        val result = remote.captureFile(File(expandedPath))
-        if (!result.isSuccess) {
-            throw DataContainerException("Could not read file $path for $bundleId: $result")
+        try {
+            return remote.captureFile(File(expandedPath))
+        } catch (e: RuntimeException) {
+            throw DataContainerException("Could not read file $path for $bundleId", e)
         }
-        return result.stdOutBytes
     }
 
     private fun sshNoEscapingWorkaround(path: String): String {
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainerException.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainerException.kt
index a7440327..3a729867 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainerException.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainerException.kt
@@ -1,3 +1,6 @@
 package com.badoo.automation.deviceserver.ios.simulator.data
 
-class DataContainerException(message: String): RuntimeException(message)
+class DataContainerException: RuntimeException {
+    constructor(message: String) : super(message)
+    constructor(message: String, cause: Throwable) : super(message, cause)
+}
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/SystemLog.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/SystemLog.kt
index 082d2af9..71b6c868 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/SystemLog.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/diagnostic/SystemLog.kt
@@ -8,6 +8,7 @@ import net.logstash.logback.marker.MapEntriesAppendingMarker
 import org.slf4j.LoggerFactory
 import org.slf4j.Marker
 import java.io.File
+import java.nio.charset.StandardCharsets
 
 class SystemLog(
     private val remote: IRemote,
@@ -38,14 +39,12 @@ class SystemLog(
         val path = remote.fbsimctl.diagnose(udid).sysLogLocation
                 ?: throw RuntimeException("Could not determine System Log path")
 
-        val result = remote.captureFile(File(path))
-
-        if (!result.isSuccess) {
-            val message = "Could not read System Log: $result"
+        try {
+            return String(remote.captureFile(File(path)), StandardCharsets.UTF_8)
+        } catch (e: RuntimeException) {
+            val message = "Could not read System Log. Cause: ${e.message}"
             logger.error(logMarker, message)
-            throw RuntimeException(message)
+            throw RuntimeException(message, e)
         }
-
-        return result.stdOut
     }
 }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt
index cf758e11..c88b77be 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt
@@ -116,14 +116,15 @@ class SimulatorVideoRecorder(
         // TODO: is there a better way to read binary file over ssh without rsyncing?
         // We should get rid of ssh and move to having 1 http server per 1 host and some proxy node to tie them together
         // once we have proper deployment solution for our macOS machines
-        val result = remote.captureFile(videoFile)
-        if (!result.isSuccess) {
-            val message = "Could not read video file. Result stdErr: ${result.stdErr}"
+        try {
+            val bytes = remote.captureFile(videoFile)
+            logger.info(logMarker, "Received video recording. Size ${bytes.size} bytes")
+            return bytes
+        } catch (e: RuntimeException) {
+            val message = "Could not read video file. Cause: ${e.message}"
             logger.error(message)
             throw SimulatorVideoRecordingException(message)
         }
-        logger.info(logMarker, "Received video recording. Size ${result.stdOutBytes.size} bytes")
-        return result.stdOutBytes
     }
 
     fun dispose() {
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainerTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainerTest.kt
index 6b69bb74..25e2d78e 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainerTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainerTest.kt
@@ -8,6 +8,7 @@ import com.nhaarman.mockito_kotlin.whenever
 import org.junit.Before
 import org.junit.Test
 import org.mockito.Mockito
+import java.io.File
 import java.nio.file.Paths
 import kotlin.test.assertEquals
 import kotlin.test.assertFails
@@ -78,11 +79,7 @@ class DataContainerTest {
     @Test
     fun shouldReadFileAsByteArray() {
         val expected = "123".toByteArray()
-        val cmdResult = CommandResult("", "", expected, 0)
-
-        whenever(remote.captureFile(Paths.get(containerPathStub, "Library/Caches/file.txt").toFile())).thenReturn(
-            cmdResult
-        )
+        whenever(remote.captureFile(File(containerPathStub, "Library/Caches/file.txt"))).thenReturn(expected)
 
         val container = DataContainer(
             remote = remote,

From d34c7d68bb5c9622516e7345dda30fecbb34c15f Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Mon, 27 Jan 2020 16:28:33 +0000
Subject: [PATCH 114/131] Cancel async task when current thread is interrupted

---
 .../kotlin/com/badoo/automation/deviceserver/util/Support.kt   | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/Support.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/Support.kt
index 093d1fbe..6ba20f07 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/Support.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/Support.kt
@@ -19,6 +19,9 @@ fun  executeWithTimeout(timeout: Duration, name: String, action: () -> T): T
 
     try {
         return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS)
+    } catch (e: InterruptedException) {
+        future.cancel(true)
+        throw e
     } catch (e: TimeoutException) {
         future.cancel(true)
         throw TimeoutException("$name timed out after ${timeout.seconds} seconds")

From a37f7b5c35b844501e66444f007a78458e79bad9 Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Mon, 27 Jan 2020 18:05:07 +0000
Subject: [PATCH 115/131] Force pseudo-terminal allocation for commands
 executed over SSH

---
 .../deviceserver/command/ChildProcess.kt      |  4 +--
 .../command/RemoteShellCommand.kt             |  8 +-----
 .../automation/deviceserver/host/Remote.kt    |  6 ++---
 .../ios/device/DeviceFbsimctlProc.kt          |  1 -
 .../deviceserver/ios/device/UsbProxy.kt       |  3 ---
 .../deviceserver/ios/proc/FbsimctlProc.kt     |  2 --
 .../deviceserver/ios/proc/WebDriverAgent.kt   |  2 --
 .../simulator/video/SimulatorVideoRecorder.kt |  4 +--
 .../command/RemoteShellCommandTest.kt         | 26 ++-----------------
 .../ios/proc/DeviceFbsimctlProcTest.kt        |  1 -
 .../deviceserver/ios/proc/FbsimctlProcTest.kt |  1 -
 11 files changed, 10 insertions(+), 48 deletions(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ChildProcess.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ChildProcess.kt
index 31f6eecd..25bac276 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ChildProcess.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ChildProcess.kt
@@ -41,11 +41,11 @@ class ChildProcess private constructor(
 
     companion object {
         fun fromCommand(
-            remoteHost: String, userName: String, cmd: List, isInteractiveShell: Boolean,
+            remoteHost: String, userName: String, cmd: List,
             out_reader: ((line: String) -> Unit)?,
             err_reader: ((line: String) -> Unit)?
         ): ChildProcess {
-            val executor = Remote.getRemoteCommandExecutor(hostName = remoteHost, userName = userName, isInteractiveShell = isInteractiveShell)
+            val executor = Remote.getRemoteCommandExecutor(hostName = remoteHost, userName = userName)
             return ChildProcess(cmd, executor, remoteHost, LongRunningProcessListener(out_reader, err_reader))
         }
     }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt
index 63bf457d..42b69f4d 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt
@@ -13,7 +13,6 @@ class RemoteShellCommand(
     userName: String,
     builderFactory: (cmd: List, env: Map) -> NuProcessBuilder = ::defaultNuProcessBuilder,
     commonEnvironment: Map = mapOf(),
-    isInteractiveShell: Boolean = false,
     isVerboseMode: Boolean = false,
     connectionTimeout: Int = 10
 ) : ShellCommand(builderFactory, commonEnvironment) {
@@ -31,11 +30,7 @@ class RemoteShellCommand(
                 QUIET_MODE
         ))
 
-        if (isInteractiveShell) {
-            sshPrefix.addAll(FORCE_PSEUDO_TERMINAL_ALLOCATION)
-        } else {
-            sshPrefix.add(NO_PSEUDO_TERMINAL_ALLOCATION)
-        }
+        sshPrefix.addAll(FORCE_PSEUDO_TERMINAL_ALLOCATION)
 
         if (isVerboseMode) {
             sshPrefix.add("-vvv")
@@ -107,7 +102,6 @@ class RemoteShellCommand(
         const val SSH_COMMAND = "/usr/bin/ssh"
         const val QUIET_MODE = "-q"
         val FORCE_PSEUDO_TERMINAL_ALLOCATION = listOf("-t", "-t")
-        const val NO_PSEUDO_TERMINAL_ALLOCATION = "-T"
         const val SSH_AUTH_SOCK = "SSH_AUTH_SOCK"
         const val SSH_ERROR = 255
     }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt
index 4b1ff7a0..958ef51a 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt
@@ -24,11 +24,11 @@ class Remote(
         const val SSH_AUTH_SOCK = "SSH_AUTH_SOCK"
         private const val INITIAL_BUFFER_SIZE = 10 * 1024 * 1024 //FIXME: looks arbitrary. taken as an average of video file sizes
 
-        fun getRemoteCommandExecutor(hostName: String, userName: String, isInteractiveShell: Boolean = false): IShellCommand {
+        fun getRemoteCommandExecutor(hostName: String, userName: String): IShellCommand {
             return if (isLocalhost(hostName, userName)) {
                 ShellCommand(commonEnvironment = mapOf("HOME" to System.getProperty("user.home")))
             } else {
-                RemoteShellCommand(hostName, userName, isInteractiveShell = isInteractiveShell)
+                RemoteShellCommand(hostName, userName)
             }
         }
     }
@@ -80,7 +80,7 @@ class Remote(
 
         val tempFile = File.createTempFile("remoteFile", ".bin")
         try {
-            scpFromRemoteHost(file.absolutePath, tempFile.absolutePath, Duration.ofMinutes(2));
+            scpFromRemoteHost(file.absolutePath, tempFile.absolutePath, Duration.ofMinutes(2))
             return tempFile.readBytes()
         } finally {
             tempFile.delete()
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceFbsimctlProc.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceFbsimctlProc.kt
index ce137ac9..c3635809 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceFbsimctlProc.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceFbsimctlProc.kt
@@ -15,7 +15,6 @@ class DeviceFbsimctlProc(
         remoteHost: String,
         username: String,
         cmd: List,
-        isInteractiveShell: Boolean,
         out_reader: (line: String) -> Unit,
         err_reader: (line: String) -> Unit
     ) -> ChildProcess = ChildProcess.Companion::fromCommand
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxy.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxy.kt
index d9887c45..8b0ab59d 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxy.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxy.kt
@@ -17,7 +17,6 @@ class UsbProxy(
         remoteHost: String,
         userName: String,
         cmd: List,
-        isInteractiveShell: Boolean,
         out_reader: (line: String) -> Unit,
         err_reader: (line: String) -> Unit
     ) -> ChildProcess = ChildProcess.Companion::fromCommand
@@ -40,7 +39,6 @@ class UsbProxy(
             remote.hostName,
             remote.userName,
             listOf(IPROXY_BIN, localPort.toString(), devicePort.toString(), udid),
-            false,
             { message -> logger.trace(logMarker, "${this}: iproxy : ${message.trim()}") },
             { message -> logger.debug(logMarker, "${this}: iproxy : ${message.trim()}") }
         )
@@ -49,7 +47,6 @@ class UsbProxy(
             remote.hostName,
             remote.userName,
             listOf(SOCAT_BIN, "tcp-listen:$localPort,reuseaddr,fork", "tcp:0.0.0.0:$localPort"),
-            false,
             { message -> logger.trace(logMarker, "${this}: socat : ${message.trim()}") },
             { message -> logger.debug(logMarker, "${this}: socat : ${message.trim()}") }
         )
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt
index 95e8ff87..6afcab76 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt
@@ -16,7 +16,6 @@ open class FbsimctlProc(
         remoteHost: String,
         username: String,
         cmd: List,
-        isInteractiveShell: Boolean,
         out_reader: (line: String) -> Unit,
         err_reader: (line: String) -> Unit
     ) -> ChildProcess = ChildProcess.Companion::fromCommand
@@ -33,7 +32,6 @@ open class FbsimctlProc(
                 remote.hostName,
                 remote.userName,
                 getFbsimctlCommand(headless),
-                true,
                 { logger.trace(logMarker, "${this@FbsimctlProc}: FbSimCtl : ${it.trim()}") },
                 { logger.debug(logMarker, "${this@FbsimctlProc}: FbSimCtl : ${it.trim()}") }
         )
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt
index 1f0ba91c..f0cf9189 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt
@@ -20,7 +20,6 @@ open class WebDriverAgent(
                 remoteHost: String,
                 userName: String,
                 cmd: List,
-                isInteractiveShell: Boolean,
                 out_reader: ((line: String) -> Unit)?,
                 err_reader: ((line: String) -> Unit)?
         ) -> ChildProcess = ChildProcess.Companion::fromCommand
@@ -51,7 +50,6 @@ open class WebDriverAgent(
                 remote.hostName,
                 remote.userName,
                 launchXctestCommand,
-                false,
                 null,
                 { message -> logger.debug(logMarker, "${this@WebDriverAgent}: WDA : ${message.trim()}") }
         )
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt
index c88b77be..596337ff 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt
@@ -15,7 +15,7 @@ import kotlin.concurrent.withLock
 class SimulatorVideoRecorder(
     private val deviceInfo: DeviceInfo,
     private val remote: IRemote,
-    private val childFactory: (remoteHost: String, username: String, cmd: List, isInteractiveShell: Boolean,
+    private val childFactory: (remoteHost: String, username: String, cmd: List,
                                    out_reader: (line: String) -> Unit, err_reader: (line: String) -> Unit
         ) -> ChildProcess? = ChildProcess.Companion::fromCommand,
     private val recorderStopTimeout: Duration = RECORDER_STOP_TIMEOUT,
@@ -68,7 +68,7 @@ class SimulatorVideoRecorder(
 
             val cmd = shell(videoRecordingCmd(fps = 5, frameWidth = frameWidth, frameHeight = frameHeight))
 
-            childProcess = childFactory(remote.hostName, remote.userName, cmd, true,
+            childProcess = childFactory(remote.hostName, remote.userName, cmd,
                 { logger.debug(logMarker, "$udid: VideoRecorder : ${it.trim()}") },
                 { logger.debug(logMarker, "$udid: VideoRecorder : ${it.trim()}") }
             )
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommandTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommandTest.kt
index 5155164f..9f93496e 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommandTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommandTest.kt
@@ -42,34 +42,11 @@ class RemoteShellCommandTest {
         System.setOut(testOut)
     }
 
-    @Test fun nonInteractiveSshCommand() {
-        remoteShell = RemoteShellCommand(
-                remoteHost = remoteHost,
-                userName = userName,
-                builderFactory = ::nuProcessBuilderForTesting,
-                connectionTimeout = 1
-        )
-        remoteShell.exec(listOf("fbsimctl"), timeOut = Duration.ofMillis(100))
-
-        val expectedCommand = listOf(
-                "/usr/bin/ssh",
-                "-o", "ConnectTimeout=1",
-                "-o", "PreferredAuthentications=publickey",
-                "-q",
-                "-T",
-                userAtHost,
-            "fbsimctl"
-        )
-
-        assertEquals(expectedCommand, spyProcessBuilder.command())
-    }
-
     @Test fun interactiveSshCommand() {
         remoteShell = RemoteShellCommand(
                 remoteHost = remoteHost,
                 userName = userName,
                 builderFactory = ::nuProcessBuilderForTesting,
-                isInteractiveShell = true,
                 connectionTimeout = 1
                 )
         remoteShell.exec(listOf("fbsimctl"), timeOut = Duration.ofMillis(100))
@@ -101,7 +78,8 @@ class RemoteShellCommandTest {
                 "-o", "ConnectTimeout=1",
                 "-o", "PreferredAuthentications=publickey",
                 "-q",
-                "-T",
+                "-t",
+                "-t",
                 userAtHost,
                 "fbsimctl",
                 "udid='UDID'",
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/DeviceFbsimctlProcTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/DeviceFbsimctlProcTest.kt
index 64d5477d..cac58b01 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/DeviceFbsimctlProcTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/DeviceFbsimctlProcTest.kt
@@ -55,7 +55,6 @@ class DeviceFbsimctlProcTest {
         remoteHost: String,
         username: String,
         cmd: List,
-        isInteractiveShell: Boolean,
         out_reader: (line: String) -> Unit,
         err_reader: (line: String) -> Unit
     ): ChildProcess {
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProcTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProcTest.kt
index ecd8b6ec..ebc724d6 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProcTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProcTest.kt
@@ -65,7 +65,6 @@ class FbsimctlProcTest {
         remoteHost: String,
         username: String,
         cmd: List,
-        isInteractiveShell: Boolean,
         out_reader: (line: String) -> Unit,
         err_reader: (line: String) -> Unit
     ): ChildProcess {

From 03a792c7be3f61779467ee4b98331d12b05e7567 Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Fri, 31 Jan 2020 18:07:58 +0000
Subject: [PATCH 116/131] Use ProcessBuilder instead of NuProcess

---
 .../deviceserver/command/ChildProcess.kt      |  92 ++++++++++--
 .../deviceserver/command/CommandResult.kt     |   4 +-
 .../deviceserver/command/IShellCommand.kt     |   7 +-
 .../command/IShellCommandListener.kt          |  10 --
 .../command/RemoteShellCommand.kt             |  17 +--
 .../deviceserver/command/ShellCommand.kt      | 135 ++++++++++++------
 .../command/ShellCommandListener.kt           |  60 --------
 .../automation/deviceserver/host/Remote.kt    |   3 +-
 .../ios/device/DeviceFbsimctlProc.kt          |   5 +-
 .../deviceserver/ios/device/UsbProxy.kt       |   3 +
 .../deviceserver/ios/proc/FbsimctlProc.kt     |   6 +-
 .../deviceserver/ios/proc/WebDriverAgent.kt   |   2 +
 .../ios/simulator/backup/SimulatorBackup.kt   |   2 +-
 .../simulator/video/SimulatorVideoRecorder.kt |   6 +-
 .../deviceserver/command/CommandResultTest.kt |   4 +-
 .../command/RemoteShellCommandTest.kt         |  67 +++------
 .../deviceserver/command/ShellCommandTest.kt  |  16 ++-
 .../deviceserver/host/RemoteTest.kt           |   2 +-
 .../deviceserver/ios/fbsimctl/FBSimctlTest.kt |   4 +-
 .../ios/proc/DeviceFbsimctlProcTest.kt        |   5 +-
 .../deviceserver/ios/proc/FbsimctlProcTest.kt |   5 +-
 .../ios/simulator/SimulatorBackupTest.kt      |  21 ++-
 .../ios/simulator/SimulatorProcessTest.kt     |   8 +-
 .../ios/simulator/data/DataContainerTest.kt   |   4 +-
 24 files changed, 262 insertions(+), 226 deletions(-)
 delete mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/IShellCommandListener.kt
 delete mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommandListener.kt

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ChildProcess.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ChildProcess.kt
index 25bac276..e65daab5 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ChildProcess.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ChildProcess.kt
@@ -2,52 +2,114 @@ package com.badoo.automation.deviceserver.command
 
 import com.badoo.automation.deviceserver.LogMarkers
 import com.badoo.automation.deviceserver.host.Remote
-import com.badoo.automation.deviceserver.ios.proc.LongRunningProcessListener
 import net.logstash.logback.marker.MapEntriesAppendingMarker
 import org.slf4j.LoggerFactory
-import java.time.Duration
+import java.io.InputStream
+import java.io.InputStreamReader
+import java.nio.charset.StandardCharsets
+import java.util.concurrent.Executors
+import java.util.concurrent.Future
+import java.util.concurrent.TimeUnit
 
 class ChildProcess private constructor(
     command: List,
     executor: IShellCommand,
     remoteHostname: String,
-    private val processListener: LongRunningProcessListener
+    commandEnvironment: Map = mapOf(),
+    outWriter: ((line: String) -> Unit)?,
+    errWriter: ((line: String) -> Unit)?
 ) {
     private val logger = LoggerFactory.getLogger(javaClass.simpleName)
     private val logMarker = MapEntriesAppendingMarker(mapOf(LogMarkers.HOSTNAME to remoteHostname))
+    private val poolExecutor = Executors.newFixedThreadPool(2)
+    private val process: Process
+    private val stdOutTask: Future<*>
+    private val stdErrTask: Future<*>
 
     init {
         logger.debug(logMarker, "Starting long living process from command [$command]")
-        executor.startProcess(command, mapOf(), processListener = processListener)
+        process = executor.startProcess(command, commandEnvironment)
+        stdOutTask = poolExecutor.submit(streamReader(process.inputStream, outWriter))
+        stdErrTask = poolExecutor.submit(streamReader(process.errorStream, errWriter))
         logger.debug(logMarker, "Started long living process $this from command [$command]")
     }
 
-    override fun toString(): String = "< PID: ${processListener.pid}>"
+    override fun toString(): String = "< PID: ${process.pid()}>"
 
-    fun isAlive(): Boolean = processListener.isAlive
+    fun isAlive(): Boolean = process.isAlive
 
-    fun kill(timeOut: Duration = Duration.ofSeconds(2)) {
+    fun kill() {
         logger.debug(logMarker, "Sending SIGTERM to process $this")
-        val result = processListener.destroy(false, timeOut)
-        if (result == Int.MIN_VALUE) {
-            logger.warn(logMarker, "Process $this did not terminate gracefully within [${timeOut.seconds}] seconds. Sending SIGKILL")
-            processListener.destroy(true, timeOut)
+        process.destroy()
+
+        val exited = process.waitFor(PROCESS_TIMEOUT, TimeUnit.SECONDS)
+
+        if (!exited) {
+            logger.warn(logMarker, "Process $this did not terminate gracefully within $PROCESS_TIMEOUT seconds. Sending SIGKILL")
+            process.destroyForcibly()
+        }
+    }
+
+    private fun streamReader(inputStream: InputStream, writer: ((line: String) -> Unit)?): Runnable {
+        return Runnable { inputStream.use { readStream(it, writer) } }
+    }
+
+    private fun readStream(stream: InputStream, writer: ((line: String) -> Unit)?) {
+        val reader = InputStreamReader(stream, StandardCharsets.UTF_8)
+        val stringBuilder = StringBuilder(BUFFER_SIZE)
+
+        while (true) {
+            val bytes = reader.read()
+
+            if (bytes == EOF) {
+                writeString(writer, stringBuilder.toString())
+                break
+            }
+
+            val char = bytes.toChar()
+
+            if (char == NEWLINE) {
+                writeString(writer, stringBuilder.toString())
+                stringBuilder.clear()
+            } else {
+                stringBuilder.append(char)
+            }
         }
     }
 
-    fun writeStdin(string: String) {
-        processListener.writeStdin(string)
+    private fun writeString(writer: ((line: String) -> Unit)?, string: String) {
+        if (writer == null || string.isEmpty()) {
+            return
+        }
+
+        writer(string)
     }
 
     companion object {
         fun fromCommand(
-            remoteHost: String, userName: String, cmd: List,
+            remoteHost: String,
+            userName: String,
+            cmd: List,
+            commandEnvironment: Map,
             out_reader: ((line: String) -> Unit)?,
             err_reader: ((line: String) -> Unit)?
         ): ChildProcess {
             val executor = Remote.getRemoteCommandExecutor(hostName = remoteHost, userName = userName)
-            return ChildProcess(cmd, executor, remoteHost, LongRunningProcessListener(out_reader, err_reader))
+            return ChildProcess(
+                command = cmd,
+                commandEnvironment = commandEnvironment,
+                executor = executor,
+                remoteHostname = remoteHost,
+                outWriter = out_reader,
+                errWriter = err_reader
+            )
         }
+
+        private const val EOF = -1
+        private const val NEWLINE = '\n'
+        private const val PROCESS_TIMEOUT = 15L
+        private const val BUFFER_SIZE = 65536 // seems arbitrary, but using buffer with this size fastest (jmh) on multiple small commands
     }
 }
 
+private fun StringBuilder.clear() = setLength(0)
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/CommandResult.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/CommandResult.kt
index 62c55df9..6b52d32f 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/CommandResult.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/CommandResult.kt
@@ -8,10 +8,10 @@ data class CommandResult(
     // FIXME: Separate binary and string results for "capture" and "process open" executors
     val stdOut: String,
     val stdErr: String,
-    @Suppress("ArrayInDataClass") val stdOutBytes: ByteArray,
     val exitCode: Int,
     val isSuccess: Boolean = exitCode == 0,
-    val cmd: List = listOf()
+    val cmd: List = listOf(),
+    val pid: Long
 ) {
     override fun toString(): String {
         val sb = StringBuilder(javaClass.simpleName)
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/IShellCommand.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/IShellCommand.kt
index 43d1785e..827fafe9 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/IShellCommand.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/IShellCommand.kt
@@ -1,6 +1,5 @@
 package com.badoo.automation.deviceserver.command
 
-import com.badoo.automation.deviceserver.ios.proc.LongRunningProcessListener
 import org.slf4j.Marker
 import java.time.Duration
 
@@ -21,15 +20,15 @@ interface IShellCommand {
         timeOut: Duration = Duration.ofSeconds(60),
         returnFailure: Boolean = true,
         logMarker: Marker? = null,
-        processListener: IShellCommandListener = ShellCommandListener()
+        processBuilder: ProcessBuilder = ProcessBuilder(listOf())
     ): CommandResult
 
     fun startProcess(
         command: List,
         environment: Map,
         logMarker: Marker? = null,
-        processListener: LongRunningProcessListener
-    )
+        processBuilder: ProcessBuilder = ProcessBuilder(listOf())
+    ): Process
 
     /**
      * Escape exec argument string if needed
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/IShellCommandListener.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/IShellCommandListener.kt
deleted file mode 100644
index c900ae4f..00000000
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/IShellCommandListener.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package com.badoo.automation.deviceserver.command
-
-import com.zaxxer.nuprocess.NuProcessHandler
-
-interface IShellCommandListener : NuProcessHandler {
-    val stdOut: String
-    val stdErr: String
-    val bytes: ByteArray // binary data. for ex.: video recording
-    val exitCode: Int
-}
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt
index 42b69f4d..7c226a50 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommand.kt
@@ -1,8 +1,6 @@
 package com.badoo.automation.deviceserver.command
 
 import com.badoo.automation.deviceserver.LogMarkers
-import com.badoo.automation.deviceserver.ios.proc.LongRunningProcessListener
-import com.zaxxer.nuprocess.NuProcessBuilder
 import net.logstash.logback.marker.MapEntriesAppendingMarker
 import org.slf4j.Marker
 import java.time.Duration
@@ -11,11 +9,10 @@ import java.util.concurrent.TimeUnit
 class RemoteShellCommand(
     private val remoteHost: String,
     userName: String,
-    builderFactory: (cmd: List, env: Map) -> NuProcessBuilder = ::defaultNuProcessBuilder,
     commonEnvironment: Map = mapOf(),
     isVerboseMode: Boolean = false,
     connectionTimeout: Int = 10
-) : ShellCommand(builderFactory, commonEnvironment) {
+) : ShellCommand(commonEnvironment) {
     private val userAtHost: String = if (userName.isBlank()) { remoteHost } else { "$userName@$remoteHost" }
     override val logMarker: Marker get() = MapEntriesAppendingMarker(mapOf(LogMarkers.HOSTNAME to remoteHost))
     private val sshEnv: Map
@@ -52,10 +49,10 @@ class RemoteShellCommand(
 
     override fun exec(command: List, environment: Map, timeOut: Duration,
                       returnFailure: Boolean, logMarker: Marker?,
-                      processListener: IShellCommandListener): CommandResult {
+                      processBuilder: ProcessBuilder): CommandResult {
         val cmd = getCommandWithSSHPrefix(command)
         val startTime = System.nanoTime()
-        val result = super.exec(cmd, getEnvironmentForSSH(), timeOut, returnFailure, logMarker, processListener)
+        val result = super.exec(cmd, getEnvironmentForSSH(), timeOut, returnFailure, logMarker, processBuilder)
         val elapsedTime = System.nanoTime() - startTime
         val elapsedMillis = TimeUnit.NANOSECONDS.toMillis(elapsedTime)
         val marker = MapEntriesAppendingMarker(
@@ -64,8 +61,8 @@ class RemoteShellCommand(
                 LogMarkers.SSH_PROFILING_MS to elapsedMillis
             )
         )
-        logger.debug(
-            marker, "Execution of SSH command took $elapsedMillis ms. Command: $cmd")
+
+        logger.debug(marker, "Execution of SSH command took $elapsedMillis ms. Command: ${cmd.joinToString(" ")}, PID: ${result.pid}")
 
         if (result.exitCode == SSH_ERROR) {
             // FIXME: Check stdout and stderr, if they are empty – ssh timeout, otherwise, it is likely to be command error
@@ -78,8 +75,8 @@ class RemoteShellCommand(
     }
 
     override fun startProcess(command: List, environment: Map, logMarker: Marker?,
-                              processListener: LongRunningProcessListener) {
-        super.startProcess(getCommandWithSSHPrefix(command), getEnvironmentForSSH(), logMarker, processListener)
+                              processBuilder: ProcessBuilder): Process {
+        return super.startProcess(getCommandWithSSHPrefix(command), getEnvironmentForSSH(), logMarker, processBuilder)
     }
 
     override fun escape(value: String): String {
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommand.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommand.kt
index a7307a9a..88e6a6e2 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommand.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommand.kt
@@ -1,77 +1,132 @@
 package com.badoo.automation.deviceserver.command
 
 import com.badoo.automation.deviceserver.LogMarkers
-import com.badoo.automation.deviceserver.ios.proc.LongRunningProcessListener
 import com.badoo.automation.deviceserver.util.ensure
-import com.zaxxer.nuprocess.NuProcess
-import com.zaxxer.nuprocess.NuProcessBuilder
-import com.zaxxer.nuprocess.NuProcessHandler
 import net.logstash.logback.marker.MapEntriesAppendingMarker
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
 import org.slf4j.Marker
+import java.io.IOException
+import java.io.InputStream
+import java.io.InputStreamReader
+import java.nio.charset.StandardCharsets
 import java.time.Duration
+import java.util.concurrent.Callable
+import java.util.concurrent.Executors
 import java.util.concurrent.TimeUnit
 
 open class ShellCommand(
-        private val builderFactory: (cmd: List, env: Map) -> NuProcessBuilder = ::defaultNuProcessBuilder, //for testing
-        private val commonEnvironment: Map = mapOf()
-    ) : IShellCommand {
+    private val commonEnvironment: Map
+) : IShellCommand {
     protected val logger: Logger = LoggerFactory.getLogger(javaClass.simpleName)
     protected open val logMarker: Marker get() = MapEntriesAppendingMarker(mapOf(LogMarkers.HOSTNAME to "localhost"))
-
-    companion object {
-        fun defaultNuProcessBuilder(cmd: List, env: Map): NuProcessBuilder = NuProcessBuilder(cmd, env)
-    }
+    private val executor = Executors.newCachedThreadPool()
 
     override fun exec(command: List, environment: Map, timeOut: Duration,
-                      returnFailure: Boolean, logMarker: Marker?, processListener: IShellCommandListener): CommandResult {
-        val process: NuProcess = startProcessInternal(command, environment, processListener)
+                      returnFailure: Boolean, logMarker: Marker?, processBuilder: ProcessBuilder): CommandResult {
+        processBuilder.command(command)
+        setEnvironment(processBuilder, environment)
 
-        var exitCode = Int.MIN_VALUE
+        val process: Process = processBuilder.start()
+        val stdOut = executor.submit(streamReader(process.inputStream))
+        val stdErr = executor.submit(streamReader(process.errorStream))
+        val pid = process.pid()
 
-        try {
-            exitCode = process.waitFor(timeOut.toMillis(), TimeUnit.MILLISECONDS)
-        } catch (e: InterruptedException) {
-            logger.warn(logMarker, "Error while running command: ${command.joinToString(" ")}", e)
-        }
+        val commandString = command.joinToString(" ")
+        logger.debug(MapEntriesAppendingMarker(mapOf("PID" to pid)), "Executing command: $commandString, PID: $pid")
 
-        if (exitCode == Int.MIN_VALUE) { // waiting timed out
-            try {
-                process.destroy(true)
-            } catch (e: RuntimeException) {
-                logger.warn(logMarker, "Error while terminating command: ${command.joinToString(" ")}", e)
-            }
+        val hasExited = process.waitFor(timeOut.toMillis(), TimeUnit.MILLISECONDS)
+        val exitCode = if (hasExited) process.exitValue() else Int.MIN_VALUE
+
+        if (!hasExited) {
+            logger.error(logMarker, "Command has failed to complete in time. Command: $commandString, PID: $pid")
+            executor.submit { waitForProcessToComplete(process, logMarker, commandString, pid.toInt(), timeOut) }
         }
 
         val result = CommandResult(
-                stdOut = processListener.stdOut,
-                stdErr = processListener.stdErr,
-                stdOutBytes = processListener.bytes,
-                exitCode = processListener.exitCode,
-                cmd = command // Store actual command - including ssh stuff.
+            stdOut = stdOut.get(),
+            stdErr = stdErr.get(),
+            exitCode = exitCode,
+            cmd = command,
+            pid = pid
         )
-        ensure(processListener.exitCode == 0 || returnFailure) {
-            val errorMessage = "Error while running command: ${command.joinToString(" ")} Result=$result"
+
+        ensure(exitCode == 0 || returnFailure) {
+            val errorMessage = "Error while running command: $commandString Result=$result"
             logger.error(logMarker, errorMessage)
             ShellCommandException(errorMessage)
         }
+
         return result
     }
 
-    override fun startProcess(command: List, environment: Map, logMarker: Marker?, processListener: LongRunningProcessListener) {
-        startProcessInternal(command, environment, processListener)
+    override fun startProcess(command: List, environment: Map, logMarker: Marker?,
+                              processBuilder: ProcessBuilder): Process {
+        logger.debug(this.logMarker, "Executing command: ${command.joinToString(" ")}")
+        processBuilder.command(command)
+        setEnvironment(processBuilder, environment)
+        return processBuilder.start()
     }
 
     override fun escape(value: String): String {
         return value
     }
 
-    private fun startProcessInternal(command: List, environment: Map, processListener: NuProcessHandler): NuProcess {
-        logger.debug(logMarker, "Executing command: ${command.joinToString(" ")}")
-        val cmdEnv = environment + commonEnvironment
-        val processBuilder = builderFactory(command, cmdEnv)
-        processBuilder.setProcessListener(processListener)
-        return processBuilder.start()
+    private fun setEnvironment(processBuilder: ProcessBuilder, environment: Map) {
+        processBuilder.environment().clear() // do not inherit current process environment
+        processBuilder.environment().putAll(commonEnvironment)
+        processBuilder.environment().putAll(environment)
+    }
+
+    private fun streamReader(inputStream: InputStream): Callable {
+        return Callable { readStream(inputStream) }
+    }
+
+    private fun readStream(inputStream: InputStream): String {
+        val reader = InputStreamReader(inputStream, StandardCharsets.UTF_8)
+        val writer = StringBuilder(BUFFER_SIZE)
+        val buffer = CharArray(BUFFER_SIZE)
+
+        try {
+            while (true) {
+                val charCount = reader.read(buffer)
+
+                if (charCount == EOF) break
+
+                writer.append(buffer, 0, charCount)
+            }
+        } catch (e: IOException) {
+            logger.error("Failed to read from input stream", e)
+        } finally {
+            inputStream.close()
+        }
+
+        return writer.toString()
+    }
+
+    private fun waitForProcessToComplete(process: Process, logMarker: Marker?, commandString: String, pid: Int, timeOut: Duration) {
+        terminateProcessGracefully(process, logMarker, commandString, pid, timeOut)
+        terminateProcessForcibly(logMarker, commandString, pid, process, timeOut)
+    }
+
+    private fun terminateProcessGracefully(process: Process, logMarker: Marker?, commandString: String, pid: Int, timeOut: Duration) {
+        if (!process.isAlive) return
+
+        logger.debug(logMarker, "Trying to destroy failed command. Command: $commandString, PID: $pid")
+        process.destroy()
+        process.waitFor(timeOut.toMillis(), TimeUnit.MILLISECONDS)
+    }
+
+    private fun terminateProcessForcibly(logMarker: Marker?, commandString: String, pid: Int, process: Process, timeOut: Duration) {
+        if (!process.isAlive) return
+
+        logger.debug(logMarker, "Trying to destroy failed command forcibly. Command: $commandString, PID: $pid")
+        process.destroyForcibly()
+        process.waitFor(timeOut.toMillis(), TimeUnit.MILLISECONDS)
+    }
+
+    companion object {
+        private const val EOF = -1
+        private const val BUFFER_SIZE = 65536 // seems arbitrary, but using buffer with this size fastest (jmh) on multiple small commands
     }
 }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommandListener.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommandListener.kt
deleted file mode 100644
index 3da19dfc..00000000
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ShellCommandListener.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-package com.badoo.automation.deviceserver.command
-
-import com.zaxxer.nuprocess.NuAbstractProcessHandler
-import com.zaxxer.nuprocess.NuProcess
-import org.slf4j.LoggerFactory
-import java.io.ByteArrayOutputStream
-import java.io.IOException
-import java.nio.ByteBuffer
-import java.nio.channels.Channels
-import java.nio.channels.WritableByteChannel
-import java.nio.charset.StandardCharsets.UTF_8
-
-open class ShellCommandListener(initialStdOutBufferSize: Int = INITIAL_BYTE_ARRAY_SIZE) : IShellCommandListener, NuAbstractProcessHandler() {
-    private lateinit var nuProcess: NuProcess
-    // setting initial size to non-default for large amounts of data. to avoid too many Arrays.copyOf
-    private val stdOutBytes = ByteArrayOutputStream(initialStdOutBufferSize)
-    private val stdErrBytes = ByteArrayOutputStream(INITIAL_BYTE_ARRAY_SIZE)
-    private val stdOutBytesChannel: WritableByteChannel = Channels.newChannel(stdOutBytes)
-    private val stdErrBytesChannel: WritableByteChannel = Channels.newChannel(stdErrBytes)
-    private val logger = LoggerFactory.getLogger(javaClass.simpleName)
-
-    override var exitCode: Int = Int.MIN_VALUE
-    override val stdOut: String get() = stdOutBytes.toString(UTF_8.name())
-    override val stdErr: String get() = stdErrBytes.toString(UTF_8.name())
-    override val bytes: ByteArray get() = stdOutBytes.toByteArray()
-
-    override fun onPreStart(nuProcess: NuProcess) {
-        this.nuProcess = nuProcess
-    }
-
-    override fun onExit(exitCode: Int) {
-        this.exitCode = exitCode
-    }
-
-    override fun onStderr(buffer: ByteBuffer?, closed: Boolean) {
-        fetchOutput(buffer, stdErrBytesChannel)
-        super.onStderr(buffer, closed)
-    }
-
-    override fun onStdout(buffer: ByteBuffer?, closed: Boolean) {
-        fetchOutput(buffer, stdOutBytesChannel)
-        super.onStdout(buffer, closed)
-    }
-
-    private fun fetchOutput(buffer: ByteBuffer?, writer: WritableByteChannel) {
-        if (buffer != null && buffer.hasRemaining()) {
-            try {
-                writer.write(buffer)
-            } catch (e: IOException) {
-                // should be ok to catch only IOException, as NonWritableChannelException is not expected as channel is writable
-                logger.error("Failed to write data from buffer", e)
-                throw e
-            }
-        }
-    }
-
-    companion object {
-        const val INITIAL_BYTE_ARRAY_SIZE = Short.MAX_VALUE.toInt() // should be ok for most cases. increase if too many array copy
-    }
-}
\ No newline at end of file
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt
index 958ef51a..19b04c7f 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt
@@ -16,13 +16,12 @@ class Remote(
     override val hostName: String,
     override val userName: String,
     override val publicHostName: String,
-    private val localExecutor: IShellCommand = ShellCommand(),
+    private val localExecutor: IShellCommand = ShellCommand(commonEnvironment = mapOf("HOME" to System.getProperty("user.home"))),
     private val remoteExecutor: IShellCommand = getRemoteCommandExecutor(hostName, userName),
     override val fbsimctl: FBSimctl = FBSimctl(remoteExecutor, FBSimctlResponseParser())
 ) : IRemote {
     companion object {
         const val SSH_AUTH_SOCK = "SSH_AUTH_SOCK"
-        private const val INITIAL_BUFFER_SIZE = 10 * 1024 * 1024 //FIXME: looks arbitrary. taken as an average of video file sizes
 
         fun getRemoteCommandExecutor(hostName: String, userName: String): IShellCommand {
             return if (isLocalhost(hostName, userName)) {
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceFbsimctlProc.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceFbsimctlProc.kt
index c3635809..efec87d4 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceFbsimctlProc.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceFbsimctlProc.kt
@@ -15,8 +15,9 @@ class DeviceFbsimctlProc(
         remoteHost: String,
         username: String,
         cmd: List,
-        out_reader: (line: String) -> Unit,
-        err_reader: (line: String) -> Unit
+        commandEnvironment: Map,
+        out_reader: ((line: String) -> Unit)?,
+        err_reader: ((line: String) -> Unit)?
     ) -> ChildProcess = ChildProcess.Companion::fromCommand
 ) : FbsimctlProc(remote, udid, fbsimctlEndpoint, headless, childFactory) {
 
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxy.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxy.kt
index 8b0ab59d..c73599de 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxy.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/UsbProxy.kt
@@ -17,6 +17,7 @@ class UsbProxy(
         remoteHost: String,
         userName: String,
         cmd: List,
+        commandEnvironment: Map,
         out_reader: (line: String) -> Unit,
         err_reader: (line: String) -> Unit
     ) -> ChildProcess = ChildProcess.Companion::fromCommand
@@ -39,6 +40,7 @@ class UsbProxy(
             remote.hostName,
             remote.userName,
             listOf(IPROXY_BIN, localPort.toString(), devicePort.toString(), udid),
+            mapOf(),
             { message -> logger.trace(logMarker, "${this}: iproxy : ${message.trim()}") },
             { message -> logger.debug(logMarker, "${this}: iproxy : ${message.trim()}") }
         )
@@ -47,6 +49,7 @@ class UsbProxy(
             remote.hostName,
             remote.userName,
             listOf(SOCAT_BIN, "tcp-listen:$localPort,reuseaddr,fork", "tcp:0.0.0.0:$localPort"),
+            mapOf(),
             { message -> logger.trace(logMarker, "${this}: socat : ${message.trim()}") },
             { message -> logger.debug(logMarker, "${this}: socat : ${message.trim()}") }
         )
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt
index 6afcab76..9ba66558 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProc.kt
@@ -16,8 +16,9 @@ open class FbsimctlProc(
         remoteHost: String,
         username: String,
         cmd: List,
-        out_reader: (line: String) -> Unit,
-        err_reader: (line: String) -> Unit
+        commandEnvironment: Map,
+        out_reader: ((line: String) -> Unit)?,
+        err_reader: ((line: String) -> Unit)?
     ) -> ChildProcess = ChildProcess.Companion::fromCommand
 ) : LongRunningProc(udid, remote.hostName) {
     private val uri: URI = uriWithPath(fbsimctlEndpoint, "list")
@@ -32,6 +33,7 @@ open class FbsimctlProc(
                 remote.hostName,
                 remote.userName,
                 getFbsimctlCommand(headless),
+                mapOf(),
                 { logger.trace(logMarker, "${this@FbsimctlProc}: FbSimCtl : ${it.trim()}") },
                 { logger.debug(logMarker, "${this@FbsimctlProc}: FbSimCtl : ${it.trim()}") }
         )
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt
index f0cf9189..0e1272b2 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt
@@ -20,6 +20,7 @@ open class WebDriverAgent(
                 remoteHost: String,
                 userName: String,
                 cmd: List,
+                commandEnvironment: Map,
                 out_reader: ((line: String) -> Unit)?,
                 err_reader: ((line: String) -> Unit)?
         ) -> ChildProcess = ChildProcess.Companion::fromCommand
@@ -50,6 +51,7 @@ open class WebDriverAgent(
                 remote.hostName,
                 remote.userName,
                 launchXctestCommand,
+                mapOf(),
                 null,
                 { message -> logger.debug(logMarker, "${this@WebDriverAgent}: WDA : ${message.trim()}") }
         )
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/backup/SimulatorBackup.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/backup/SimulatorBackup.kt
index 48c12855..41270ca6 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/backup/SimulatorBackup.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/backup/SimulatorBackup.kt
@@ -30,7 +30,7 @@ class SimulatorBackup(
     ))
 
     companion object {
-        private const val CURRENT_VERSION = 1
+        const val CURRENT_VERSION = 1
     }
 
     data class BackupMeta(val version: Int, val created: String) {
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt
index 596337ff..1ab07bf0 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt
@@ -15,7 +15,7 @@ import kotlin.concurrent.withLock
 class SimulatorVideoRecorder(
     private val deviceInfo: DeviceInfo,
     private val remote: IRemote,
-    private val childFactory: (remoteHost: String, username: String, cmd: List,
+    private val childFactory: (remoteHost: String, username: String, cmd: List, commandEnvironment: Map,
                                    out_reader: (line: String) -> Unit, err_reader: (line: String) -> Unit
         ) -> ChildProcess? = ChildProcess.Companion::fromCommand,
     private val recorderStopTimeout: Duration = RECORDER_STOP_TIMEOUT,
@@ -68,7 +68,7 @@ class SimulatorVideoRecorder(
 
             val cmd = shell(videoRecordingCmd(fps = 5, frameWidth = frameWidth, frameHeight = frameHeight))
 
-            childProcess = childFactory(remote.hostName, remote.userName, cmd,
+            childProcess = childFactory(remote.hostName, remote.userName, cmd, mapOf(),
                 { logger.debug(logMarker, "$udid: VideoRecorder : ${it.trim()}") },
                 { logger.debug(logMarker, "$udid: VideoRecorder : ${it.trim()}") }
             )
@@ -133,7 +133,7 @@ class SimulatorVideoRecorder(
         }
 
         logger.info(logMarker, "Terminating video recording process")
-        childProcess?.kill(Duration.ofSeconds(1))
+        childProcess?.kill()
         delete()
         logger.info(logMarker, "Disposed video recording")
     }
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/CommandResultTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/CommandResultTest.kt
index 07b0bc83..1f9e4ba2 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/CommandResultTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/CommandResultTest.kt
@@ -11,9 +11,9 @@ class CommandResultTest {
         val actualResult = CommandResult(
                 stdOut = "out",
                 stdErr = "err",
-                stdOutBytes = "out".toByteArray(),
                 exitCode = 0,
-                cmd = listOf("rm", "-rf", "/")
+                cmd = listOf("rm", "-rf", "/"),
+                pid = 1
         ).toString()
 
         Assert.assertEquals("Wrong result string", expectedResult, actualResult)
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommandTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommandTest.kt
index 9f93496e..b06033bb 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommandTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/RemoteShellCommandTest.kt
@@ -2,35 +2,30 @@ package com.badoo.automation.deviceserver.command
 
 import com.badoo.automation.deviceserver.mockThis
 import com.nhaarman.mockito_kotlin.whenever
-import com.zaxxer.nuprocess.NuProcess
-import com.zaxxer.nuprocess.NuProcessBuilder
 import org.junit.Assert.assertEquals
 import org.junit.Before
 import org.junit.Test
-import org.mockito.Mock
 import org.mockito.MockitoAnnotations
 import java.io.ByteArrayOutputStream
+import java.io.InputStream
+import java.io.OutputStream
 import java.io.PrintStream
 import java.time.Duration
 
 class RemoteShellCommandTest {
     private lateinit var systemErr: PrintStream
     private lateinit var systemOut: PrintStream
-    private lateinit var spyProcessBuilder: TestProcessBuilder
     private lateinit var remoteShell: IShellCommand
-    @Mock private lateinit var processListener: ShellCommandListener
 
     private val remoteHost = "node"
     private val userName = "user"
     private val userAtHost = "user@node"
 
-    @Before fun setUp() {
+    @Before
+    fun setUp() {
         hideTestOutput() // comment out to debug
 
         MockitoAnnotations.initMocks(this)
-        whenever(processListener.exitCode).thenReturn(0)
-        whenever(processListener.stdOut).thenReturn("")
-        whenever(processListener.stdErr).thenReturn("")
     }
 
     private fun hideTestOutput() {
@@ -42,36 +37,25 @@ class RemoteShellCommandTest {
         System.setOut(testOut)
     }
 
-    @Test fun interactiveSshCommand() {
-        remoteShell = RemoteShellCommand(
-                remoteHost = remoteHost,
-                userName = userName,
-                builderFactory = ::nuProcessBuilderForTesting,
-                connectionTimeout = 1
-                )
-        remoteShell.exec(listOf("fbsimctl"), timeOut = Duration.ofMillis(100))
-
-        val expectedCommand = listOf(
-                "/usr/bin/ssh",
-                "-o", "ConnectTimeout=1",
-                "-o", "PreferredAuthentications=publickey",
-                "-q",
-                "-t", "-t",
-                userAtHost,
-                "fbsimctl"
-        )
-
-        assertEquals(expectedCommand, spyProcessBuilder.command())
-    }
+    @Test
+    fun sshCommandWithEnvironmentVariables() {
+        val processBuilder: ProcessBuilder = mockThis()
+        val process: Process = mockThis()
+        val outputStream: OutputStream = mockThis()
+        val stdOutStream: InputStream = mockThis()
+        val stdErrStream: InputStream = mockThis()
+        whenever(process.inputStream).thenReturn(stdOutStream)
+        whenever(process.outputStream).thenReturn(outputStream)
+        whenever(process.errorStream).thenReturn(stdErrStream)
+        whenever(processBuilder.start()).thenReturn(process)
 
-    @Test fun sshCommandWithEnvironmentVariables() {
         remoteShell = RemoteShellCommand(
                 remoteHost = remoteHost,
                 userName = userName,
-                builderFactory = ::nuProcessBuilderForTesting,
                 connectionTimeout = 1
                 )
-        remoteShell.exec(listOf("fbsimctl", "udid='UDID'", "\$PWD"), timeOut = Duration.ofMillis(100))
+
+        val result = remoteShell.exec(listOf("fbsimctl", "udid='UDID'", "\$PWD"), timeOut = Duration.ofMillis(100), processBuilder = processBuilder)
 
         val expectedCommand = listOf(
                 "/usr/bin/ssh",
@@ -86,22 +70,7 @@ class RemoteShellCommandTest {
                 "\$PWD"
         )
 
-        assertEquals(expectedCommand, spyProcessBuilder.command())
+        assertEquals(expectedCommand, result.cmd)
     }
 
-    private class TestProcessBuilder(cmd: List, env: Map) : NuProcessBuilder(cmd, env) {
-        val mockedProcess: NuProcess = mockThis()
-        init {
-            whenever(mockedProcess.pid).thenReturn(Int.MAX_VALUE)
-        }
-
-        override fun start(): NuProcess {
-            return mockedProcess
-        }
-    }
-
-    private fun nuProcessBuilderForTesting(cmd: List, env: Map): NuProcessBuilder {
-        spyProcessBuilder = TestProcessBuilder(cmd, env)
-        return spyProcessBuilder
-    }
 }
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ShellCommandTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ShellCommandTest.kt
index d497908b..f780086b 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ShellCommandTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ShellCommandTest.kt
@@ -13,9 +13,12 @@ import kotlin.test.assertFailsWith
 class ShellCommandTest {
     private lateinit var systemErr: PrintStream
     private lateinit var systemOut: PrintStream
+    private lateinit var shellCommand: ShellCommand
 
-    @Before fun setUp() {
+    @Before
+    fun setUp() {
         hideTestOutput() // comment out to debug
+        shellCommand = ShellCommand(mapOf())
     }
 
     private fun hideTestOutput() {
@@ -27,8 +30,9 @@ class ShellCommandTest {
         System.setOut(testOut)
     }
 
-    @Test fun testCommandWithRealProcess() {
-        val result = ShellCommand().exec(listOf("ls", "-lah"))
+    @Test
+    fun testCommandWithRealProcess() {
+        val result = shellCommand.exec(listOf("ls", "-lah"))
         assertThat("Wrong exit code", result.exitCode, equalTo(0))
         assertThat("StdOut should not be empty", result.stdOut, not(emptyString()))
         assertThat("StdErr should be empty", result.stdErr, emptyString())
@@ -37,20 +41,20 @@ class ShellCommandTest {
     @Test
     fun testCommandThrowsErrorWithRealProcess() {
         assertFailsWith {
-            ShellCommand().exec(listOf("/bin/cp"), returnFailure = false)
+            shellCommand.exec(listOf("/bin/cp"), returnFailure = false)
         }
     }
 
     @Test @Ignore("Flaky in docker container")
     fun testCommandThrowsErrorWhenCommandNotFound() {
         assertFailsWith {
-            ShellCommand().exec(listOf("/usr/bin/not_existing_command"), returnFailure = false)
+            shellCommand.exec(listOf("/usr/bin/not_existing_command"), returnFailure = false)
         }
     }
 
     @Test(expected = ShellCommandException::class)
     fun testTimeOutLongRunningCommand() {
-        val result = ShellCommand().exec(listOf("sleep", "600"), timeOut = Duration.ofMillis(100), returnFailure = false)
+        val result = shellCommand.exec(listOf("sleep", "600"), timeOut = Duration.ofMillis(100), returnFailure = false)
         assertThat("Wrong exit code", result.exitCode, equalTo(0))
         assertThat("StdOut should not be empty", result.stdOut, not(emptyString()))
         assertThat("StdErr should be empty", result.stdErr, emptyString())
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/RemoteTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/RemoteTest.kt
index 3450ab54..5edb026f 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/RemoteTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/RemoteTest.kt
@@ -31,7 +31,7 @@ class RemoteTest {
 
     @Test
     fun isReachable() {
-        val successfulResult = CommandResult("", "", ByteArray(0), 0)
+        val successfulResult = CommandResult("", "", 0, pid = 1)
         whenever(remoteExecutor.exec(anyList(), anyMap(), anyType(), anyBoolean(), anyType(), anyType()))
             .thenReturn(successfulResult)
 
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlTest.kt
index f8d3f93e..93fe3f6f 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctlTest.kt
@@ -25,7 +25,7 @@ class FBSimctlTest {
     }
 
     @Test fun mustTrimLastNewLine() {
-        whenever(executor.exec(anyList(), anyMap(), anyType(), anyBoolean(), any(), anyType())).thenReturn(CommandResult(fbsimctlResponse, "", ByteArray(0), 0))
+        whenever(executor.exec(anyList(), anyMap(), anyType(), anyBoolean(), any(), anyType())).thenReturn(CommandResult(fbsimctlResponse, "", 0, pid = 1))
         val fbSimctl = FBSimctl(executor, FBSimctlResponseParser())
         val deviceSets = fbSimctl.defaultDeviceSet()
         Assert.assertEquals("/a", deviceSets)
@@ -33,7 +33,7 @@ class FBSimctlTest {
 
     @Test(expected = FBSimctlError::class)
     fun shouldThrowWhenNoDeviceSets() {
-        whenever(executor.exec(anyList(), anyMap(), anyType(), anyBoolean(), any(), anyType())).thenReturn(CommandResult("\n", "", ByteArray(0), 0))
+        whenever(executor.exec(anyList(), anyMap(), anyType(), anyBoolean(), any(), anyType())).thenReturn(CommandResult("\n", "", 0, pid = 1))
         whenever(parser.parseDeviceSets(anyString())).thenReturn(emptyList())
         val fbSimctl = FBSimctl(executor, parser)
         fbSimctl.defaultDeviceSet()
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/DeviceFbsimctlProcTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/DeviceFbsimctlProcTest.kt
index cac58b01..2e828603 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/DeviceFbsimctlProcTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/DeviceFbsimctlProcTest.kt
@@ -55,8 +55,9 @@ class DeviceFbsimctlProcTest {
         remoteHost: String,
         username: String,
         cmd: List,
-        out_reader: (line: String) -> Unit,
-        err_reader: (line: String) -> Unit
+        commandEnvironment: Map,
+        out_reader: ((line: String) -> Unit)?,
+        err_reader: ((line: String) -> Unit)?
     ): ChildProcess {
         actualCommand = cmd
         return childProcess
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProcTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProcTest.kt
index ebc724d6..8172a665 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProcTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/proc/FbsimctlProcTest.kt
@@ -65,8 +65,9 @@ class FbsimctlProcTest {
         remoteHost: String,
         username: String,
         cmd: List,
-        out_reader: (line: String) -> Unit,
-        err_reader: (line: String) -> Unit
+        commandEnvironment: Map,
+        out_reader: ((line: String) -> Unit)?,
+        err_reader: ((line: String) -> Unit)?
     ): ChildProcess {
         actualCommand = cmd
         return childProcess
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorBackupTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorBackupTest.kt
index b13283e2..ea4d17df 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorBackupTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorBackupTest.kt
@@ -1,5 +1,6 @@
 package com.badoo.automation.deviceserver.ios.simulator
 
+import com.badoo.automation.deviceserver.ApplicationConfiguration
 import com.badoo.automation.deviceserver.command.CommandResult
 import com.badoo.automation.deviceserver.data.UDID
 import com.badoo.automation.deviceserver.host.IRemote
@@ -16,25 +17,34 @@ import org.mockito.ArgumentCaptor
 import org.mockito.Mock
 import org.mockito.Mockito.*
 import org.mockito.MockitoAnnotations
+import java.io.File
 
 class SimulatorBackupTest {
     private val metaJson = """
-        {"version":1,"created":"2018-01-12 01:46:48 +0000"}
+        {"version":${SimulatorBackup.CURRENT_VERSION},"created":"2018-01-12 01:46:48 +0000"}
     """.trimIndent()
 
     @Mock private lateinit var remote: IRemote
     private val udid: UDID = "M-Y-P-H-O-N-E"
     private val deviceSetPath = "/home/user/backup"
     private val captor = ArgumentCaptor.forClass(listOf("").javaClass)
-    private val resultStub = CommandResult("", "", ByteArray(0), 0)
-    private val resultFailureStub = CommandResult("", "", ByteArray(0), 1)
+    private val resultStub = CommandResult("", "", 0, pid = 1)
+    private val resultFailureStub = CommandResult("There is no such file or directory!", "", 1, pid = 1)
+    @Mock private lateinit var config: ApplicationConfiguration
+    @Mock
+    private lateinit var simulatorDirectory: File
+    @Mock
+    private lateinit var simulatorDataDirectory: File
+
 
     @Before fun setUp() {
         MockitoAnnotations.initMocks(this)
+        whenever(simulatorDirectory.absolutePath).thenReturn("simulatorDirectory")
+        whenever(simulatorDataDirectory.absolutePath).thenReturn("simulatorDataDirectory")
     }
 
     @Test fun shouldExistBackup() {
-        val resultWithMeta = CommandResult(metaJson, "", ByteArray(0), 0)
+        val resultWithMeta = CommandResult(metaJson, "", 0, pid = 1)
 
         whenever(remote.isDirectory(anyString())).thenReturn(true)
         whenever(remote.execIgnoringErrors(anyList(), anyMap(), anyLong())).thenReturn(resultWithMeta)
@@ -68,7 +78,7 @@ class SimulatorBackupTest {
         val cmdCaptor = ArgumentCaptor.forClass("".javaClass)
         verify(remote, times(1)).shell(cmdCaptor.capture() ?: "", anyBoolean())
         val redirectPath = "$deviceSetPath/${udid}_BACKUP/data/device_server/meta.json"
-        val regex = """echo \\\{\\"version\\":1,\\"created\\":\\"[0-9T:-]+\\"\\} > $redirectPath""".toRegex()
+        val regex = """echo \\\{\\"version\\":[0-9]+,\\"created\\":\\"[0-9T:-]+\\"\\} > $redirectPath""".toRegex()
         assertThat(cmdCaptor.firstValue,  matchesPattern(regex.pattern))
     }
 
@@ -87,6 +97,7 @@ class SimulatorBackupTest {
     @Test(expected = SimulatorBackupError::class)
     fun shouldRestoreThrow() {
         whenever(remote.execIgnoringErrors(anyList(), anyMap(), anyLong())).thenReturn(resultFailureStub)
+        whenever(remote.shell(anyString(), anyBoolean())).thenReturn(resultFailureStub)
         SimulatorBackup(remote, udid, deviceSetPath).restore()
     }
 }
\ No newline at end of file
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcessTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcessTest.kt
index 010ab19d..02fb09e3 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcessTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcessTest.kt
@@ -20,7 +20,7 @@ class SimulatorProcessTest {
     @Test
     fun testSimulatorProcessFound() {
         val simulatorProcess = SimulatorProcess(remote, udid)
-        val simulatorFoundCommandResult = CommandResult(stdOutWithSimulatorPid, "", ByteArray(0), 0)
+        val simulatorFoundCommandResult = CommandResult(stdOutWithSimulatorPid, "", 0, pid = 1)
 
         whenever(remote.execIgnoringErrors(any(), any(), any()))
             .thenReturn(simulatorFoundCommandResult)
@@ -31,7 +31,7 @@ class SimulatorProcessTest {
     @Test
     fun testSimulatorProcessNotFound() {
         val simulatorProcess = SimulatorProcess(remote, udid)
-        val noSimulatorFoundCommandResult = CommandResult("", "", ByteArray(0), 0)
+        val noSimulatorFoundCommandResult = CommandResult("", "", 0, pid = 1)
 
         whenever(remote.execIgnoringErrors(any(), any(), any()))
             .thenReturn(noSimulatorFoundCommandResult)
@@ -46,8 +46,8 @@ class SimulatorProcessTest {
         val simulatorProcess = SimulatorProcess(remote, udid)
 
         whenever(remote.execIgnoringErrors(any(), any(), any()))
-            .thenReturn(CommandResult(stdOutWithSimulatorPid, "", ByteArray(0), 0))
-            .thenReturn(CommandResult("", "", ByteArray(0), 0))
+            .thenReturn(CommandResult(stdOutWithSimulatorPid, "", 0, pid = 1))
+            .thenReturn(CommandResult("", "", 0, pid = 1))
 
         simulatorProcess.terminateChildProcess("SafariViewService")
 
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainerTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainerTest.kt
index 25e2d78e..8629ff0e 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainerTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainerTest.kt
@@ -28,7 +28,7 @@ class DataContainerTest {
 
     @Test
     fun shouldListFiles() {
-        val cmdResult = CommandResult("Caches/\nImage Cache/\nfile.ext\n", "", ByteArray(0), 0)
+        val cmdResult = CommandResult("Caches/\nImage Cache/\nfile.ext\n", "", 0, pid = 1)
 
         whenever(
             remote.execIgnoringErrors(
@@ -56,7 +56,7 @@ class DataContainerTest {
 
     @Test
     fun shouldReturnEmptyListForEmptyDirectory() {
-        val cmdResult = CommandResult("", "", ByteArray(0), 0)
+        val cmdResult = CommandResult("", "", 0, pid = 1)
 
         whenever(
             remote.execIgnoringErrors(

From 33bc163558df0fff1ceb30ecd4783560c0ead66a Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Sun, 2 Feb 2020 13:30:55 +0000
Subject: [PATCH 117/131] Remove unused class LongRunningProcessListener

---
 .../ios/proc/LongRunningProcessListener.kt    | 98 -------------------
 1 file changed, 98 deletions(-)
 delete mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProcessListener.kt

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProcessListener.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProcessListener.kt
deleted file mode 100644
index cad63aa1..00000000
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProcessListener.kt
+++ /dev/null
@@ -1,98 +0,0 @@
-package com.badoo.automation.deviceserver.ios.proc
-
-import com.zaxxer.nuprocess.NuProcess
-import com.zaxxer.nuprocess.codec.NuAbstractCharsetHandler
-import org.slf4j.LoggerFactory
-import java.nio.ByteBuffer
-import java.nio.CharBuffer
-import java.nio.charset.CoderResult
-import java.nio.charset.StandardCharsets.UTF_8
-import java.time.Duration
-import java.util.concurrent.TimeUnit
-
-class LongRunningProcessListener(
-    private val outReader: ((line: String) -> Unit)?,
-    private val errReader: ((line: String) -> Unit)?
-) : NuAbstractCharsetHandler(UTF_8.newEncoder(), UTF_8.newDecoder(), UTF_8.newDecoder()) {
-    private val logger = LoggerFactory.getLogger(javaClass.simpleName)
-    private val errStringBuilder = StringBuilder(BUFFER_SIZE)
-    private val outStringBuilder = StringBuilder(BUFFER_SIZE)
-    private lateinit var nuProcess: NuProcess
-
-    val isAlive: Boolean get() = nuProcess.isRunning
-    val pid: Int get() = nuProcess.pid
-    var exitCode: Int = Int.MIN_VALUE
-        private set
-
-    fun writeStdin(string: String) {
-        nuProcess.writeStdin(ByteBuffer.wrap(string.toByteArray()))
-    }
-
-    fun destroy(force: Boolean, timeOut: Duration = Duration.ofSeconds(1)): Int {
-        try {
-            nuProcess.destroy(force)
-        } catch (e: RuntimeException) {
-            // destroy throws exception when it failed to send the signal to process (RuntimeException)
-            logger.debug("Exception while terminating process $this", e)
-        }
-
-        return nuProcess.waitFor(timeOut.toMillis(), TimeUnit.MILLISECONDS)
-    }
-
-    override fun onPreStart(nuProcess: NuProcess) {
-        this.nuProcess = nuProcess
-        super.onPreStart(nuProcess)
-    }
-
-    override fun onExit(exitCode: Int) {
-        this.exitCode = exitCode
-        super.onExit(exitCode)
-    }
-
-    //region read out and err
-    override fun onStderrChars(buffer: CharBuffer?, closed: Boolean, coderResult: CoderResult?) {
-        if (errReader != null) {
-            fetchOutput(buffer, closed, errStringBuilder, errReader)
-        }
-        super.onStderrChars(buffer, closed, coderResult)
-    }
-
-    override fun onStdoutChars(buffer: CharBuffer?, closed: Boolean, coderResult: CoderResult?) {
-        if (outReader != null) {
-            fetchOutput(buffer, closed, outStringBuilder, outReader)
-        }
-        super.onStdoutChars(buffer, closed, coderResult)
-    }
-
-    private fun fetchOutput(
-        buffer: CharBuffer?,
-        closed: Boolean,
-        stringBuilder: StringBuilder,
-        readerFunction: (string: String) -> Unit
-    ) {
-        if (buffer != null && buffer.hasRemaining()) {
-            val chars = CharArray(buffer.remaining())
-            buffer.get(chars) // writes from buffer to char array
-
-            for (c in chars) { // in case output is multi-line
-                if (NEW_LINE == c) {
-                    readerFunction(stringBuilder.toString())
-                    stringBuilder.setLength(0) // clear
-                } else {
-                    stringBuilder.append(c)
-                }
-            }
-
-            if (closed && stringBuilder.isNotEmpty()) { // in case no new line at the end
-                readerFunction(stringBuilder.toString())
-                stringBuilder.setLength(0) // clear
-            }
-        }
-    }
-    //endregion
-
-    private companion object {
-        const val BUFFER_SIZE = 262144 // should be ok: example 200 simulators * 2buffers * 256KB == 100 MB
-        const val NEW_LINE: Char = '\n'
-    }
-}
\ No newline at end of file

From b6a24c6c9e2ffc48299eae9c98a41c5e89034205 Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Tue, 4 Feb 2020 23:13:29 +0000
Subject: [PATCH 118/131] Cleanup zombie processes

---
 device-server/build.gradle                    |   2 +-
 .../automation/deviceserver/DeviceServer.kt   |   4 +
 .../deviceserver/command/ZombieReaper.kt      | 119 ++++++++++++++++++
 .../deviceserver/command/ZombieReaperTest.kt  |  98 +++++++++++++++
 4 files changed, 222 insertions(+), 1 deletion(-)
 create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ZombieReaper.kt
 create mode 100644 device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ZombieReaperTest.kt

diff --git a/device-server/build.gradle b/device-server/build.gradle
index da8d4110..f710d901 100644
--- a/device-server/build.gradle
+++ b/device-server/build.gradle
@@ -70,7 +70,7 @@ dependencies {
     //endregion
 
     //region process management dependencies
-    compile group: 'net.java.dev.jna', name: 'jna', version: "4.5.1"
+    compile group: 'net.java.dev.jna', name: 'jna', version: "5.5.0"
     compile group: 'com.zaxxer', name: 'nuprocess', version: "1.1.3"
     //region
 
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
index 3ad0c7cf..a1054aff 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
@@ -1,5 +1,6 @@
 package com.badoo.automation.deviceserver
 
+import com.badoo.automation.deviceserver.command.ZombieReaper
 import com.badoo.automation.deviceserver.controllers.DevicesController
 import com.badoo.automation.deviceserver.controllers.StatusController
 import com.badoo.automation.deviceserver.data.*
@@ -69,6 +70,7 @@ fun getAddresses(): List {
 }
 
 private val appConfiguration = ApplicationConfiguration()
+private val zombieReaper = ZombieReaper()
 
 private fun serverConfig(): DeviceServerConfig {
     if (appConfiguration.deviceServerConfigPath.isEmpty()) {
@@ -107,6 +109,8 @@ fun Application.module() {
     deviceManager.startAutoRegisteringDevices()
     deviceManager.launchAutoReleaseLoop()
 
+    zombieReaper.launchReapingZombies()
+
     val devicesController = DevicesController(deviceManager)
     val statusController = StatusController(deviceManager)
 
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ZombieReaper.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ZombieReaper.kt
new file mode 100644
index 00000000..6c00874a
--- /dev/null
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/command/ZombieReaper.kt
@@ -0,0 +1,119 @@
+package com.badoo.automation.deviceserver.command
+
+import com.sun.jna.Library
+import com.sun.jna.Native
+import com.sun.jna.ptr.IntByReference
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import kotlin.streams.toList
+
+class ZombieReaper {
+    private val logger: Logger = LoggerFactory.getLogger(javaClass.simpleName)
+    private val executor = Executors.newScheduledThreadPool(1)
+
+    fun launchReapingZombies() {
+        val task = {
+            // Try-catch is here in order to:
+            // - not to loose errors silently
+            // - not to interrupt subsequent executions
+            try {
+                reapZombies()
+            } catch (t: Throwable) {
+                logger.error("Failed to reap zombie processes. Reason: ${t.message}", t)
+            }
+        }
+
+        executor.scheduleWithFixedDelay(task, 60L, 60L, TimeUnit.SECONDS)
+    }
+
+    fun reapZombies() {
+        val zombies = findZombies()
+
+        zombies.forEach { zombie ->
+            reap(zombie)
+        }
+    }
+
+    private fun findZombies(): List {
+        val childProcesses = ProcessHandle.current().children()
+        val zombies = childProcesses.filter { it.isZombie }
+        return zombies.map { it.pid().toInt() }.toList()
+    }
+
+    private fun reap(pid: Int) {
+        val statusReference = IntByReference()
+        val waitResult = cLibrary.waitpid(pid, statusReference, WNOHANG)
+        val exitStatus = ProcessExitStatus(statusReference.value)
+
+        when (waitResult) {
+            pid -> logger.trace("Successfully reaped zombie process with PID $pid")
+            0 -> logger.trace("The zombie process with PID $pid has not yet changed it's state")
+            -1 -> logger.error("Error happened while reaping zombie process with PID $pid")
+        }
+
+        when {
+            exitStatus.isExited -> logger.trace("Zombie process with PID $pid had normal termination")
+            exitStatus.isSignaled -> logger.error("Zombie process with PID $pid was terminated by signal ${exitStatus.termSignal}")
+        }
+    }
+
+    private val ProcessHandle.isZombie: Boolean get() = !info().command().isPresent
+
+    companion object {
+        private val cLibrary: CLibrary = Native.load("c", CLibrary::class.java)
+        private const val WNOHANG = 1 /* Don't block waiting. */
+    }
+}
+
+/**
+ * Have to use C library to reap zombies
+ */
+private interface CLibrary : Library {
+    /**
+     * Wait for process to change state
+     * Refer to man page for waitpid
+     */
+    fun waitpid(pid: Int, statusReference: IntByReference, options: Int): Int
+}
+
+/**
+ * Exit status of a child process
+ * Refer to man page for waitpid
+ */
+private class ProcessExitStatus(private val status: Int) {
+    /**
+     * LibC macros WIFSIGNALED
+     *
+     * Nonzero if STATUS indicates termination by a signal.
+     *
+     * (((status) & 0x7f) + 1) >> 1) > 0)
+     */
+    val isSignaled: Boolean
+        get() {
+            return (((status and 0x7f) + 1) shl 1) > 0
+        }
+
+    /**
+     * LibC macros WIFEXITED
+     *
+     * True if STATUS indicates normal termination.
+     */
+    val isExited: Boolean
+        get() {
+            return termSignal == 0
+        }
+
+    /**
+     * LibC macros WTERMSIG
+     *
+     * If WIFSIGNALED(STATUS), the terminating signal.
+     *
+     * ((status) & 0x7f)
+     */
+    val termSignal: Int
+        get() {
+            return status and 0x7f
+        }
+}
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ZombieReaperTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ZombieReaperTest.kt
new file mode 100644
index 00000000..9241b5dc
--- /dev/null
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/command/ZombieReaperTest.kt
@@ -0,0 +1,98 @@
+package com.badoo.automation.deviceserver.command
+
+import com.sun.jna.Library
+import com.sun.jna.Native
+import org.junit.After
+import org.junit.Assert
+import org.junit.Test
+import kotlin.streams.toList
+
+class ZombieReaperTest {
+    private val reaper = ZombieReaper()
+
+    @After
+    fun tearDown() {
+        cleanupChildProcesses()
+        assertChildProcessCount(0)
+    }
+
+    @Test
+    fun testReapZombie() {
+        createZombie()
+        assertChildProcessCount(1)
+
+        reaper.reapZombies()
+
+        assertChildProcessCount(0)
+    }
+
+    @Test
+    fun testReapOnlyZombies() {
+        createZombie()
+        createRegularChildProcess()
+        assertChildProcessCount(2)
+
+        reaper.reapZombies()
+
+        assertChildProcessCount(1)
+    }
+
+    private fun assertChildProcessCount(expectedCount: Int) {
+        Assert.assertEquals(
+            "Wrong number of child processes",
+            expectedCount.toLong(),
+            ProcessHandle.current().children().count()
+        )
+    }
+
+    private fun cleanupChildProcesses() {
+        val pids = ProcessHandle.current().children().map { it.pid().toInt() }.toList()
+
+        pids.forEach { pid ->
+            testCLibrary.kill(pid, SIGKILL)
+        }
+
+        Thread.sleep(100L) // to ensure process has changed it's state
+
+        reaper.reapZombies()
+    }
+
+    private fun createRegularChildProcess(): Int {
+        val forkPID = testCLibrary.fork()
+
+        if (forkPID == 0) {
+            Thread.sleep(2000L) // ensure no actions are taken by a fork
+        }
+
+        return forkPID
+    }
+
+    private fun createZombie(): Int {
+        val forkPID = testCLibrary.fork()
+
+        if (forkPID == 0) {
+            Thread.sleep(2000L) // ensure no actions are taken by a fork
+        } else {
+            Thread.sleep(100L) // wait until initialized properly
+            killProcess(forkPID)
+        }
+
+        return forkPID
+    }
+
+    private fun killProcess(pid: Int) {
+        val signalResult = testCLibrary.kill(pid, SIGKILL)
+        Assert.assertEquals("Failed to send SIGKILL to process $pid", 0, signalResult)
+        Thread.sleep(50L) // to ensure process has changed it's state
+    }
+
+    companion object {
+        private const val SIGKILL = 9
+        private val testCLibrary: TestCLibrary = Native.load("c", TestCLibrary::class.java)
+    }
+}
+
+private interface TestCLibrary : Library {
+    fun kill(pid: Int, signal: Int): Int
+    fun fork(): Int
+}

From c8889c41f38905a229c07f69817fdca8ee07700f Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Thu, 6 Feb 2020 09:52:23 +0000
Subject: [PATCH 119/131] Disable pasteboard automatic sync

---
 .../deviceserver/host/management/ISimulatorHostChecker.kt  | 7 ++++---
 1 file changed, 4 insertions(+), 3 deletions(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt
index 4770d387..24b46025 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt
@@ -136,11 +136,12 @@ class SimulatorHostChecker(
 
     override fun setupHost() {
         // disable node hardware keyboard, i.e. use on-screen one
-        remote.execIgnoringErrors("defaults write com.apple.iphonesimulator ConnectHardwareKeyboard -bool false".split(" "))
+        remote.execIgnoringErrors("/usr/bin/defaults write com.apple.iphonesimulator ConnectHardwareKeyboard -bool false".split(" "))
+        remote.execIgnoringErrors("/usr/bin/defaults write com.apple.iphonesimulator PasteboardAutomaticSync -bool false".split(" "))
 
         // disable simulator location
-        remote.execIgnoringErrors("defaults write com.apple.iphonesimulator LocationMode \"3101\"".split(" "))
-        remote.execIgnoringErrors("defaults write com.apple.iphonesimulator ShowChrome -bool false".split(" "))
+        remote.execIgnoringErrors("/usr/bin/defaults write com.apple.iphonesimulator LocationMode \"3101\"".split(" "))
+        remote.execIgnoringErrors("/usr/bin/defaults write com.apple.iphonesimulator ShowChrome -bool false".split(" "))
         //  other options that might be useful are:
         //  EnableKeyboardSync = 0;
         //  GraphicsQualityOverride = 10;

From 76a188cc4122608d469d3a06fa476130033c247e Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Thu, 6 Feb 2020 10:08:38 +0000
Subject: [PATCH 120/131] Simplify DesiredCapabilities matcher

---
 .../deviceserver/host/SimulatorProvider.kt    |  6 ++---
 .../management/DesiredCapabilitiesMatcher.kt  | 26 +++++--------------
 2 files changed, 10 insertions(+), 22 deletions(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorProvider.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorProvider.kt
index dda652d0..4ed0aac7 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorProvider.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorProvider.kt
@@ -1,13 +1,13 @@
 package com.badoo.automation.deviceserver.host
 
 import com.badoo.automation.deviceserver.data.DesiredCapabilities
+import com.badoo.automation.deviceserver.data.DeviceInfo
 import com.badoo.automation.deviceserver.host.management.DesiredCapabilitiesMatcher
-import com.badoo.automation.deviceserver.host.management.IDesiredCapabilitiesMatcher
 import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDevice
 
 class SimulatorProvider(
         val remote: IRemote,
-        private val desiredCapsMatcher: IDesiredCapabilitiesMatcher = DesiredCapabilitiesMatcher()
+        private val desiredCapsMatcher: DesiredCapabilitiesMatcher = DesiredCapabilitiesMatcher()
 ) : ISimulatorProvider {
     private var cache: List = emptyList()
 
@@ -15,7 +15,7 @@ class SimulatorProvider(
         val matchList =
                 when {
                     desiredCaps.udid != null -> listOfNotNull(findBy(desiredCaps.udid))
-                    desiredCaps.existing -> list().filter { desiredCapsMatcher.isMatch(it, desiredCaps) }
+                    desiredCaps.existing -> list().filter { desiredCapsMatcher.isMatch(DeviceInfo(it), desiredCaps) }
                     else -> return create(desiredCaps.model, desiredCaps.os, true)
                 }
 
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DesiredCapabilitiesMatcher.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DesiredCapabilitiesMatcher.kt
index 3cfc0744..ccbf1b40 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DesiredCapabilitiesMatcher.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DesiredCapabilitiesMatcher.kt
@@ -2,29 +2,17 @@ package com.badoo.automation.deviceserver.host.management
 
 import com.badoo.automation.deviceserver.data.DesiredCapabilities
 import com.badoo.automation.deviceserver.data.DeviceInfo
-import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDevice
 
-interface IDesiredCapabilitiesMatcher {
-    fun isMatch(actual: FBSimctlDevice, desiredCaps: DesiredCapabilities): Boolean
-    fun isMatch(actual: DeviceInfo, desiredCaps: DesiredCapabilities): Boolean
-}
-
-class DesiredCapabilitiesMatcher : IDesiredCapabilitiesMatcher {
-    override fun isMatch(actual: DeviceInfo, desiredCaps: DesiredCapabilities): Boolean {
-        if (desiredCaps.udid != null) {
-            return desiredCaps.udid == actual.udid
-        }
+class DesiredCapabilitiesMatcher {
 
+    fun isMatch(actual: DeviceInfo, desiredCaps: DesiredCapabilities): Boolean {
         with(desiredCaps) {
-            return (model == null || model == actual.model) && (os == null || isRuntimeMatch(os, actual.os))
+            return if (udid == null || udid.isBlank()) {
+                (model == null || model == actual.model) && (os == null || isRuntimeMatch(os, actual.os))
+            } else {
+                udid == actual.udid
+            }
         }
-
-    }
-
-    override fun isMatch(actual: FBSimctlDevice, desiredCaps: DesiredCapabilities): Boolean {
-        val deviceInfo = DeviceInfo(actual)
-
-        return isMatch(deviceInfo, desiredCaps)
     }
 
     internal fun isRuntimeMatch(desired: String, actual: String): Boolean {

From aa40b3346d39f39a4e73f16b8938c39c427e21e0 Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Fri, 7 Feb 2020 10:30:19 +0000
Subject: [PATCH 121/131] Add DeviceRef marker to LongRunningProc

---
 .../badoo/automation/deviceserver/ios/proc/LongRunningProc.kt  | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProc.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProc.kt
index fa6faaee..fd389491 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProc.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProc.kt
@@ -15,7 +15,8 @@ abstract class LongRunningProc(udid: UDID, remoteHostName: String) : ILongRunnin
     protected val logger: Logger = LoggerFactory.getLogger(javaClass.simpleName)
     protected val logMarker = MapEntriesAppendingMarker(mapOf(
             LogMarkers.HOSTNAME to remoteHostName,
-            LogMarkers.UDID to udid
+            LogMarkers.UDID to udid,
+            LogMarkers.DEVICE_REF to "$udid-$remoteHostName".replace(Regex("[^-\\w]"), "-")
     ))
     @Volatile protected var childProcess: ChildProcess? = null
     override val isProcessAlive: Boolean get() = true == childProcess?.isAlive()

From 5f2a11aadf2a3b971108e06b0b728cabbea0ab2c Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Fri, 7 Feb 2020 18:06:17 +0000
Subject: [PATCH 122/131] Extract method to create Device References

---
 .../com/badoo/automation/deviceserver/host/DevicesNode.kt | 8 ++------
 .../badoo/automation/deviceserver/host/SimulatorsNode.kt  | 5 ++---
 .../automation/deviceserver/ios/proc/LongRunningProc.kt   | 3 ++-
 .../com/badoo/automation/deviceserver/util/Support.kt     | 3 +++
 .../badoo/automation/deviceserver/util/SupportKtTest.kt   | 8 +++++++-
 5 files changed, 16 insertions(+), 11 deletions(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
index 2fed89c8..b82f79c2 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
@@ -8,6 +8,7 @@ import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundEx
 import com.badoo.automation.deviceserver.ios.device.*
 import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl
 import com.badoo.automation.deviceserver.ios.simulator.periodicTasksPool
+import com.badoo.automation.deviceserver.util.deviceRefFromUDID
 import net.logstash.logback.marker.MapEntriesAppendingMarker
 import org.slf4j.LoggerFactory
 import java.io.File
@@ -182,7 +183,7 @@ class DevicesNode(
 
         synchronized(this) {
             slot = slots.reserve(desiredCaps)
-            ref = newRef(slot!!.udid)
+            ref = deviceRefFromUDID(slot!!.udid, remote.publicHostName)
 
             activeRefs[ref!!] = slot!!.udid
         }
@@ -309,11 +310,6 @@ class DevicesNode(
         )
     }
 
-    private fun newRef(udid: UDID): DeviceRef {
-        val unsafe = Regex("[^\\-_a-zA-Z\\d]") // TODO: Replace with UUID 4
-        return "$udid-${remote.publicHostName}".replace(unsafe, "-")
-    }
-
     private fun slotByExternalRef(deviceRef: DeviceRef): DeviceSlot {
         val udid = activeRefs[deviceRef] ?: throw(DeviceNotFoundException("Device $deviceRef not found"))
 
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
index 41a304b8..be57ed69 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
@@ -9,6 +9,7 @@ import com.badoo.automation.deviceserver.host.management.PortAllocator
 import com.badoo.automation.deviceserver.host.management.errors.OverCapacityException
 import com.badoo.automation.deviceserver.ios.simulator.ISimulator
 import com.badoo.automation.deviceserver.ios.simulator.simulatorsThreadPool
+import com.badoo.automation.deviceserver.util.deviceRefFromUDID
 import kotlinx.coroutines.experimental.launch
 import kotlinx.coroutines.experimental.newFixedThreadPoolContext
 import kotlinx.coroutines.experimental.runBlocking
@@ -80,7 +81,7 @@ class SimulatorsNode(
                 throw RuntimeException(message)
             }
 
-            val ref = newRef(fbSimctlDevice.udid)
+            val ref = deviceRefFromUDID(fbSimctlDevice.udid, remote.publicHostName)
             val ports = portAllocator.allocateDAP()
             allocatedPorts[ref] = ports
 
@@ -155,8 +156,6 @@ class SimulatorsNode(
         }
     }
 
-    private fun newRef(udid: String): DeviceRef = "$udid-${remote.publicHostName}".replace(Regex("[^-\\w]"), "-")
-
     override fun approveAccess(deviceRef: DeviceRef, bundleId: String) {
         getDeviceFor(deviceRef).approveAccess(bundleId)
     }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProc.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProc.kt
index fd389491..be984357 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProc.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/LongRunningProc.kt
@@ -4,6 +4,7 @@ import com.badoo.automation.deviceserver.LogMarkers
 import com.badoo.automation.deviceserver.command.ChildProcess
 import com.badoo.automation.deviceserver.data.UDID
 import com.badoo.automation.deviceserver.util.CustomHttpClient
+import com.badoo.automation.deviceserver.util.deviceRefFromUDID
 import net.logstash.logback.marker.MapEntriesAppendingMarker
 import org.slf4j.Logger
 import org.slf4j.LoggerFactory
@@ -16,7 +17,7 @@ abstract class LongRunningProc(udid: UDID, remoteHostName: String) : ILongRunnin
     protected val logMarker = MapEntriesAppendingMarker(mapOf(
             LogMarkers.HOSTNAME to remoteHostName,
             LogMarkers.UDID to udid,
-            LogMarkers.DEVICE_REF to "$udid-$remoteHostName".replace(Regex("[^-\\w]"), "-")
+            LogMarkers.DEVICE_REF to deviceRefFromUDID(udid, remoteHostName)
     ))
     @Volatile protected var childProcess: ChildProcess? = null
     override val isProcessAlive: Boolean get() = true == childProcess?.isAlive()
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/Support.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/Support.kt
index 6ba20f07..85c41f59 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/Support.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/util/Support.kt
@@ -1,6 +1,7 @@
 package com.badoo.automation.deviceserver.util
 
 import com.badoo.automation.deviceserver.WaitTimeoutError
+import com.badoo.automation.deviceserver.data.DeviceRef
 import org.slf4j.Logger
 import org.slf4j.Marker
 import java.net.URI
@@ -78,3 +79,5 @@ fun ensure(condition: Boolean, exception: () -> RuntimeException) {
 fun uriWithPath(uri: URI, path: String): URI {
     return URI(listOf(uri.toString(), path).joinToString("/")).normalize()
 }
+
+fun deviceRefFromUDID(udid: String, hostName: String): DeviceRef = "$udid-$hostName".replace(Regex("[^-\\w]"), "-")
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/util/SupportKtTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/util/SupportKtTest.kt
index 2596a660..4799fb62 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/util/SupportKtTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/util/SupportKtTest.kt
@@ -40,4 +40,10 @@ class SupportKtTest {
             }
         }
     }
-}
\ No newline at end of file
+
+    @Test
+    fun testDeviceRefFromUDID() {
+        val deviceRef = deviceRefFromUDID("asDF-124", "my.host.name.domain() @")
+        assertEquals("Wrong device ref", "asDF-124-my-host-name-domain----", deviceRef)
+    }
+}

From a1ce93523d77baedaf78cf3bb6d26dd24832ab97 Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Fri, 7 Feb 2020 20:48:10 +0000
Subject: [PATCH 123/131] Record video using WebDriverAgent's MJPEG streamer

---
 .../deviceserver/ApplicationConfiguration.kt  |   6 +-
 .../deviceserver/data/DeviceAllocatedPorts.kt |   5 +-
 .../automation/deviceserver/data/DeviceDTO.kt |   3 +-
 .../deviceserver/host/DevicesNode.kt          |  17 +-
 .../automation/deviceserver/host/IRemote.kt   |   3 +
 .../automation/deviceserver/host/Remote.kt    |   4 +-
 .../deviceserver/host/SimulatorsNode.kt       |   1 +
 .../host/management/PortAllocator.kt          |   8 +-
 .../automation/deviceserver/ios/WdaClient.kt  |   4 +
 .../deviceserver/ios/device/Device.kt         |  34 +++-
 .../ios/device/DeviceWebDriverAgent.kt        |   6 +-
 .../ios/proc/SimulatorWebDriverAgent.kt       |   8 +-
 .../deviceserver/ios/proc/WebDriverAgent.kt   |   5 +-
 .../deviceserver/ios/simulator/ISimulator.kt  |   5 +-
 .../deviceserver/ios/simulator/Simulator.kt   |  41 +++-
 .../ios/simulator/video/MJPEGVideoRecorder.kt | 178 ++++++++++++++++++
 .../simulator/video/SimulatorVideoRecorder.kt |  16 +-
 .../ios/simulator/video/VideoRecorder.kt      |   9 +
 .../badoo/automation/deviceserver/TestUtil.kt |   4 +-
 .../deviceserver/host/SimulatorsNodeTest.kt   |  15 +-
 20 files changed, 322 insertions(+), 50 deletions(-)
 create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/MJPEGVideoRecorder.kt
 create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/VideoRecorder.kt

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt
index c5a5c197..2b86a20c 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt
@@ -1,5 +1,7 @@
 package com.badoo.automation.deviceserver
 
+import com.badoo.automation.deviceserver.ios.simulator.video.SimulatorVideoRecorder
+
 class ApplicationConfiguration {
     private val wdaSimulatorBundlePathProperty = "wda.bundle.path"
     val wdaSimulatorBundlePath: String = System.getProperty(wdaSimulatorBundlePathProperty)
@@ -23,4 +25,6 @@ class ApplicationConfiguration {
 
     val trustStorePath: String = System.getProperty("trust.store.path", "")
     val assetsPath: String = System.getProperty("media.assets.path", "")
-}
\ No newline at end of file
+    val videoRecorderClassName = System.getProperty("video.recorder", SimulatorVideoRecorder::class.qualifiedName)
+    val videoRecorderFrameRate = Integer.getInteger("video.recorder.frame.rate", 4)
+}
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceAllocatedPorts.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceAllocatedPorts.kt
index 9adb3ee0..b24985cf 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceAllocatedPorts.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceAllocatedPorts.kt
@@ -4,9 +4,10 @@ data class DeviceAllocatedPorts(
     val fbsimctlPort: Int,
     val wdaPort: Int,
     val calabashPort: Int,
+    val mjpegServerPort: Int,
     private val defaultCalabashPort: Int = 37265
 ) {
     fun toSet(): Set {
-        return setOf(fbsimctlPort, wdaPort, calabashPort, defaultCalabashPort)
+        return setOf(fbsimctlPort, wdaPort, calabashPort, mjpegServerPort, defaultCalabashPort)
     }
-}
\ No newline at end of file
+}
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceDTO.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceDTO.kt
index d5b35068..d702270c 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceDTO.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/DeviceDTO.kt
@@ -9,6 +9,7 @@ data class DeviceDTO(
         val fbsimctl_endpoint: URI,
         val wda_endpoint: URI,
         val calabash_port: Int,
+        val mjpeg_server_port: Int,
         val user_ports: Set, // From PortAllocator
         val info: DeviceInfo,
         val last_error: ErrorDto?,
@@ -24,4 +25,4 @@ data class ActualCapabilities(
 
         @JsonProperty("video_capture")
         val videoCapture: Boolean
-)
\ No newline at end of file
+)
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
index b82f79c2..bb31be0d 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
@@ -274,13 +274,21 @@ class DevicesNode(
         return false // crash logs are not supported on devices yet
     }
 
-    override fun videoRecordingDelete(deviceRef: DeviceRef): Unit = throw(NotImplementedError())
+    override fun videoRecordingDelete(deviceRef: DeviceRef) {
+        slotByExternalRef(deviceRef).device.videoRecorder.delete()
+    }
 
-    override fun videoRecordingGet(deviceRef: DeviceRef): ByteArray = throw(NotImplementedError())
+    override fun videoRecordingGet(deviceRef: DeviceRef): ByteArray {
+        return slotByExternalRef(deviceRef).device.videoRecorder.getRecording()
+    }
 
-    override fun videoRecordingStart(deviceRef: DeviceRef): Unit = throw(NotImplementedError())
+    override fun videoRecordingStart(deviceRef: DeviceRef) {
+        slotByExternalRef(deviceRef).device.videoRecorder.start()
+    }
 
-    override fun videoRecordingStop(deviceRef: DeviceRef): Unit = throw(NotImplementedError())
+    override fun videoRecordingStop(deviceRef: DeviceRef) {
+        slotByExternalRef(deviceRef).device.videoRecorder.stop()
+    }
 
     override fun listFiles(deviceRef: DeviceRef, dataPath: DataPath): List = throw(NotImplementedError())
 
@@ -299,6 +307,7 @@ class DevicesNode(
             fbsimctl_endpoint = device.fbsimctlEndpoint,
             wda_endpoint = device.wdaEndpoint,
             calabash_port = device.calabashPort,
+            mjpeg_server_port = device.mjpegServerPort,
             user_ports = emptySet(),
             info = device.deviceInfo,
             last_error = device.lastException?.toDto(),
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt
index 26fb2a36..2d026038 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/IRemote.kt
@@ -1,6 +1,7 @@
 package com.badoo.automation.deviceserver.host
 
 import com.badoo.automation.deviceserver.command.CommandResult
+import com.badoo.automation.deviceserver.command.IShellCommand
 import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl
 import java.io.File
 import java.time.Duration
@@ -21,6 +22,8 @@ interface IRemote {
     val hostName: String
     val userName: String
     val publicHostName: String
+    val localExecutor: IShellCommand
+    val remoteExecutor: IShellCommand
     fun isReachable(): Boolean
     fun isLocalhost(): Boolean = isLocalhost(hostName, userName)
 
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt
index 19b04c7f..9ed14b39 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/Remote.kt
@@ -16,8 +16,8 @@ class Remote(
     override val hostName: String,
     override val userName: String,
     override val publicHostName: String,
-    private val localExecutor: IShellCommand = ShellCommand(commonEnvironment = mapOf("HOME" to System.getProperty("user.home"))),
-    private val remoteExecutor: IShellCommand = getRemoteCommandExecutor(hostName, userName),
+    override val localExecutor: IShellCommand = ShellCommand(commonEnvironment = mapOf("HOME" to System.getProperty("user.home"))),
+    override val remoteExecutor: IShellCommand = getRemoteCommandExecutor(hostName, userName),
     override val fbsimctl: FBSimctl = FBSimctl(remoteExecutor, FBSimctlResponseParser())
 ) : IRemote {
     companion object {
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
index be57ed69..e10a429e 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
@@ -144,6 +144,7 @@ class SimulatorsNode(
                 fbsimctlEndpoint,
                 wdaEndpoint,
                 calabashPort,
+                mjpegServerPort,
                 device.userPorts.toSet(),
                 device.info,
                 device.lastError?.toDto(),
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/PortAllocator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/PortAllocator.kt
index ee772427..f9d32ff1 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/PortAllocator.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/PortAllocator.kt
@@ -12,12 +12,12 @@ class PortAllocator(min: Int = PORT_RANGE_START, max: Int = PORT_RANGE_END) {
     private var ports: Set = IntRange(min, max).toSet()
 
     fun allocateDAP(): DeviceAllocatedPorts {
-        val take = allocate(3)
-        return DeviceAllocatedPorts(take[0], take[1], take[2])
+        val ports = allocate(4)
+        return DeviceAllocatedPorts(ports[0], ports[1], ports[2], ports[3])
     }
 
     fun deallocateDAP(dap: DeviceAllocatedPorts) {
-        deallocate(listOf(dap.calabashPort, dap.fbsimctlPort, dap.wdaPort))
+        deallocate(listOf(dap.calabashPort, dap.fbsimctlPort, dap.wdaPort, dap.mjpegServerPort))
     }
 
     fun available(): Int {
@@ -40,4 +40,4 @@ class PortAllocator(min: Int = PORT_RANGE_START, max: Int = PORT_RANGE_END) {
             ports = ports.plus(entries)
         }
     }
-}
\ No newline at end of file
+}
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/WdaClient.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/WdaClient.kt
index 1b5af499..b13224b5 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/WdaClient.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/WdaClient.kt
@@ -113,4 +113,8 @@ class WdaClient(
             throw WdaException("WebDriver returned non zero status ${json["status"]}: ${json["value"]}")
         }
     }
+
+    fun updateAppiumSettings(settings: Map): JsonNode {
+        return post("/session/$sessionId/appium/settings", settings)
+    }
 }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt
index 53060c7c..f4e5b487 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt
@@ -1,5 +1,6 @@
 package com.badoo.automation.deviceserver.ios.device
 
+import com.badoo.automation.deviceserver.ApplicationConfiguration
 import com.badoo.automation.deviceserver.LogMarkers
 import com.badoo.automation.deviceserver.WaitTimeoutError
 import com.badoo.automation.deviceserver.data.*
@@ -8,7 +9,10 @@ import com.badoo.automation.deviceserver.ios.DeviceStatus
 import com.badoo.automation.deviceserver.ios.WdaClient
 import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDeviceState
 import com.badoo.automation.deviceserver.ios.proc.WebDriverAgentError
+import com.badoo.automation.deviceserver.ios.simulator.video.MJPEGVideoRecorder
+import com.badoo.automation.deviceserver.ios.simulator.video.VideoRecorder
 import com.badoo.automation.deviceserver.util.executeWithTimeout
+import com.badoo.automation.deviceserver.util.deviceRefFromUDID
 import com.badoo.automation.deviceserver.util.pollFor
 import net.logstash.logback.marker.MapEntriesAppendingMarker
 import org.slf4j.LoggerFactory
@@ -26,7 +30,8 @@ class Device(
     val deviceInfo: DeviceInfo,
     val allocatedPorts: DeviceAllocatedPorts,
     wdaRunnerXctest: File,
-    usbProxy: UsbProxyFactory = UsbProxyFactory(remote)
+    usbProxy: UsbProxyFactory = UsbProxyFactory(remote),
+    configuration: ApplicationConfiguration = ApplicationConfiguration()
 ) {
     val udid: String = deviceInfo.udid
 
@@ -45,6 +50,21 @@ class Device(
     val fbsimctlEndpoint = URI("http://${remote.publicHostName}:${allocatedPorts.fbsimctlPort}/$udid/")
     val wdaEndpoint = URI("http://${remote.publicHostName}:${wdaProxy.localPort}")
     val calabashPort = calabashProxy.localPort
+    val mjpegServerPort = allocatedPorts.mjpegServerPort
+    private val mjpegProxy = usbProxy.create(
+        udid = deviceInfo.udid,
+        localPort = mjpegServerPort,
+        devicePort = mjpegServerPort
+    )
+    val videoRecorder: VideoRecorder = MJPEGVideoRecorder(
+        deviceInfo,
+        remote,
+        wdaEndpoint,
+        mjpegServerPort,
+        configuration.videoRecorderFrameRate,
+        deviceRefFromUDID(deviceInfo.udid, remote.publicHostName),
+        deviceInfo.udid
+    )
 
     @Volatile
     var lastException: Exception? = null
@@ -59,7 +79,7 @@ class Device(
         }
 
     private val fbsimctlProc: DeviceFbsimctlProc = DeviceFbsimctlProc(remote, deviceInfo.udid, fbsimctlEndpoint, false)
-    private val wdaProc = DeviceWebDriverAgent(remote, wdaRunnerXctest, deviceInfo.udid, wdaEndpoint, wdaProxy.devicePort)
+    private val wdaProc = DeviceWebDriverAgent(remote, wdaRunnerXctest, deviceInfo.udid, wdaEndpoint, wdaProxy.devicePort, mjpegServerPort)
 
     private val status = SimulatorStatus()
 
@@ -162,6 +182,7 @@ class Device(
         ignoringDisposeErrors { wdaProc.kill() }
         ignoringDisposeErrors { calabashProxy.stop() }
         ignoringDisposeErrors { wdaProxy.stop() }
+        ignoringDisposeErrors { mjpegProxy.stop() }
     }
 
     private fun ignoringDisposeErrors(action: () -> Unit?) {
@@ -308,6 +329,7 @@ class Device(
         wdaProc.kill()
 
         wdaProxy.stop()
+        mjpegProxy.stop()
         calabashProxy.stop()
 
         executeWithTimeout(timeout, name = "Preparing devices") {
@@ -317,6 +339,12 @@ class Device(
                 throw DeviceException("Failed to start $wdaProxy")
             }
 
+            mjpegProxy.start()
+
+            if (!mjpegProxy.isHealthy()) {
+                throw DeviceException("Failed to start $mjpegProxy")
+            }
+
             calabashProxy.start()
 
             if (!calabashProxy.isHealthy()) {
@@ -399,4 +427,4 @@ class Device(
         private const val WDA_PORT = 8100
         private val PREPARE_TIMEOUT = Duration.ofMinutes(4)
     }
-}
\ No newline at end of file
+}
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceWebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceWebDriverAgent.kt
index b944efc8..4ac1ac78 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceWebDriverAgent.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/DeviceWebDriverAgent.kt
@@ -12,6 +12,7 @@ class DeviceWebDriverAgent(
     udid: UDID,
     wdaEndpoint: URI,
     port: Int,
+    mjpegServerPort: Int,
     hostApp: String = wdaRunnerXctest.parentFile.parentFile.absolutePath
 ) : WebDriverAgent(
     remote = remote,
@@ -19,10 +20,11 @@ class DeviceWebDriverAgent(
     hostApp = hostApp,
     udid = udid,
     wdaEndpoint = wdaEndpoint,
-    port = port
+    port = port,
+    mjpegServerPort = mjpegServerPort
 ) {
     override fun terminateHostApp() {
         remote.fbsimctl.uninstallApp(udid, hostApp)
         Thread.sleep(1000)
     }
-}
\ No newline at end of file
+}
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt
index 771986ba..be0eef20 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt
@@ -11,13 +11,15 @@ class SimulatorWebDriverAgent(
         remote: IRemote,
         wdaRunnerXctest: File,
         udid: UDID,
-        wdaEndpoint: URI
+        wdaEndpoint: URI,
+        mjpegServerPort: Int
 ) : WebDriverAgent(
         remote = remote,
         wdaRunnerXctest = wdaRunnerXctest,
         hostApp = wdaRunnerXctest.parentFile.parentFile.absolutePath,
         udid = udid,
-        wdaEndpoint = wdaEndpoint
+        wdaEndpoint = wdaEndpoint,
+        mjpegServerPort = mjpegServerPort
 ) {
     override fun start() {
         installHostApp()
@@ -49,4 +51,4 @@ class SimulatorWebDriverAgent(
         return remote.fbsimctl.listApps(udid)
             .any { it.bundle.bundle_id.contains("WebDriverAgentRunner-Runner") }
     }
-}
\ No newline at end of file
+}
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt
index 0e1272b2..c6e45cc5 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt
@@ -16,6 +16,7 @@ open class WebDriverAgent(
     protected val udid: UDID,
     private val wdaEndpoint: URI,
     port: Int = wdaEndpoint.port,
+    mjpegServerPort: Int,
     private val childFactory: (
                 remoteHost: String,
                 userName: String,
@@ -33,6 +34,8 @@ open class WebDriverAgent(
             hostApp,
             "--port",
             port.toString(),
+            "--mjpeg-server-port",
+            mjpegServerPort.toString(),
             "--",
             "listen"
     )
@@ -74,4 +77,4 @@ open class WebDriverAgent(
             false
         }
     }
-}
\ No newline at end of file
+}
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt
index 510aff06..86782493 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt
@@ -5,7 +5,7 @@ import com.badoo.automation.deviceserver.ios.simulator.data.DataContainer
 import com.badoo.automation.deviceserver.ios.simulator.data.Media
 import com.badoo.automation.deviceserver.ios.simulator.diagnostic.OsLog
 import com.badoo.automation.deviceserver.ios.simulator.diagnostic.SystemLog
-import com.badoo.automation.deviceserver.ios.simulator.video.SimulatorVideoRecorder
+import com.badoo.automation.deviceserver.ios.simulator.video.VideoRecorder
 import java.net.URI
 import java.net.URL
 
@@ -20,7 +20,8 @@ interface ISimulator {
     val info: DeviceInfo
     val lastError: Exception?
     val calabashPort: Int
-    val videoRecorder: SimulatorVideoRecorder
+    val mjpegServerPort: Int
+    val videoRecorder: VideoRecorder
     val fbsimctlSubject: String
     val systemLog: SystemLog
     val osLog: OsLog
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
index 2425094d..8e54b379 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
@@ -16,7 +16,9 @@ import com.badoo.automation.deviceserver.ios.simulator.data.FileSystem
 import com.badoo.automation.deviceserver.ios.simulator.data.Media
 import com.badoo.automation.deviceserver.ios.simulator.diagnostic.OsLog
 import com.badoo.automation.deviceserver.ios.simulator.diagnostic.SystemLog
+import com.badoo.automation.deviceserver.ios.simulator.video.MJPEGVideoRecorder
 import com.badoo.automation.deviceserver.ios.simulator.video.SimulatorVideoRecorder
+import com.badoo.automation.deviceserver.ios.simulator.video.VideoRecorder
 import com.badoo.automation.deviceserver.util.executeWithTimeout
 import com.badoo.automation.deviceserver.util.pollFor
 import kotlinx.coroutines.experimental.*
@@ -28,7 +30,6 @@ import java.io.FileOutputStream
 import java.io.IOException
 import java.net.URI
 import java.net.URL
-import java.nio.file.Files
 import java.nio.file.Paths
 import java.time.Duration
 import java.util.concurrent.locks.ReentrantLock
@@ -38,7 +39,7 @@ import kotlin.system.measureTimeMillis
 class Simulator (
         private val deviceRef: DeviceRef,
         private val remote: IRemote,
-        deviceInfo: DeviceInfo,
+        private val deviceInfo: DeviceInfo,
         private val allocatedPorts: DeviceAllocatedPorts,
         private val deviceSetPath: String,
         wdaRunnerXctest: File,
@@ -46,8 +47,9 @@ class Simulator (
         headless: Boolean,
         private val useWda: Boolean,
         override val fbsimctlSubject: String,
-        private val trustStoreFile: String = ApplicationConfiguration().trustStorePath,
-        private val assetsPath: String = ApplicationConfiguration().assetsPath
+        private val configuration: ApplicationConfiguration = ApplicationConfiguration(),
+        private val trustStoreFile: String = configuration.trustStorePath,
+        private val assetsPath: String = configuration.assetsPath
 ) : ISimulator
 {
     private companion object {
@@ -63,10 +65,8 @@ class Simulator (
     override val userPorts = allocatedPorts
     override val info = deviceInfo
     override val calabashPort: Int = allocatedPorts.calabashPort
-
-    private val recordingLocation = Paths.get(deviceSetPath, udid, "video.mp4").toFile()
-
-    override val videoRecorder: SimulatorVideoRecorder = SimulatorVideoRecorder(deviceInfo, remote, location = recordingLocation)
+    override val mjpegServerPort: Int = allocatedPorts.mjpegServerPort
+    override val videoRecorder: VideoRecorder = createVideoRecorder()
 
     override val systemLog = SystemLog(remote, udid)
     override val osLog = OsLog(remote, udid)
@@ -78,7 +78,7 @@ class Simulator (
 
     private lateinit var criticalAsyncPromise: Job // 1-1 from ruby
     private val fbsimctlProc: FbsimctlProc = FbsimctlProc(remote, deviceInfo.udid, fbsimctlEndpoint, headless)
-    private val wdaProc = SimulatorWebDriverAgent(remote, wdaRunnerXctest, deviceInfo.udid, wdaEndpoint)
+    private val wdaProc = SimulatorWebDriverAgent(remote, wdaRunnerXctest, deviceInfo.udid, wdaEndpoint, mjpegServerPort)
     private val backup: ISimulatorBackup = SimulatorBackup(remote, udid, deviceSetPath)
     private val logger = LoggerFactory.getLogger(javaClass.simpleName)
     private val commonLogMarkerDetails = mapOf(
@@ -745,4 +745,27 @@ class Simulator (
         }
         remote.shell("xcrun simctl spawn $udid launchctl setenv ${envsArguments.joinToString(" ")}")
     }
+
+    private fun createVideoRecorder(): VideoRecorder {
+        return when (configuration.videoRecorderClassName) {
+            SimulatorVideoRecorder::class.qualifiedName -> SimulatorVideoRecorder(
+                deviceInfo,
+                remote,
+                location = Paths.get(deviceSetPath, udid, "video.mp4").toFile()
+            )
+            MJPEGVideoRecorder::class.qualifiedName -> MJPEGVideoRecorder(
+                deviceInfo,
+                remote,
+                wdaEndpoint,
+                mjpegServerPort,
+                configuration.videoRecorderFrameRate,
+                ref,
+                udid
+            )
+            else -> throw IllegalArgumentException(
+                "Wrong class specified as video recorder: ${configuration.videoRecorderClassName}. " +
+                        "Available are: [${SimulatorVideoRecorder::class.qualifiedName}, ${MJPEGVideoRecorder::class.qualifiedName}]"
+            )
+        }
+    }
 }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/MJPEGVideoRecorder.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/MJPEGVideoRecorder.kt
new file mode 100644
index 00000000..812fa01c
--- /dev/null
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/MJPEGVideoRecorder.kt
@@ -0,0 +1,178 @@
+package com.badoo.automation.deviceserver.ios.simulator.video
+
+import com.badoo.automation.deviceserver.LogMarkers
+import com.badoo.automation.deviceserver.data.DeviceInfo
+import com.badoo.automation.deviceserver.data.DeviceRef
+import com.badoo.automation.deviceserver.data.UDID
+import com.badoo.automation.deviceserver.host.IRemote
+import com.badoo.automation.deviceserver.ios.WdaClient
+import com.badoo.automation.deviceserver.util.CustomHttpClient
+import net.logstash.logback.marker.MapEntriesAppendingMarker
+import okhttp3.Call
+import okhttp3.Request
+import okhttp3.Response
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import java.io.File
+import java.net.URI
+import java.net.URL
+import java.nio.file.Files
+import java.nio.file.StandardCopyOption.REPLACE_EXISTING
+import java.time.Duration
+import java.util.concurrent.Executors
+import java.util.concurrent.Future
+
+class MJPEGVideoRecorder(
+    private val deviceInfo: DeviceInfo,
+    private val remote: IRemote,
+    wdaEndpoint: URI,
+    mjpegServerPort: Int,
+    frameRate: Int,
+    private val ref: DeviceRef,
+    udid: UDID,
+    maxVideoDuration: Duration = Duration.ofMinutes(15),
+    private val videoFile: File = File.createTempFile("videoRecording_${deviceInfo.udid}_", ".mjpeg"),
+    private val encodedVideoFile: File = File.createTempFile("videoRecording_${deviceInfo.udid}_", ".mp4")
+) : VideoRecorder {
+    private val logger: Logger = LoggerFactory.getLogger(javaClass.simpleName)
+    private val logMarker = MapEntriesAppendingMarker(
+        mapOf(
+            LogMarkers.HOSTNAME to remote.publicHostName,
+            LogMarkers.UDID to udid,
+            LogMarkers.DEVICE_REF to ref
+        )
+    )
+    private val uniqueTag = "video-recording-$ref"
+    private val wdaClient = WdaClient(wdaEndpoint.toURL())
+    private val httpClient = CustomHttpClient().client.newBuilder().readTimeout(maxVideoDuration).build()
+    private val mjpegStreamUrl = URL("http://${remote.publicHostName}:${mjpegServerPort}")
+    private val mjpegSettings: Map = mapOf(
+        "settings" to mapOf(
+            "mjpegServerFramerate" to frameRate,
+            "mjpegScalingFactor" to 50,
+            "mjpegServerScreenshotQuality" to 100
+        )
+    )
+    private val ffmpegCommand: List = listOf(
+        "$FFMPEG_PATH",
+        "-hide_banner",
+        "-loglevel", "warning",
+        "-f", "mjpeg",
+        "-framerate", "$frameRate",
+        "-i", videoFile.absolutePath,
+        "-vf", "pad=ceil(iw/2)*2:ceil(ih/2)*2", // might use ffprobe to get resolution as well
+        "-an",
+        "-vcodec", "h264",
+        "-preset", "ultrafast",
+        "-tune", "fastdecode",
+        "-pix_fmt", "yuv420p",
+        "-metadata", "comment=$uniqueTag",
+        "-y",
+        encodedVideoFile.absolutePath
+    )
+    private var videoRecordingTask: Future<*>? = null
+    private var videoRecordingHttpCall: Call? = null
+
+    override fun toString(): String = "${javaClass.simpleName} for $ref"
+
+    override fun start() {
+        cleanupOldRecordings()
+        adjustVideoStreamSettings()
+        logger.debug(logMarker, "Starting video recording")
+
+        val request: Request = Request.Builder()
+            .get()
+            .url(mjpegStreamUrl)
+            .build()
+
+        val httpCall = httpClient.newCall(request)
+        val executor = Executors.newSingleThreadExecutor()
+        val recordingTask = executor.submit(recordStream(httpCall.execute()))
+        executor.shutdown()
+
+        videoRecordingTask = recordingTask
+        videoRecordingHttpCall = httpCall
+
+        logger.debug(logMarker, "Started video recording")
+    }
+
+    override fun stop() {
+        logger.debug(logMarker, "Stopping video recording")
+        stopVideoRecording()
+        logger.debug(logMarker, "Stopped video recording stopped")
+    }
+
+    override fun getRecording(): ByteArray {
+        logger.debug(logMarker, "Getting video recording")
+
+        return try {
+            if (FFMPEG_PATH == null) {
+                logger.error("Failed to find ffmpeg utility. Returning uncompressed video.")
+                videoFile.readBytes()
+            } else {
+                compressedVideo()
+            }
+        } finally {
+            delete()
+        }
+    }
+
+    override fun delete() {
+        logger.debug(logMarker, "Deleting video recording")
+        Files.deleteIfExists(videoFile.toPath())
+        Files.deleteIfExists(encodedVideoFile.toPath())
+    }
+
+    override fun dispose() {
+        logger.debug(logMarker, "Disposing video recording")
+        cleanupOldRecordings()
+        logger.debug(logMarker, "Disposed video recording")
+    }
+
+    private fun adjustVideoStreamSettings() {
+        wdaClient.attachToSession()
+        val response = wdaClient.updateAppiumSettings(mjpegSettings)
+        logger.trace(logMarker, "Updated MJPEG streaming server settings: $response")
+    }
+
+    private fun cleanupOldRecordings() {
+        stopVideoRecording()
+        delete()
+    }
+
+    private fun recordStream(response: Response): Runnable = Runnable {
+        response.use {
+            it.body?.let { responseBody ->
+                responseBody.byteStream().use { inputStream ->
+                    Files.copy(inputStream, videoFile.toPath(), REPLACE_EXISTING)
+                }
+            }
+        }
+    }
+
+    private fun compressedVideo(): ByteArray {
+        val result = remote.localExecutor.exec(ffmpegCommand, timeOut = Duration.ofSeconds(60L))
+
+        if (!result.isSuccess && (!encodedVideoFile.exists() || Files.size(encodedVideoFile.toPath()) == 0L)) {
+            val message = "Could not compress video file. Result stdErr: ${result.stdErr}"
+            logger.error(message)
+            throw SimulatorVideoRecordingException(message)
+        }
+
+        logger.debug(logMarker, "Successfully compressed video recording.")
+        return encodedVideoFile.readBytes()
+    }
+
+    private fun stopVideoRecording() {
+        videoRecordingHttpCall?.cancel()
+        videoRecordingHttpCall = null
+
+        videoRecordingTask?.cancel(true)
+        videoRecordingTask = null
+    }
+
+    private companion object {
+        private val ffmpegBinaries = listOf("/usr/local/bin/ffmpeg", "/usr/bin/ffmpeg")
+        val FFMPEG_PATH = ffmpegBinaries.find { File(it).canExecute() }
+    }
+}
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt
index 1ab07bf0..4f8878ab 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/SimulatorVideoRecorder.kt
@@ -20,7 +20,7 @@ class SimulatorVideoRecorder(
         ) -> ChildProcess? = ChildProcess.Companion::fromCommand,
     private val recorderStopTimeout: Duration = RECORDER_STOP_TIMEOUT,
     location: File
-) : LongRunningProc(deviceInfo.udid, remote.hostName) {
+) : LongRunningProc(deviceInfo.udid, remote.hostName), VideoRecorder {
 
     private val udid = deviceInfo.udid
 
@@ -37,7 +37,7 @@ class SimulatorVideoRecorder(
 
     private val uniqueTag = "video-recording-$udid"
 
-    fun delete() {
+    override fun delete() {
         logger.debug(logMarker, "Deleting video recording")
 
         val result = remote.execIgnoringErrors(listOf("rm", "-f", recordingLocation.toString()))
@@ -70,7 +70,7 @@ class SimulatorVideoRecorder(
 
             childProcess = childFactory(remote.hostName, remote.userName, cmd, mapOf(),
                 { logger.debug(logMarker, "$udid: VideoRecorder : ${it.trim()}") },
-                { logger.debug(logMarker, "$udid: VideoRecorder : ${it.trim()}") }
+                { logger.warn(logMarker, "$udid: VideoRecorder : ${it.trim()}") }
             )
 
             logger.info(logMarker, "Started video recording")
@@ -78,12 +78,12 @@ class SimulatorVideoRecorder(
         }
     }
 
-    fun stop() {
+    override fun stop() {
         lock.withLock {
             if (!isStarted) {
                 val message = "Video recording has not yet started"
                 logger.warn(logMarker, message)
-                throw SimulatorVideoRecordingException(message)
+                return
             }
 
             try {
@@ -108,7 +108,7 @@ class SimulatorVideoRecorder(
         }
     }
 
-    fun getRecording(): ByteArray {
+    override fun getRecording(): ByteArray {
         logger.info(logMarker, "Getting video recording")
 
         val videoFile = recordingLocation
@@ -127,7 +127,7 @@ class SimulatorVideoRecorder(
         }
     }
 
-    fun dispose() {
+    override fun dispose() {
         if (childProcess?.isAlive() != true) {
             return
         }
@@ -188,4 +188,4 @@ class SimulatorVideoRecorder(
         const val FFMPEG_PATH = "/usr/local/bin/ffmpeg"
         val RECORDER_STOP_TIMEOUT: Duration = Duration.ofSeconds(3)
     }
-}
\ No newline at end of file
+}
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/VideoRecorder.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/VideoRecorder.kt
new file mode 100644
index 00000000..7e434506
--- /dev/null
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/video/VideoRecorder.kt
@@ -0,0 +1,9 @@
+package com.badoo.automation.deviceserver.ios.simulator.video
+
+interface VideoRecorder {
+    fun start()
+    fun stop()
+    fun getRecording(): ByteArray
+    fun delete()
+    fun dispose()
+}
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/TestUtil.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/TestUtil.kt
index 1a3c0d5a..d7a66cf4 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/TestUtil.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/TestUtil.kt
@@ -31,8 +31,8 @@ fun deviceDTOStub(ref: DeviceRef): DeviceDTO {
     ref, DeviceState.NONE,
     URI("http://fbsimctl/endpoint/for/testing"),
     URI("http://wda/endpoint/for/testing"),
-    0, setOf(0),
+    0, 1, setOf(0, 1),
     DeviceInfo("", "", "", "", ""),
     Exception().toDto(),
     capabilities = null)
-}
\ No newline at end of file
+}
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt
index f67c80dc..5c9ae8f9 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNodeTest.kt
@@ -81,7 +81,8 @@ class SimulatorsNodeTest {
             URI("http://fbsimctl"),
             URI("http://wda"),
             4444,
-            setOf(1, 2, 3, 37265),
+            5555,
+            setOf(1, 2, 3, 4, 37265),
             DeviceInfo("", "", "", "", ""),
             null,
             ActualCapabilities(true, true, true)
@@ -119,7 +120,7 @@ class SimulatorsNodeTest {
                 eq("Udid1-rem-ote-node"),
                 eq(iRemote),
                 eq(fbsimulatorDevice),
-                eq(DeviceAllocatedPorts(1,2, 3)),
+                eq(DeviceAllocatedPorts(1, 2, 3, 4)),
                 eq("/node/specific/device/set"),
                 eq(File("some/file/from/wdaPathProc")),
                 any(),
@@ -165,10 +166,11 @@ class SimulatorsNodeTest {
             whenever(it.ref).thenReturn("someref$index")
             whenever(it.state).thenReturn(DeviceState.CREATING)
             whenever(it.info).thenReturn(DeviceInfo("","","","",""))
-            whenever(it.userPorts).thenReturn(DeviceAllocatedPorts(1,2,3))
+            whenever(it.userPorts).thenReturn(DeviceAllocatedPorts(1,2,3,4))
             whenever(it.fbsimctlEndpoint).thenReturn(URI("http://fbsimctl"))
             whenever(it.wdaEndpoint).thenReturn(URI("http://wda"))
             whenever(it.calabashPort).thenReturn(4444 + index)
+            whenever(it.mjpegServerPort).thenReturn(5555 + index)
             whenever(it.fbsimctlSubject).thenReturn("string representation of simulatorMock $index")
         }
     }
@@ -275,7 +277,8 @@ class SimulatorsNodeTest {
                 URI("http://fbsimctl"),
                 URI("http://wda"),
                 4444,
-                setOf(1,2,3, 37265),
+                5555,
+                setOf(1,2,3,4,37265),
                 DeviceInfo("", "", "", "", ""),
                 null,
                 ActualCapabilities(true, true, true)
@@ -310,13 +313,13 @@ class SimulatorsNodeTest {
     fun deleteReleaseReleasesExistingRef() {
         createTwoDevicesForTest()
         assertThat(simulatorsNode.count(), equalTo(2))
-        assertThat(portAllocator.available(), equalTo(4))
+        assertThat(portAllocator.available(), equalTo(2))
 
         val actual = simulatorsNode.deleteRelease(ref1, "test")
         assertThat(actual, equalTo(true))
         verify(simulatorMock).release(any())
         assertThat(simulatorsNode.count(), equalTo(1))
-        assertThat(portAllocator.available(), equalTo(7))
+        assertThat(portAllocator.available(), equalTo(6))
     }
 
     @Test

From f2947651280aab57fb333f97c0cd8ea4d1fd29ba Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Mon, 10 Feb 2020 18:38:29 +0000
Subject: [PATCH 124/131] Improve WebDriverAgent start process

---
 .../deviceserver/ApplicationConfiguration.kt  |   5 +
 .../deviceserver/ios/device/Device.kt         |  16 +--
 .../deviceserver/ios/proc/IWebDriverAgent.kt  |   8 ++
 .../ios/proc/SimulatorWebDriverAgent.kt       |  60 +++++---
 .../ios/proc/SimulatorXcrunWebDriverAgent.kt  | 136 ++++++++++++++++++
 .../deviceserver/ios/proc/WebDriverAgent.kt   |  12 +-
 .../deviceserver/ios/simulator/Simulator.kt   |  66 ++++++---
 7 files changed, 257 insertions(+), 46 deletions(-)
 create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/IWebDriverAgent.kt
 create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorXcrunWebDriverAgent.kt

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt
index 2b86a20c..9248da3a 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ApplicationConfiguration.kt
@@ -1,12 +1,15 @@
 package com.badoo.automation.deviceserver
 
+import com.badoo.automation.deviceserver.ios.proc.SimulatorWebDriverAgent
 import com.badoo.automation.deviceserver.ios.simulator.video.SimulatorVideoRecorder
+import java.lang.Boolean.getBoolean
 
 class ApplicationConfiguration {
     private val wdaSimulatorBundlePathProperty = "wda.bundle.path"
     val wdaSimulatorBundlePath: String = System.getProperty(wdaSimulatorBundlePathProperty)
             ?: throw RuntimeException("Must set system property: -D$wdaSimulatorBundlePathProperty=" +
                     "/ABSOLUTE/PATH/ios/facebook/simulators/WebDriverAgentRunner-Runner.app")
+    val wdaSimulatorBundleId: String = System.getProperty("wda.bundle.id", "com.facebook.WebDriverAgentRunner.xctrunner")
 
     private val wdaDeviceBundlePathProperty = "wda.device.bundle.path"
     val wdaDeviceBundlePath: String = System.getProperty(wdaDeviceBundlePathProperty)
@@ -27,4 +30,6 @@ class ApplicationConfiguration {
     val assetsPath: String = System.getProperty("media.assets.path", "")
     val videoRecorderClassName = System.getProperty("video.recorder", SimulatorVideoRecorder::class.qualifiedName)
     val videoRecorderFrameRate = Integer.getInteger("video.recorder.frame.rate", 4)
+    val simulatorWdaClassName = System.getProperty("simulator.wda.class", SimulatorWebDriverAgent::class.qualifiedName)
+    val shouldPreinstallWDA: Boolean = getBoolean("preinstall.simulator.wda")
 }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt
index f4e5b487..a93c1d6b 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/device/Device.kt
@@ -79,7 +79,7 @@ class Device(
         }
 
     private val fbsimctlProc: DeviceFbsimctlProc = DeviceFbsimctlProc(remote, deviceInfo.udid, fbsimctlEndpoint, false)
-    private val wdaProc = DeviceWebDriverAgent(remote, wdaRunnerXctest, deviceInfo.udid, wdaEndpoint, wdaProxy.devicePort, mjpegServerPort)
+    private val webDriverAgent = DeviceWebDriverAgent(remote, wdaRunnerXctest, deviceInfo.udid, wdaEndpoint, wdaProxy.devicePort, mjpegServerPort)
 
     private val status = SimulatorStatus()
 
@@ -131,7 +131,7 @@ class Device(
             return
         }
 
-        val wdaStatus = wdaProc.isHealthy()
+        val wdaStatus = webDriverAgent.isHealthy()
         val fbsimctlStatus = fbsimctlProc.isHealthy()
 
         // check if WDA or fbsimctl crashed after being ok for some time
@@ -179,7 +179,7 @@ class Device(
 
     private fun disposeResources() {
         ignoringDisposeErrors { fbsimctlProc.kill() }
-        ignoringDisposeErrors { wdaProc.kill() }
+        ignoringDisposeErrors { webDriverAgent.stop() }
         ignoringDisposeErrors { calabashProxy.stop() }
         ignoringDisposeErrors { wdaProxy.stop() }
         ignoringDisposeErrors { mjpegProxy.stop() }
@@ -326,7 +326,7 @@ class Device(
         logger.info(logMarker, "Starting to prepare $this")
 
         fbsimctlProc.kill()
-        wdaProc.kill()
+        webDriverAgent.stop()
 
         wdaProxy.stop()
         mjpegProxy.stop()
@@ -378,8 +378,8 @@ class Device(
     }
 
     private fun startWda() {
-        wdaProc.kill()
-        wdaProc.start()
+        webDriverAgent.stop()
+        webDriverAgent.start()
 
         pollFor(
             Duration.ofMinutes(1),
@@ -388,8 +388,8 @@ class Device(
             logger = logger,
             marker = logMarker
         ) {
-            if (wdaProc.isProcessAlive) {
-                wdaProc.isHealthy()
+            if (webDriverAgent.isProcessAlive) {
+                webDriverAgent.isHealthy()
             } else {
                 throw WaitTimeoutError("WebDriverAgent process is not alive")
             }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/IWebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/IWebDriverAgent.kt
new file mode 100644
index 00000000..1b182a72
--- /dev/null
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/IWebDriverAgent.kt
@@ -0,0 +1,8 @@
+package com.badoo.automation.deviceserver.ios.proc
+
+interface IWebDriverAgent {
+    fun start()
+    fun stop()
+    fun isHealthy(): Boolean
+    fun installHostApp()
+}
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt
index be0eef20..e5b2b8ec 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorWebDriverAgent.kt
@@ -1,18 +1,24 @@
 package com.badoo.automation.deviceserver.ios.proc
 
+import com.badoo.automation.deviceserver.LogMarkers
+import com.badoo.automation.deviceserver.data.DeviceRef
 import com.badoo.automation.deviceserver.data.UDID
 import com.badoo.automation.deviceserver.host.IRemote
 import com.badoo.automation.deviceserver.util.pollFor
+import net.logstash.logback.marker.MapEntriesAppendingMarker
 import java.io.File
 import java.net.URI
 import java.time.Duration
+import java.util.concurrent.TimeUnit
+import kotlin.system.measureNanoTime
 
 class SimulatorWebDriverAgent(
         remote: IRemote,
         wdaRunnerXctest: File,
         udid: UDID,
         wdaEndpoint: URI,
-        mjpegServerPort: Int
+        mjpegServerPort: Int,
+        deviceRef: DeviceRef
 ) : WebDriverAgent(
         remote = remote,
         wdaRunnerXctest = wdaRunnerXctest,
@@ -21,30 +27,50 @@ class SimulatorWebDriverAgent(
         wdaEndpoint = wdaEndpoint,
         mjpegServerPort = mjpegServerPort
 ) {
+    private val commonLogMarkerDetails = mapOf(
+        LogMarkers.DEVICE_REF to deviceRef,
+        LogMarkers.UDID to udid,
+        LogMarkers.HOSTNAME to remote.hostName
+    )
+
     override fun start() {
         installHostApp()
         super.start()
     }
 
-    private fun installHostApp() {
-        val result = remote.execIgnoringErrors(listOf("xcrun", "simctl", "install", udid, hostApp))
+    override fun installHostApp() {
+        logger.debug(logMarker, "Installing WDA on Simulator with xcrun simctl")
 
-        if (!result.isSuccess) {
-            val errorMessage = "Failed to install WebDriverAgent $hostApp to simulator $udid. Result: $result"
-            logger.error(logMarker, errorMessage)
-            throw RuntimeException(errorMessage)
-        }
+        val nanos = measureNanoTime {
+            val result = remote.execIgnoringErrors(listOf("xcrun", "simctl", "install", udid, hostApp))
+
+            if (!result.isSuccess) {
+                val errorMessage = "Failed to install WebDriverAgent $hostApp to simulator $udid. Result: $result"
+                logger.error(logMarker, errorMessage)
+                throw RuntimeException(errorMessage)
+            }
+
+            pollFor(
+                Duration.ofSeconds(30),
+                "Installing WDA host application $hostApp",
+                true,
+                Duration.ofSeconds(2),
+                logger,
+                logMarker
+            ) {
+                isHostAppInstalled()
+            }
 
-        pollFor(
-            Duration.ofSeconds(20),
-            "Installing WDA host application $hostApp",
-            true,
-            Duration.ofSeconds(2),
-            logger,
-            logMarker
-        ) {
-            isHostAppInstalled()
         }
+
+        val seconds = TimeUnit.NANOSECONDS.toSeconds(nanos)
+        val measurement = mutableMapOf(
+            "action_name" to "install_WDA",
+            "duration" to seconds
+        )
+        measurement.putAll(commonLogMarkerDetails)
+
+        logger.debug(MapEntriesAppendingMarker(measurement), "Successfully installed WDA on Simulator with xcrun simctl. Took $seconds seconds")
     }
 
     private fun isHostAppInstalled(): Boolean {
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorXcrunWebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorXcrunWebDriverAgent.kt
new file mode 100644
index 00000000..cdf4c87a
--- /dev/null
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/SimulatorXcrunWebDriverAgent.kt
@@ -0,0 +1,136 @@
+package com.badoo.automation.deviceserver.ios.proc
+
+import com.badoo.automation.deviceserver.ApplicationConfiguration
+import com.badoo.automation.deviceserver.LogMarkers
+import com.badoo.automation.deviceserver.data.DeviceRef
+import com.badoo.automation.deviceserver.data.UDID
+import com.badoo.automation.deviceserver.host.IRemote
+import com.badoo.automation.deviceserver.util.CustomHttpClient
+import com.badoo.automation.deviceserver.util.pollFor
+import com.badoo.automation.deviceserver.util.uriWithPath
+import net.logstash.logback.marker.MapEntriesAppendingMarker
+import org.slf4j.Logger
+import org.slf4j.LoggerFactory
+import java.io.File
+import java.lang.RuntimeException
+import java.net.URI
+import java.time.Duration
+import java.util.concurrent.TimeUnit
+import kotlin.system.measureNanoTime
+
+class SimulatorXcrunWebDriverAgent(
+    private val remote: IRemote,
+    wdaRunnerXctest: File,
+    private val udid: UDID,
+    private val wdaEndpoint: URI,
+    private val mjpegServerPort: Int,
+    deviceRef: DeviceRef,
+    applicationConfiguration: ApplicationConfiguration = ApplicationConfiguration()
+) : IWebDriverAgent {
+    private val logger: Logger = LoggerFactory.getLogger(javaClass.simpleName)
+    private val logMarker = MapEntriesAppendingMarker(mapOf(
+        LogMarkers.HOSTNAME to remote.publicHostName,
+        LogMarkers.UDID to udid
+    ))
+    private val commonLogMarkerDetails = mapOf(
+        LogMarkers.DEVICE_REF to deviceRef,
+        LogMarkers.UDID to udid,
+        LogMarkers.HOSTNAME to remote.hostName
+    )
+    private val hostApp = wdaRunnerXctest.parentFile.parentFile.absolutePath
+    private val wdaBundleId = applicationConfiguration.wdaSimulatorBundleId
+    private val uri: URI = uriWithPath(wdaEndpoint, "status")
+    private val client: CustomHttpClient = CustomHttpClient()
+
+    override fun isHealthy(): Boolean {
+        return try {
+            val result = client.get(uri.toURL())
+            result.isSuccess
+        } catch (e: RuntimeException) {
+            logger.warn(logMarker, "Failed to determine WDA driver state. Exception: $e")
+            false
+        }
+    }
+
+    override fun stop() {
+        remote.execIgnoringErrors(
+            listOf(
+                "/usr/bin/xcrun",
+                "simctl",
+                "terminate",
+                udid,
+                wdaBundleId
+            )
+        )
+    }
+
+    override fun start() {
+        if (!isHostAppInstalled()) {
+            installHostApp()
+        }
+
+        val cmd = listOf(
+            "/usr/bin/xcrun",
+            "simctl",
+            "launch",
+            udid,
+            wdaBundleId,
+            "--port", wdaEndpoint.port.toString(),
+            "--mjpeg-server-port", mjpegServerPort.toString()
+        )
+
+        val result = remote.execIgnoringErrors(cmd)
+
+        if (!result.isSuccess) {
+            val message = "Failed to start WDA on Simulator with xcrun simctl. Stderr: ${result.stdErr}"
+            logger.error(logMarker, message)
+            throw RuntimeException(message)
+        }
+    }
+
+    override fun installHostApp() {
+        logger.debug(logMarker, "Installing WDA on Simulator with xcrun simctl")
+
+        val nanos = measureNanoTime {
+            val result = remote.execIgnoringErrors(listOf("xcrun", "simctl", "install", udid, hostApp))
+
+            if (!result.isSuccess) {
+                val errorMessage = "Failed to install WebDriverAgent $hostApp to simulator $udid. Result: $result"
+                logger.error(logMarker, errorMessage)
+                throw RuntimeException(errorMessage)
+            }
+
+            pollFor(
+                Duration.ofSeconds(30),
+                "Installing WDA host application $hostApp",
+                true,
+                Duration.ofSeconds(5),
+                logger,
+                logMarker
+            ) {
+                isHostAppInstalled()
+            }
+        }
+
+        val seconds = TimeUnit.NANOSECONDS.toSeconds(nanos)
+        val measurement = mutableMapOf(
+            "action_name" to "install_WDA",
+            "duration" to seconds
+        )
+        measurement.putAll(commonLogMarkerDetails)
+
+        logger.debug(MapEntriesAppendingMarker(measurement), "Successfully installed WDA on Simulator with xcrun simctl. Took $seconds seconds")
+    }
+
+    private fun isHostAppInstalled(): Boolean {
+        val result = remote.execIgnoringErrors(listOf(
+            "/usr/bin/xcrun",
+            "simctl",
+            "get_app_container",
+            udid,
+            wdaBundleId
+        ))
+
+        return result.isSuccess
+    }
+}
\ No newline at end of file
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt
index c6e45cc5..a4586b57 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/proc/WebDriverAgent.kt
@@ -25,7 +25,7 @@ open class WebDriverAgent(
                 out_reader: ((line: String) -> Unit)?,
                 err_reader: ((line: String) -> Unit)?
         ) -> ChildProcess = ChildProcess.Companion::fromCommand
-) : LongRunningProc(udid, remote.hostName) {
+) : LongRunningProc(udid, remote.hostName), IWebDriverAgent {
     private val launchXctestCommand: List = listOf(
             FBSimctl.FBSIMCTL_BIN,
             udid,
@@ -43,6 +43,10 @@ open class WebDriverAgent(
 
     override fun toString(): String = "<$udid at ${remote.hostName}:${wdaEndpoint.port}>"
 
+    override fun installHostApp() {
+        remote.fbsimctl.installApp(udid, wdaRunnerXctest)
+    }
+
     override fun start() {
         ensure(childProcess == null) { WebDriverAgentError("Previous WebDriverAgent childProcess $childProcess has not been killed") }
         ensure(remote.isDirectory(wdaRunnerXctest.absolutePath)) { WebDriverAgentError("WebDriverAgent ${wdaRunnerXctest.absolutePath} does not exist or is not a directory") }
@@ -56,13 +60,17 @@ open class WebDriverAgent(
                 launchXctestCommand,
                 mapOf(),
                 null,
-                { message -> logger.debug(logMarker, "${this@WebDriverAgent}: WDA : ${message.trim()}") }
+                { message -> logger.warn(logMarker, "${this@WebDriverAgent}: WDA : ${message.trim()}") }
         )
 
         Thread.sleep(5000) // 5 should be ok
         logger.debug(logMarker, "$this WDA: $childProcess")
     }
 
+    override fun stop() {
+        kill()
+    }
+
     protected open fun terminateHostApp() {
         remote.fbsimctl.terminateApp(udid, bundleId = hostApp, raiseOnError = false)
         Thread.sleep(1000)
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
index 8e54b379..b5124a65 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
@@ -8,7 +8,9 @@ import com.badoo.automation.deviceserver.data.*
 import com.badoo.automation.deviceserver.host.IRemote
 import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlDeviceState
 import com.badoo.automation.deviceserver.ios.proc.FbsimctlProc
+import com.badoo.automation.deviceserver.ios.proc.IWebDriverAgent
 import com.badoo.automation.deviceserver.ios.proc.SimulatorWebDriverAgent
+import com.badoo.automation.deviceserver.ios.proc.SimulatorXcrunWebDriverAgent
 import com.badoo.automation.deviceserver.ios.simulator.backup.ISimulatorBackup
 import com.badoo.automation.deviceserver.ios.simulator.backup.SimulatorBackup
 import com.badoo.automation.deviceserver.ios.simulator.data.DataContainer
@@ -33,6 +35,7 @@ import java.net.URL
 import java.nio.file.Paths
 import java.time.Duration
 import java.util.concurrent.locks.ReentrantLock
+import javax.net.ssl.HostnameVerifier
 import kotlin.concurrent.withLock
 import kotlin.system.measureTimeMillis
 
@@ -42,7 +45,7 @@ class Simulator (
         private val deviceInfo: DeviceInfo,
         private val allocatedPorts: DeviceAllocatedPorts,
         private val deviceSetPath: String,
-        wdaRunnerXctest: File,
+        private val wdaRunnerXctest: File,
         private val concurrentBootsPool: ThreadPoolDispatcher,
         headless: Boolean,
         private val useWda: Boolean,
@@ -67,7 +70,6 @@ class Simulator (
     override val calabashPort: Int = allocatedPorts.calabashPort
     override val mjpegServerPort: Int = allocatedPorts.mjpegServerPort
     override val videoRecorder: VideoRecorder = createVideoRecorder()
-
     override val systemLog = SystemLog(remote, udid)
     override val osLog = OsLog(remote, udid)
 
@@ -78,7 +80,7 @@ class Simulator (
 
     private lateinit var criticalAsyncPromise: Job // 1-1 from ruby
     private val fbsimctlProc: FbsimctlProc = FbsimctlProc(remote, deviceInfo.udid, fbsimctlEndpoint, headless)
-    private val wdaProc = SimulatorWebDriverAgent(remote, wdaRunnerXctest, deviceInfo.udid, wdaEndpoint, mjpegServerPort)
+    private val webDriverAgent: IWebDriverAgent = createWebDriverAgent()
     private val backup: ISimulatorBackup = SimulatorBackup(remote, udid, deviceSetPath)
     private val logger = LoggerFactory.getLogger(javaClass.simpleName)
     private val commonLogMarkerDetails = mapOf(
@@ -127,7 +129,7 @@ class Simulator (
     private fun prepare(timeout: Duration = PREPARE_TIMEOUT, clean: Boolean) {
         logger.info(logMarker, "Starting to prepare ${this@Simulator}. Will wait for ${timeout.seconds} seconds")
         lastException = null
-        wdaProc.kill()
+        webDriverAgent.stop()
         shutdown()
 
         //FIXME: add checks for cancellation of criticalAsyncPromise
@@ -178,7 +180,7 @@ class Simulator (
                     }
                 }
 
-                if (wdaProc.isHealthy()) {
+                if (webDriverAgent.isHealthy()) {
                     wdaFailCount = 0
                 } else {
                     wdaFailCount += 1
@@ -207,8 +209,10 @@ class Simulator (
             try {
                 logger.info(logMarker, "Starting WebDriverAgent on ${this@Simulator}")
 
-                wdaProc.kill()
-                wdaProc.start()
+                webDriverAgent.stop()
+                webDriverAgent.start()
+
+                Thread.sleep(1000)
 
                 pollFor(
                     pollTimeout,
@@ -217,15 +221,12 @@ class Simulator (
                     logger = logger,
                     marker = logMarker
                 ) {
-                    //FIXME: add short_circuit: and throw if wdaProc.childProcess is dead
-                    if (wdaProc.isProcessAlive) {
-                        wdaProc.isHealthy()
-                    } else {
-                        throw WaitTimeoutError("WebDriverAgent process is not alive")
-                    }
+                    webDriverAgent.isHealthy()
                 }
 
-                break
+                logger.info(logMarker, "Started WebDriverAgent on ${this@Simulator}")
+
+                return
             }
             catch (e: RuntimeException) {
                 logger.warn(logMarker, "Attempt $attempt to start WebDriverAgent for ${this@Simulator} failed: $e")
@@ -234,8 +235,6 @@ class Simulator (
                 }
             }
         }
-
-        logger.info(logMarker, "Started WebDriverAgent on ${this@Simulator}")
     }
 
     private fun eraseSimulatorAndCreateBackup() {
@@ -253,6 +252,10 @@ class Simulator (
             copyMediaAssets()
         }
 
+        if (useWda && configuration.shouldPreinstallWDA) {
+            webDriverAgent.installHostApp()
+        }
+
         logger.info(logMarker, "Shutting down ${this@Simulator} before creating a backup")
         shutdown()
 
@@ -267,7 +270,7 @@ class Simulator (
         if (remote.isLocalhost()) {
             remote.shell("cp $trustStoreFile $keyChainLocation", returnOnFailure = false)
         } else {
-            remote.rsync(trustStoreFile, keyChainLocation, setOf("--delete"))
+            remote.scpToRemoteHost(trustStoreFile, keyChainLocation, Duration.ofMinutes(1))
         }
 
         logger.info(logMarker, "Copied trust store to ${this@Simulator}")
@@ -500,7 +503,7 @@ class Simulator (
 
         if (deviceState == DeviceState.CREATED) {
             isFbsimctlReady = fbsimctlProc.isHealthy()
-            isWdaReady = (if (useWda) { wdaProc.isHealthy() } else true)
+            isWdaReady = (if (useWda) { webDriverAgent.isHealthy() } else true)
         }
 
         val isSimulatorReady = deviceState == DeviceState.CREATED && isFbsimctlReady && isWdaReady
@@ -580,7 +583,7 @@ class Simulator (
 
     private fun disposeResources() {
         ignoringErrors({ videoRecorder.dispose() })
-        ignoringErrors({ wdaProc.kill() })
+        ignoringErrors({ webDriverAgent.stop() })
     }
 
     private fun ignoringErrors(action: () -> Unit?) {
@@ -768,4 +771,29 @@ class Simulator (
             )
         }
     }
+
+    private fun createWebDriverAgent(): IWebDriverAgent {
+        return when (configuration.simulatorWdaClassName) {
+            SimulatorWebDriverAgent::class.qualifiedName -> SimulatorWebDriverAgent(
+                remote,
+                wdaRunnerXctest,
+                udid,
+                wdaEndpoint,
+                mjpegServerPort,
+                deviceRef
+            )
+            SimulatorXcrunWebDriverAgent::class.qualifiedName -> SimulatorXcrunWebDriverAgent(
+                remote,
+                wdaRunnerXctest,
+                udid,
+                wdaEndpoint,
+                mjpegServerPort,
+                deviceRef
+            )
+            else -> throw IllegalArgumentException(
+                "Wrong class specified as Simulator WebDriverAgent: $configuration.simulatorWdaClassName. " +
+                        "Available are: [${SimulatorWebDriverAgent::class.qualifiedName}, ${SimulatorXcrunWebDriverAgent::class.qualifiedName}]"
+            )
+        }
+    }
 }

From 276f613f6558916a8c51d6310b14a97bc7c035a7 Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Thu, 13 Feb 2020 15:42:49 +0000
Subject: [PATCH 125/131] Add logs to find Simulator process and refactor a bit

---
 .../deviceserver/ios/simulator/Simulator.kt   |  2 +-
 .../ios/simulator/SimulatorProcess.kt         | 79 ++++++++++++-------
 .../ios/simulator/SimulatorProcessTest.kt     | 15 ++--
 3 files changed, 61 insertions(+), 35 deletions(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
index b5124a65..78155fb7 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
@@ -90,7 +90,7 @@ class Simulator (
     )
     private val logMarker: Marker = MapEntriesAppendingMarker(commonLogMarkerDetails)
     private val fileSystem = FileSystem(remote, udid)
-    private val simulatorProcess = SimulatorProcess(remote, udid)
+    private val simulatorProcess = SimulatorProcess(remote, udid, deviceRef)
     @Volatile private var healthChecker: Job? = null
     //endregion
 
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcess.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcess.kt
index 5540d425..edef768c 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcess.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcess.kt
@@ -1,40 +1,63 @@
 package com.badoo.automation.deviceserver.ios.simulator
 
+import com.badoo.automation.deviceserver.LogMarkers
+import com.badoo.automation.deviceserver.data.DeviceRef
 import com.badoo.automation.deviceserver.data.UDID
 import com.badoo.automation.deviceserver.host.IRemote
+import net.logstash.logback.marker.MapEntriesAppendingMarker
+import org.slf4j.LoggerFactory
+import org.slf4j.Marker
+import java.util.*
 
 class SimulatorProcess(
     private val remote: IRemote,
-    private val udid: UDID
+    private val udid: UDID,
+    private val deviceRef: DeviceRef
 ) {
-    val mainProcessPid: Int
-        get() {
-            val command = listOf("/usr/bin/pgrep", "-fl", "launchd_sim")
-            val result = remote.execIgnoringErrors(command)
-
-            check(result.isSuccess) {
-                "No launchd_sim process is found for simulator with udid: $udid. Result: $result"
-            }
-
-            val processList = result
-                .stdOut
-                .lines()
-                .filter { it.contains(udid) }
-
-            check(processList.isNotEmpty()) {
-                "No launchd_sim process is found for simulator with udid: $udid. Result: $result"
-            }
-
-            return processList
-                .first()
-                .split(" ")
-                .first()
-                .toInt()
-        }
+    private val logger = LoggerFactory.getLogger(javaClass.simpleName)
+    private val commonLogMarkerDetails = mapOf(
+        LogMarkers.DEVICE_REF to deviceRef,
+        LogMarkers.UDID to udid,
+        LogMarkers.HOSTNAME to remote.publicHostName
+    )
+    private val logMarker: Marker = MapEntriesAppendingMarker(commonLogMarkerDetails)
 
     fun terminateChildProcess(processName: String) {
-        // Sends SIGKILL to process with parent pid $mainProcessPid and name $processName
-        val command = listOf("/usr/bin/pkill", "-9", "-P", "$mainProcessPid", "-f", processName)
-        remote.execIgnoringErrors(command)
+        val mainProcessPid = getSimulatorMainProcessPid()
+
+        // Sends SIGKILL to all processes that:
+        // 1. have parent pid $mainProcessPid
+        // 2. and full command line contains substring $processName
+
+        remote.execIgnoringErrors(listOf("/usr/bin/pkill", "-9", "-P", "$mainProcessPid", "-f", processName))
+    }
+
+    fun getSimulatorMainProcessPid(): Int {
+        val result = remote.execIgnoringErrors(listOf("/usr/bin/pgrep", "-fl", "launchd_sim"))
+
+        if (result.isSuccess) {
+            return parseSimulatorPid(result.stdOut)
+        }
+
+        val errorMessage = if (result.exitCode == 1) {
+            "No launchd_sim processes found on ${remote.publicHostName}. Result: $result"
+        } else {
+            "Failed to get process list for simulators at $deviceRef. StdErr: ${result.stdErr}"
+        }
+
+        logger.error(logMarker, errorMessage)
+        throw IllegalStateException(errorMessage)
+    }
+
+    private fun parseSimulatorPid(result: String): Int {
+        val simulatorProcess = result.lineSequence().firstOrNull { it.contains(udid) && !it.contains("pgrep") }
+
+        if (simulatorProcess != null) {
+            return Scanner(simulatorProcess).nextInt()
+        }
+
+        val errorMessage = "No launchd_sim process for $udid found on ${remote.publicHostName}. Result: $result"
+        logger.error(logMarker, errorMessage)
+        throw IllegalStateException(errorMessage)
     }
 }
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcessTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcessTest.kt
index 02fb09e3..516ca8a4 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcessTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/SimulatorProcessTest.kt
@@ -1,16 +1,19 @@
 package com.badoo.automation.deviceserver.ios.simulator
 
 import com.badoo.automation.deviceserver.command.CommandResult
+import com.badoo.automation.deviceserver.data.DeviceRef
 import com.badoo.automation.deviceserver.data.UDID
 import com.badoo.automation.deviceserver.host.IRemote
 import com.badoo.automation.deviceserver.mockThis
 import com.nhaarman.mockito_kotlin.*
 import org.junit.Assert.assertEquals
 import org.junit.Test
+import java.lang.IllegalStateException
 import kotlin.test.assertFailsWith
 
 class SimulatorProcessTest {
     private val udid: UDID = "ADB25768-5C9D-487E-A787-D271934B78B0"
+    private val deviceRef: DeviceRef = "asdf"
     private val remote = mockThis()
     private val stdOutWithSimulatorPid = """
         41757 launchd_sim /Users/z/Library/Developer/CoreSimulator/Devices/ADB25768-5C9D-487E-A787-D271934B78B0/data/var/run/launchd_bootstrap.plist
@@ -19,31 +22,31 @@ class SimulatorProcessTest {
 
     @Test
     fun testSimulatorProcessFound() {
-        val simulatorProcess = SimulatorProcess(remote, udid)
+        val simulatorProcess = SimulatorProcess(remote, udid, deviceRef)
         val simulatorFoundCommandResult = CommandResult(stdOutWithSimulatorPid, "", 0, pid = 1)
 
         whenever(remote.execIgnoringErrors(any(), any(), any()))
             .thenReturn(simulatorFoundCommandResult)
 
-        assertEquals(41757, simulatorProcess.mainProcessPid)
+        assertEquals(41757, simulatorProcess.getSimulatorMainProcessPid())
     }
 
     @Test
     fun testSimulatorProcessNotFound() {
-        val simulatorProcess = SimulatorProcess(remote, udid)
-        val noSimulatorFoundCommandResult = CommandResult("", "", 0, pid = 1)
+        val simulatorProcess = SimulatorProcess(remote, udid, deviceRef)
+        val noSimulatorFoundCommandResult = CommandResult("", "", 1, pid = 1)
 
         whenever(remote.execIgnoringErrors(any(), any(), any()))
             .thenReturn(noSimulatorFoundCommandResult)
 
         assertFailsWith {
-            simulatorProcess.mainProcessPid
+            simulatorProcess.getSimulatorMainProcessPid()
         }
     }
 
     @Test
     fun testKillSimulatorProcess() {
-        val simulatorProcess = SimulatorProcess(remote, udid)
+        val simulatorProcess = SimulatorProcess(remote, udid, deviceRef)
 
         whenever(remote.execIgnoringErrors(any(), any(), any()))
             .thenReturn(CommandResult(stdOutWithSimulatorPid, "", 0, pid = 1))

From e42562ab199b6e2696b2dcea5fcbefb396a81fcf Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Fri, 14 Feb 2020 13:06:22 +0000
Subject: [PATCH 126/131] Delete auto-release logic

---
 .../automation/deviceserver/DeviceServer.kt   |  1 -
 .../host/management/DeviceManager.kt          | 35 +-------
 .../host/management/IAutoreleaseLooper.kt     |  5 --
 .../host/management/NodeRegistrar.kt          |  5 +-
 .../host/management/NodeRegistry.kt           |  6 +-
 .../host/management/NodeRestarter.kt          | 36 +-------
 .../host/management/util/AutoreleaseLooper.kt | 54 ------------
 .../deviceserver/ios/ActiveDevices.kt         | 40 +--------
 .../deviceserver/host/NodeRegistryTest.kt     |  9 +-
 .../deviceserver/ios/ActiveDevicesTest.kt     |  8 +-
 .../deviceserver/ios/DeviceManagerTest.kt     | 86 ++++++++-----------
 .../deviceserver/ios/SessionTest.kt           | 47 +++-------
 12 files changed, 70 insertions(+), 262 deletions(-)
 delete mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IAutoreleaseLooper.kt
 delete mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/util/AutoreleaseLooper.kt

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
index a1054aff..15b2519c 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
@@ -107,7 +107,6 @@ fun Application.module() {
     )
     val deviceManager = DeviceManager(config, hostFactory)
     deviceManager.startAutoRegisteringDevices()
-    deviceManager.launchAutoReleaseLoop()
 
     zombieReaper.launchReapingZombies()
 
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
index e3a76965..e5f5d41c 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
@@ -4,22 +4,16 @@ import com.badoo.automation.deviceserver.DeviceServerConfig
 import com.badoo.automation.deviceserver.data.*
 import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException
 import com.badoo.automation.deviceserver.host.management.errors.NoNodesRegisteredException
-import com.badoo.automation.deviceserver.host.management.util.AutoreleaseLooper
 import com.badoo.automation.deviceserver.ios.ActiveDevices
 import org.slf4j.LoggerFactory
 import java.net.URL
-import java.time.Duration
-
-private val INFINITE_DEVICE_TIMEOUT: Duration = Duration.ofSeconds(Integer.MAX_VALUE.toLong())
 
 class DeviceManager(
         config: DeviceServerConfig,
         nodeFactory: IHostFactory,
-        activeDevices: ActiveDevices = ActiveDevices(),
-        private val autoreleaseLooper: IAutoreleaseLooper = AutoreleaseLooper()
+        activeDevices: ActiveDevices = ActiveDevices()
 ) {
     private val logger = LoggerFactory.getLogger(javaClass.simpleName)
-    private val deviceTimeoutInSecs: Duration
     private val nodeRegistry = NodeRegistry(activeDevices)
     private val autoRegistrar = NodeRegistrar(
             nodesConfig = config.nodes,
@@ -27,27 +21,12 @@ class DeviceManager(
             nodeRegistry = nodeRegistry
     )
 
-    init {
-        val timeoutFromConfig: Long? = config.timeouts["device"]?.toLong()
-
-        deviceTimeoutInSecs =
-                if (timeoutFromConfig != null && timeoutFromConfig > 0) {
-                    Duration.ofSeconds(timeoutFromConfig)
-                } else {
-                    INFINITE_DEVICE_TIMEOUT
-                }
-    }
-
     fun startAutoRegisteringDevices() {
         autoRegistrar.startAutoRegistering()
     }
 
     fun restartNodesGracefully(isParallelRestart: Boolean): Boolean {
-        return autoRegistrar.restartNodesGracefully(isParallelRestart, INFINITE_DEVICE_TIMEOUT)
-    }
-
-    fun launchAutoReleaseLoop() {
-        autoreleaseLooper.autoreleaseLoop(this)
+        return autoRegistrar.restartNodesGracefully(isParallelRestart)
     }
 
     fun getStatus(): Map {
@@ -57,14 +36,6 @@ class DeviceManager(
         )
     }
 
-    fun readyForRelease(): List {
-        return nodeRegistry.activeDevices.readyForRelease()
-    }
-
-    fun nextReleaseAtSeconds(): Long {
-        return nodeRegistry.activeDevices.nextReleaseAtSeconds()
-    }
-
     fun getTotalCapacity(desiredCaps: DesiredCapabilities): Map {
         return nodeRegistry.capacitiesTotal(desiredCaps)
     }
@@ -144,7 +115,7 @@ class DeviceManager(
 
     fun createDeviceAsync(desiredCaps: DesiredCapabilities, userId: String?): DeviceDTO {
         try {
-            return nodeRegistry.createDeviceAsync(desiredCaps, deviceTimeoutInSecs, userId)
+            return nodeRegistry.createDeviceAsync(desiredCaps, userId)
         } catch(e: NoNodesRegisteredException) {
             val erredNodes = autoRegistrar.nodeWrappers.filter { n -> n.lastError != null }
             val errors = erredNodes.joinToString { n -> "${n.node.remoteAddress} -> ${n.lastError?.localizedMessage}" }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IAutoreleaseLooper.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IAutoreleaseLooper.kt
deleted file mode 100644
index 7c20a046..00000000
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/IAutoreleaseLooper.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package com.badoo.automation.deviceserver.host.management
-
-interface IAutoreleaseLooper {
-    fun autoreleaseLoop(deviceManager: DeviceManager)
-}
\ No newline at end of file
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt
index 7b612477..cd31b494 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistrar.kt
@@ -74,7 +74,7 @@ class NodeRegistrar(
     }
 
     @Synchronized
-    fun restartNodesGracefully(isParallel: Boolean, infiniteDeviceTimeout: Duration): Boolean {
+    fun restartNodesGracefully(isParallel: Boolean): Boolean {
         val job = restartingJob
 
         if (job == null || job.isDone) {
@@ -82,8 +82,7 @@ class NodeRegistrar(
             restartingJob = executor.submit {
                 nodeRestarter.restartNodeWrappers(
                     nodeRegistry.getAll(),
-                    isParallel,
-                    infiniteDeviceTimeout
+                    isParallel
                 )
             }
             executor.shutdown()
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt
index 14a49df6..31f13caa 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRegistry.kt
@@ -67,7 +67,7 @@ class NodeRegistry(val activeDevices: ActiveDevices = ActiveDevices()) {
         return mapOf("total" to count)
     }
 
-    fun createDeviceAsync(desiredCapabilities: DesiredCapabilities, deviceTimeout: Duration, userId: String?): DeviceDTO {
+    fun createDeviceAsync(desiredCapabilities: DesiredCapabilities, userId: String?): DeviceDTO {
         if (getAll().isEmpty()) {
             throw NoNodesRegisteredException("No nodes are registered to create a device")
         }
@@ -84,9 +84,9 @@ class NodeRegistry(val activeDevices: ActiveDevices = ActiveDevices()) {
                 LogMarkers.DEVICE_REF to dto.ref,
                 LogMarkers.UDID to dto.info.udid
         ))
-        logger.info(logMarker, "Create device started, register with timeout ${deviceTimeout.seconds} secs")
+        logger.info(logMarker, "Device is created ${dto.info.model} (${dto.info.os}) ${dto.info.arch}. Ref: ${dto.ref}")
 
-        activeDevices.registerDevice(dto.ref, node, deviceTimeout, userId)
+        activeDevices.registerDevice(dto.ref, node, userId)
 
         return dto
     }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRestarter.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRestarter.kt
index 0e6a974e..586cb16b 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRestarter.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/NodeRestarter.kt
@@ -3,18 +3,15 @@ package com.badoo.automation.deviceserver.host.management
 import com.badoo.automation.deviceserver.host.ISimulatorsNode
 import com.badoo.automation.deviceserver.ios.SessionEntry
 import org.slf4j.LoggerFactory
-import java.time.Duration
 
 class NodeRestarter(
     private val nodeRegistry: NodeRegistry
 ) {
     private val logger = LoggerFactory.getLogger(javaClass.simpleName)
-    private val activeSessionsCheckInterval = Duration.ofSeconds(30)
 
     fun restartNodeWrappers(
         nodes: Set,
-        isParallel: Boolean,
-        infiniteDeviceTimeout: Duration
+        isParallel: Boolean
     ) {
         val nodesToRestart = if (isParallel) {
             logger.info("Going to restart nodes in parallel.")
@@ -25,14 +22,12 @@ class NodeRestarter(
         }
 
         nodesToRestart.forEach { nodeWrapper ->
-            nodeWrapper.disable()
-
-            if (!waitForActiveSessionsReleased(nodeWrapper.node, infiniteDeviceTimeout)) {
+            if (activeSessions(nodeWrapper.node).isNotEmpty()) {
                 logger.error("Failed to re-start node $nodeWrapper as it has active sessions with infinite timeout")
-                nodeWrapper.enable()
                 return@forEach
             }
 
+            nodeWrapper.disable()
             nodeWrapper.stop()
 
             if (nodeWrapper.start()) {
@@ -48,29 +43,4 @@ class NodeRestarter(
     private fun activeSessions(node: ISimulatorsNode): Collection {
         return nodeRegistry.activeDevices.activeDevicesByNode(node.publicHostName).values
     }
-
-    private fun hasInfiniteTimeout(sessions: Collection, infiniteDeviceTimeout: Duration): Boolean {
-        return sessions.any { it.releaseTimeout == infiniteDeviceTimeout }
-    }
-
-    private fun waitForActiveSessionsReleased(node: ISimulatorsNode, infiniteDeviceTimeout: Duration): Boolean {
-        while (!Thread.currentThread().isInterrupted) {
-            val sessions = activeSessions(node)
-
-            if (sessions.isEmpty()) {
-                logger.trace("Node $node has no active sessions")
-                return true
-            } else {
-                if (hasInfiniteTimeout(sessions, infiniteDeviceTimeout)) {
-                    logger.error("Some sessions have infinite device timeout. Sessions: $sessions")
-                    return false
-                }
-
-                logger.debug("Node $node still has active sessions: $sessions")
-                Thread.sleep(activeSessionsCheckInterval.toMillis())
-            }
-        }
-
-        return false
-    }
 }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/util/AutoreleaseLooper.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/util/AutoreleaseLooper.kt
deleted file mode 100644
index a368f8ed..00000000
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/util/AutoreleaseLooper.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-package com.badoo.automation.deviceserver.host.management.util
-
-import com.badoo.automation.deviceserver.host.management.IAutoreleaseLooper
-import com.badoo.automation.deviceserver.host.management.DeviceManager
-import com.badoo.automation.deviceserver.util.executeWithTimeout
-import kotlinx.coroutines.experimental.delay
-import kotlinx.coroutines.experimental.launch
-import kotlinx.coroutines.experimental.newFixedThreadPoolContext
-import kotlinx.coroutines.experimental.runBlocking
-import org.slf4j.LoggerFactory
-import java.time.Duration
-import java.util.concurrent.TimeUnit
-import kotlin.math.max
-
-class AutoreleaseLooper : IAutoreleaseLooper {
-    private val autoreleaseThreadPool = newFixedThreadPoolContext(1, "AutoreleaseLoop")
-    private val logger = LoggerFactory.getLogger(javaClass.simpleName)
-
-    override fun autoreleaseLoop(deviceManager: DeviceManager) {
-        launch(autoreleaseThreadPool) {
-            while (isActive) {
-                try {
-                    autoRelease(deviceManager)
-                } catch (t: Throwable) {
-                    logger.warn("Autorelease thread ignored: $t")
-                }
-
-                val seconds = max(1, (deviceManager.nextReleaseAtSeconds() - TimeUnit.NANOSECONDS.toSeconds(System.nanoTime())))
-                delay(seconds, TimeUnit.SECONDS)
-            }
-        }
-    }
-
-    private fun autoRelease(deviceManager: DeviceManager) {
-        val jobs = deviceManager.readyForRelease().map { deviceRef ->
-            launch {
-                val message = "Failed to release device $deviceRef"
-                try {
-                    executeWithTimeout(Duration.ofMinutes(2), message) {
-                        if (isActive) {
-                            deviceManager.deleteReleaseDevice(deviceRef, "autoRelease")
-                        }
-                    }
-                } catch (e: RuntimeException) {
-                    logger.error(message, e)
-                }
-            }
-        }
-
-        runBlocking {
-            jobs.forEach { it.join() }
-        }
-    }
-}
\ No newline at end of file
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt
index a59776af..370cc85f 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevices.kt
@@ -13,8 +13,6 @@ import java.util.concurrent.TimeUnit
 data class SessionEntry(
         val ref: DeviceRef,
         val node: ISimulatorsNode,
-        var updatedAtSeconds: Long,
-        val releaseTimeout: Duration,
         val userId: String?
 )
 
@@ -51,8 +49,8 @@ class ActiveDevices(
         return list
     }
 
-    fun registerDevice(ref: DeviceRef, node: ISimulatorsNode, releaseTimeout: Duration, userId: String?) {
-        devices[ref] = SessionEntry(ref, node, currentTimeSeconds(), releaseTimeout, userId)
+    fun registerDevice(ref: DeviceRef, node: ISimulatorsNode, userId: String?) {
+        devices[ref] = SessionEntry(ref, node, userId)
     }
 
     fun unregisterNodeDevices(node: ISimulatorsNode) {
@@ -61,18 +59,8 @@ class ActiveDevices(
                 .forEach { unregisterDeleteDevice(it.key) }
     }
 
-    private fun refreshSessionEntry(sessionEntry: SessionEntry) {
-        sessionEntry.updatedAtSeconds = currentTimeSeconds()
-    }
-
     private fun tryGetNodeFor(ref: DeviceRef): ISimulatorsNode? {
-        val sessionEntry = devices[ref]
-        if (sessionEntry != null) {
-            refreshSessionEntry(sessionEntry)
-            return sessionEntry.node
-        } else {
-            return null
-        }
+        return devices[ref]?.node
     }
 
     fun getNodeFor(ref: DeviceRef): ISimulatorsNode {
@@ -94,28 +82,6 @@ class ActiveDevices(
         devices.remove(ref)
     }
 
-    fun readyForRelease(): List {
-        val secondsNow = currentTimeSeconds()
-        return devices.filter { (_, session) -> secondsNow - session.updatedAtSeconds >= session.releaseTimeout.seconds }
-                .map { (deviceRef, _) -> deviceRef }
-                .also { logger.info("Ready to release $it"); }
-    }
-
-    fun nextReleaseAtSeconds(): Long {
-        val sessionEntry = devices
-            .map { it.value }
-            .minBy { it.updatedAtSeconds + it.releaseTimeout.seconds }
-
-        val nextReleaseAtSeconds = if (sessionEntry == null) {
-            currentTimeSeconds() + DEFAULT_RELEASE_TIMEOUT.seconds
-        } else {
-            sessionEntry.updatedAtSeconds + sessionEntry.releaseTimeout.seconds
-        }
-
-        logger.info("nextReleaseAtSeconds = $nextReleaseAtSeconds seconds")
-        return nextReleaseAtSeconds
-    }
-
     fun releaseDevice(ref: DeviceRef, reason: String) {
         val session = sessionByRef(ref)
         session.node.deleteRelease(session.ref, reason)
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeRegistryTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeRegistryTest.kt
index d4bd13fa..0d6d36d8 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeRegistryTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/host/NodeRegistryTest.kt
@@ -78,7 +78,6 @@ class NodeRegistryTest {
     @Test
     fun createSimulatorByCapacity() {
         // arrange
-        val deviceTimeout = Duration.ofSeconds(0)
         whenever(nodeWrapper1.isAlive()).thenReturn(true)
         whenever(nodeWrapper2.isAlive()).thenReturn(true)
         whenever(wrappedNode1.createDeviceAsync(desiredCapabilities)).then { deviceDTOStub("") }
@@ -88,10 +87,10 @@ class NodeRegistryTest {
         whenever(wrappedNode2.capacityRemaining(desiredCapabilities)).thenReturn(capacityNotBusy)
 
         // act
-        nodeRegistry.createDeviceAsync(desiredCapabilities, deviceTimeout, null)
+        nodeRegistry.createDeviceAsync(desiredCapabilities, null)
 
         // assert
-        verify(activeDevices).registerDevice("", wrappedNode2, deviceTimeout, null)
+        verify(activeDevices).registerDevice("", wrappedNode2, null)
     }
 
     @Test
@@ -100,14 +99,14 @@ class NodeRegistryTest {
         whenever(nodeWrapper1.isAlive()).thenReturn(true)
         whenever(wrappedNode1.createDeviceAsync(desiredCapabilities)).then { deviceDTOStub("") }
         whenever(wrappedNode1.capacityRemaining(desiredCapabilities)).thenReturn(capacityNotBusy)
-        assertNotNull(nodeRegistry.createDeviceAsync(desiredCapabilities, Duration.ZERO, null))
+        assertNotNull(nodeRegistry.createDeviceAsync(desiredCapabilities, null))
 
         // act
         whenever(nodeWrapper1.isEnabled).thenReturn(false)
 
         // assert
         assertFailsWith {
-            nodeRegistry.createDeviceAsync(desiredCapabilities, Duration.ZERO, null)
+            nodeRegistry.createDeviceAsync(desiredCapabilities, null)
         }
     }
 
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevicesTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevicesTest.kt
index d2daaf0d..7fffe359 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevicesTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/ActiveDevicesTest.kt
@@ -20,13 +20,13 @@ class ActiveDevicesTest {
         whenever(node.getDeviceDTO("ref2")).thenThrow(DeviceNotFoundException(""))
         whenever(node.getDeviceDTO("ref3")).thenReturn(deviceDTOStub("ref3"))
 
-        activeDevices.registerDevice("ref1", node, Duration.ZERO, null)
-        activeDevices.registerDevice("ref2", node, Duration.ZERO, null)
-        activeDevices.registerDevice("ref3", node, Duration.ZERO, null)
+        activeDevices.registerDevice("ref1", node, null)
+        activeDevices.registerDevice("ref2", node, null)
+        activeDevices.registerDevice("ref3", node, null)
 
         val list = activeDevices.deviceList()
 
         assertEquals(listOf("ref1", "ref3"), list.map { it.ref }.sorted())
         assertEquals(2, activeDevices.deviceRefs().size)
     }
-}
\ No newline at end of file
+}
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/DeviceManagerTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/DeviceManagerTest.kt
index 8cdb5697..77d92a80 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/DeviceManagerTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/DeviceManagerTest.kt
@@ -6,7 +6,6 @@ import com.badoo.automation.deviceserver.data.*
 import com.badoo.automation.deviceserver.deviceDTOStub
 import com.badoo.automation.deviceserver.host.ISimulatorsNode
 import com.badoo.automation.deviceserver.host.management.DeviceManager
-import com.badoo.automation.deviceserver.host.management.IAutoreleaseLooper
 import com.badoo.automation.deviceserver.host.management.IHostFactory
 import com.badoo.automation.deviceserver.host.management.errors.DeviceNotFoundException
 import com.badoo.automation.deviceserver.mockThis
@@ -34,8 +33,8 @@ class DeviceManagerTest {
     private val hostTwo = mockHostWithTotalCapacity(2, true)
 
     private val hostsMap = mapOf(
-            "zero" to hostZero, "one" to hostOne, "two" to hostTwo,
-            "unreachable" to mockHostWithTotalCapacity(4, false)
+        "zero" to hostZero, "one" to hostOne, "two" to hostTwo,
+        "unreachable" to mockHostWithTotalCapacity(4, false)
     )
     private val hostFactory: IHostFactory = object : IHostFactory {
         override fun getHostFromConfig(config: NodeConfig): ISimulatorsNode {
@@ -53,16 +52,13 @@ class DeviceManagerTest {
 
     private val activeDevices: ActiveDevices = mockThis()
 
-    private val autoreleaseLooper: IAutoreleaseLooper = mockThis()
-
     private val deviceManager = DeviceManager(
-            DeviceServerConfig(
-                    emptyMap(),
-                    setOf()
-            ),
-            hostFactory,
-            activeDevices,
-            autoreleaseLooper
+        DeviceServerConfig(
+            emptyMap(),
+            setOf()
+        ),
+        hostFactory,
+        activeDevices
     )
 
     @Test
@@ -78,7 +74,7 @@ class DeviceManagerTest {
     fun deleteReleaseDeviceThatHasBeenReleased() {
         val sessionId = "defaultSessionId"
         whenever(activeDevices.getNodeFor(ref)).thenThrow(
-                DeviceNotFoundException("Device [$ref] not found in [$sessionId] activeDevices")
+            DeviceNotFoundException("Device [$ref] not found in [$sessionId] activeDevices")
         )
         deviceManager.deleteReleaseDevice(ref, "httpRequest")
         verify(activeDevices, times(0)).unregisterDeleteDevice(any())
@@ -91,48 +87,48 @@ class DeviceManagerTest {
 
     @Test
     fun getMethodReturningDeviceDTO() {
-        withDeviceOnHost(hostTwo ) {
-        whenever(hostTwo.getDeviceDTO(ref)).thenReturn(expectedDto)
+        withDeviceOnHost(hostTwo) {
+            whenever(hostTwo.getDeviceDTO(ref)).thenReturn(expectedDto)
 
-        val actualDto = deviceManager.getGetDeviceDTO(ref)
+            val actualDto = deviceManager.getGetDeviceDTO(ref)
 
-        assertThat(actualDto, equalTo(expectedDto))
-    }
+            assertThat(actualDto, equalTo(expectedDto))
+        }
     }
 
     @Test
     fun clearSafariCookies() {
-        withDeviceOnHost(hostTwo ) {
-        deviceManager.clearSafariCookies(ref)
-        verify(hostTwo).clearSafariCookies(ref)
-    }
+        withDeviceOnHost(hostTwo) {
+            deviceManager.clearSafariCookies(ref)
+            verify(hostTwo).clearSafariCookies(ref)
+        }
     }
 
     @Test
     fun resetAsyncDevice() {
-        withDeviceOnHost(hostTwo ) {
+        withDeviceOnHost(hostTwo) {
 
-        deviceManager.resetAsyncDevice(ref)
+            deviceManager.resetAsyncDevice(ref)
 
-        verify(hostTwo).resetAsync(ref)
-    }
+            verify(hostTwo).resetAsync(ref)
+        }
     }
 
     @Test
     fun approveAccess() {
-        withDeviceOnHost(hostTwo ) {
-        deviceManager.approveAccess(ref, bundleId)
-        verify(hostTwo).approveAccess(ref, bundleId)
-    }
+        withDeviceOnHost(hostTwo) {
+            deviceManager.approveAccess(ref, bundleId)
+            verify(hostTwo).approveAccess(ref, bundleId)
+        }
     }
 
     @Test
     fun getEndpointFor() {
-        withDeviceOnHost(hostTwo ) {
-        whenever(hostTwo.endpointFor(ref, 1234)).thenReturn(someUrl)
-        val actual = deviceManager.getEndpointFor(ref, 1234)
-        assertThat(actual, equalTo(someUrl))
-    }
+        withDeviceOnHost(hostTwo) {
+            whenever(hostTwo.endpointFor(ref, 1234)).thenReturn(someUrl)
+            val actual = deviceManager.getEndpointFor(ref, 1234)
+            assertThat(actual, equalTo(someUrl))
+        }
     }
 
     @Test
@@ -181,19 +177,13 @@ class DeviceManagerTest {
 
     @Test
     fun getDeviceState() { // deviceStateDTO
-        withDeviceOnHost(hostTwo ) {
-        val deviceOrSimulatorStatusBloodyContradictoryNonsense = SimulatorStatusDTO(
-            false, false, false, DeviceState.NONE.value, null)
-        whenever(hostTwo.state(ref)).thenReturn(deviceOrSimulatorStatusBloodyContradictoryNonsense)
-        val actual = deviceManager.getDeviceState(ref)
-        assertThat(actual, equalTo(deviceOrSimulatorStatusBloodyContradictoryNonsense))
-    }
-    }
-
-    @Test
-    fun autoReleaseLoopIsCalledByConstructor() {
-        deviceManager.launchAutoReleaseLoop()
-        verify(autoreleaseLooper).autoreleaseLoop(deviceManager)
+        withDeviceOnHost(hostTwo) {
+            val deviceOrSimulatorStatusBloodyContradictoryNonsense = SimulatorStatusDTO(
+                false, false, false, DeviceState.NONE.value, null)
+            whenever(hostTwo.state(ref)).thenReturn(deviceOrSimulatorStatusBloodyContradictoryNonsense)
+            val actual = deviceManager.getDeviceState(ref)
+            assertThat(actual, equalTo(deviceOrSimulatorStatusBloodyContradictoryNonsense))
+        }
     }
 
     @Test
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/SessionTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/SessionTest.kt
index 1af6313a..502c7e6b 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/SessionTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/SessionTest.kt
@@ -16,8 +16,6 @@ class SessionTest {
     private var sillySeconds:  Long = 42L
     private val session = ActiveDevices(currentTimeSeconds = { sillySeconds++ })
 
-    private val releaseAfterSecs = Duration.ofSeconds(5)
-
     private val deviceRef1: DeviceRef = "hello-1"
     private val deviceRef2: DeviceRef = "hello-2"
 
@@ -28,14 +26,14 @@ class SessionTest {
 
     @Test
     fun registerDevice() {
-        session.registerDevice(deviceRef1, host1, releaseAfterSecs, null)
+        session.registerDevice(deviceRef1, host1, null)
         assertThat(session.deviceRefs().size, equalTo(1))
     }
 
     @Test
     fun unregisterNodeDevices() {
-        session.registerDevice(deviceRef1, host1, releaseAfterSecs, null)
-        session.registerDevice(deviceRef2, host2, releaseAfterSecs, null)
+        session.registerDevice(deviceRef1, host1, null)
+        session.registerDevice(deviceRef2, host2, null)
 
         session.unregisterNodeDevices(host1)
 
@@ -44,8 +42,8 @@ class SessionTest {
 
     @Test
     fun tryGetNodeForReturnsDeviceIfPresent() {
-        session.registerDevice(deviceRef1, host1, releaseAfterSecs, null)
-        session.registerDevice(deviceRef2, host2, releaseAfterSecs, null)
+        session.registerDevice(deviceRef1, host1, null)
+        session.registerDevice(deviceRef2, host2, null)
         assertThat(session.getNodeFor(deviceRef1), equalTo(host1))
         assertThat(session.getNodeFor(deviceRef2), equalTo(host2))
     }
@@ -57,43 +55,18 @@ class SessionTest {
 
     @Test
     fun getNodeForReturnsDeviceIfPresent() {
-        session.registerDevice(deviceRef1, host1, releaseAfterSecs, null)
-        session.registerDevice(deviceRef2, host2, releaseAfterSecs, null)
+        session.registerDevice(deviceRef1, host1, null)
+        session.registerDevice(deviceRef2, host2, null)
         assertThat(session.getNodeFor(deviceRef1), equalTo(host1))
         assertThat(session.getNodeFor(deviceRef2), equalTo(host2))
     }
 
-    @Test
-    fun refreshDevice() {
-        session.registerDevice(deviceRef1, host1, releaseAfterSecs, null)
-        val preReleaseSeconds = session.nextReleaseAtSeconds()
-
-        session.getNodeFor(deviceRef1)
-
-        assertThat(session.nextReleaseAtSeconds(), greaterThan(preReleaseSeconds))
-    }
-
     @Test
     fun unregisterDeleteDevice() {
-        session.registerDevice(deviceRef1, host1, releaseAfterSecs, null)
-        session.registerDevice(deviceRef2, host2, releaseAfterSecs, null)
+        session.registerDevice(deviceRef1, host1, null)
+        session.registerDevice(deviceRef2, host2, null)
         session.unregisterDeleteDevice(deviceRef2)
         assertThat(session.deviceRefs().size, equalTo(1))
         assertThat(session.deviceRefs().first(), equalTo(deviceRef1))
     }
-
-    @Test
-    fun readyForRelease() {
-        session.registerDevice(deviceRef1, host1, Duration.ofSeconds(1), null)
-        // Implementation detail: secondsSinceEpoch always increments once per call.
-        session.registerDevice(deviceRef2, host2, Duration.ofSeconds(1), null)
-        val deviceList = session.readyForRelease()
-        assertThat(deviceList.size, equalTo(2))
-    }
-
-    @Test
-    fun nextReleaseAtSeconds() {
-        session.registerDevice(deviceRef1, host1, Duration.ofSeconds(10), null)
-        assertThat(session.nextReleaseAtSeconds(), equalTo(10L + 42L))
-    }
-}
\ No newline at end of file
+}

From 9879ab4f9599f9ef844b1ce838873bc47a9d619f Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Fri, 14 Feb 2020 14:56:36 +0000
Subject: [PATCH 127/131] Rename variable devicePool to createdDevices in
 SimulatorsNode

---
 .../deviceserver/host/SimulatorsNode.kt       | 20 +++++++++----------
 1 file changed, 10 insertions(+), 10 deletions(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
index e10a429e..63453a74 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
@@ -59,21 +59,21 @@ class SimulatorsNode(
     private val concurrentBoot = newFixedThreadPoolContext(concurrentBoots, "sim_boot_${remote.hostName}")
 
     private fun getDeviceFor(ref: DeviceRef): ISimulator {
-        return devicePool[ref]!! //FIXME: replace with explicit unwrapping
+        return createdDevices[ref]!! //FIXME: replace with explicit unwrapping
     }
 
-    private val devicePool = ConcurrentHashMap()
+    private val createdDevices = ConcurrentHashMap()
     private val allocatedPorts = HashMap()
 
     override fun createDeviceAsync(desiredCaps: DesiredCapabilities): DeviceDTO {
         synchronized(this) { // FIXME: synchronize in some other place?
-            if (devicePool.size >= simulatorLimit) {
+            if (createdDevices.size >= simulatorLimit) {
                 val message = "$this was asked for a newSimulator, but is already at capacity $simulatorLimit"
                 logger.error(logMarker, message)
                 throw OverCapacityException(message)
             }
 
-            val usedUdids = devicePool.map { it.value.udid }.toSet()
+            val usedUdids = createdDevices.map { it.value.udid }.toSet()
             val fbSimctlDevice = simulatorProvider.match(desiredCaps, usedUdids)
             if (fbSimctlDevice == null) {
                 val message = "$this could not construct or match a simulator for $desiredCaps"
@@ -96,7 +96,7 @@ class SimulatorsNode(
             val simulator = simulatorFactory.newSimulator(ref, remote, fbSimctlDevice, ports, deviceSetPath,
                     wdaRunnerXctest, concurrentBoot, desiredCaps.headless, desiredCaps.useWda, fbSimctlDevice.toString())
             simulator.prepareAsync()
-            devicePool[ref] = simulator
+            createdDevices[ref] = simulator
 
             logger.debug(simLogMarker, "Created simulator $ref")
 
@@ -177,12 +177,12 @@ class SimulatorsNode(
         getDeviceFor(deviceRef).shake()
     }
 
-    override fun count(): Int = devicePool.size
+    override fun count(): Int = createdDevices.size
 
     override fun dispose() {
         logger.info(logMarker, "Finalising simulator pool for ${remote.hostName}")
 
-        val disposeJobs = devicePool.map {
+        val disposeJobs = createdDevices.map {
             launch(context = simulatorsThreadPool) {
                 try {
                     it.value.release("Finalising pool for ${remote.hostName}")
@@ -226,15 +226,15 @@ class SimulatorsNode(
     }
 
     override fun list(): List {
-        return devicePool.map { simulatorToDTO(it.value) }
+        return createdDevices.map { simulatorToDTO(it.value) }
     }
 
     override fun isReachable(): Boolean = remote.isReachable()
 
     override fun deleteRelease(deviceRef: DeviceRef, reason: String): Boolean {
-        val iSimulator = devicePool[deviceRef] ?: return false
+        val iSimulator = createdDevices[deviceRef] ?: return false
         iSimulator.release("deleteRelease $reason $deviceRef")
-        devicePool.remove(deviceRef)
+        createdDevices.remove(deviceRef)
         val entries = allocatedPorts[deviceRef] ?: return true
         portAllocator.deallocateDAP(entries)
         return true

From 36e3c7759b6f41591bb2c3fdfc3d9f4849639326 Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Fri, 14 Feb 2020 18:06:08 +0000
Subject: [PATCH 128/131] Use scp to add media files

---
 .../automation/deviceserver/ios/simulator/data/Media.kt  | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/Media.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/Media.kt
index a312f9ed..134e6e52 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/Media.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/Media.kt
@@ -7,6 +7,7 @@ import com.badoo.automation.deviceserver.util.withDefers
 import org.slf4j.LoggerFactory
 import java.io.File
 import java.nio.file.Paths
+import java.time.Duration
 
 class Media(
     private val remote: IRemote,
@@ -43,13 +44,13 @@ class Media(
             val mediaPath: String = if (remote.isLocalhost()) {
                 tmpFile.absolutePath
             } else {
-                val remoteMediaDir = remote.execIgnoringErrors(listOf("mktemp", "-d")).stdOut.trim()
-                defer { remote.execIgnoringErrors(listOf("rm", "-rf", remoteMediaDir)) }
-                remote.rsync(tmpFile.absolutePath, remoteMediaDir, setOf("-r", "--delete"))
+                val remoteMediaDir = remote.execIgnoringErrors(listOf("/usr/bin/mktemp", "-d")).stdOut.trim()
+                defer { remote.execIgnoringErrors(listOf("/bin/rm", "-rf", remoteMediaDir)) }
+                remote.scpToRemoteHost(tmpFile.absolutePath, remoteMediaDir, Duration.ofMinutes(1))
                 File(remoteMediaDir, tmpFile.name).absolutePath
             }
 
-            val result = remote.execIgnoringErrors(listOf("xcrun", "simctl", "addmedia", udid, mediaPath))
+            val result = remote.execIgnoringErrors(listOf("/usr/bin/xcrun", "simctl", "addmedia", udid, mediaPath))
 
             if (!result.isSuccess) {
                 throw RuntimeException("Could not add Media to device: $result")

From b86c6ccb617225e56a3874d32557e438263778fb Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Fri, 14 Feb 2020 18:48:46 +0000
Subject: [PATCH 129/131] Add API to update application's plist files

---
 .../automation/deviceserver/DeviceServer.kt   |  5 ++
 .../controllers/DevicesController.kt          |  5 ++
 .../deviceserver/data/PlistEntryDTO.kt        | 23 ++++++++
 .../deviceserver/host/DevicesNode.kt          |  4 ++
 .../deviceserver/host/ISimulatorsNode.kt      |  1 +
 .../deviceserver/host/SimulatorsNode.kt       | 15 ++++++
 .../host/management/DeviceManager.kt          |  4 ++
 .../deviceserver/ios/simulator/ISimulator.kt  |  1 +
 .../deviceserver/ios/simulator/Simulator.kt   |  5 +-
 .../ios/simulator/data/DataContainer.kt       | 25 +++++++--
 .../ios/simulator/data/FileSystem.kt          | 53 ++++++++++++++++---
 .../ios/simulator/data/DataContainerTest.kt   | 10 ++--
 .../ios/simulator/data/FileSystemTest.kt      | 35 +++---------
 13 files changed, 141 insertions(+), 45 deletions(-)
 create mode 100644 device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/PlistEntryDTO.kt

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
index 15b2519c..ad01ca21 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
@@ -237,6 +237,11 @@ fun Application.module() {
                         val bundleId = param(call, "bundleId")
                         call.respond(devicesController.uninstallApplication(ref, bundleId))
                     }
+                    post("update_plist") {
+                        val ref = param(call, "ref")
+                        val plistEntries = jsonContent(call)
+                        call.respond(devicesController.updateApplicationPlist(ref, plistEntries))
+                    }
                 }
                 route("media") {
                     get {
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt
index 57a4796e..630c0805 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt
@@ -163,4 +163,9 @@ class DevicesController(private val deviceManager: DeviceManager) {
         deviceManager.setEnvironmentVariables(ref, environmentVariables)
         return happy
     }
+
+    fun updateApplicationPlist(deviceRef: String, plistEntry: PlistEntryDTO): Any {
+        deviceManager.updateApplicationPlist(deviceRef, plistEntry)
+        return happy
+    }
 }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/PlistEntryDTO.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/PlistEntryDTO.kt
new file mode 100644
index 00000000..d77ff333
--- /dev/null
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/PlistEntryDTO.kt
@@ -0,0 +1,23 @@
+package com.badoo.automation.deviceserver.data
+
+import com.fasterxml.jackson.annotation.JsonProperty
+
+data class PlistEntryDTO(
+    @JsonProperty("bundle_id")
+    val bundleId: String,
+
+    @JsonProperty("file_name")
+    val file_name: String,
+
+    @JsonProperty("property_name")
+    val key: String,
+
+    @JsonProperty("property_value")
+    val value: String,
+
+    @JsonProperty("property_type")
+    val type: String?,
+
+    @JsonProperty("command") // set / add
+    val command: String
+)
\ No newline at end of file
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
index bb31be0d..3eea9b7f 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
@@ -381,6 +381,10 @@ class DevicesNode(
         throw(NotImplementedError("Setting environment variables is not supported by physical devices"))
     }
 
+    override fun updateApplicationPlist(ref: DeviceRef, plistEntry: PlistEntryDTO) {
+        throw(NotImplementedError("Updating application plist is not supported by physical devices"))
+    }
+
     override fun equals(other: Any?): Boolean {
         if (this === other) return true
         if (javaClass != other?.javaClass) return false
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
index eba2a301..ba932984 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
@@ -49,5 +49,6 @@ interface ISimulatorsNode {
     fun dispose()
     fun uninstallApplication(deviceRef: DeviceRef, bundleId: String)
     fun setEnvironmentVariables(deviceRef: DeviceRef, envs: Map)
+    fun updateApplicationPlist(ref: DeviceRef, plistEntry: PlistEntryDTO)
     val publicHostName: String
 }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
index 63453a74..afa7d667 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
@@ -31,6 +31,21 @@ class SimulatorsNode(
         private val simulatorFactory: ISimulatorFactory = object : ISimulatorFactory {}
 ) : ISimulatorsNode {
 
+    override fun updateApplicationPlist(ref: DeviceRef, plistEntry: PlistEntryDTO) {
+        val applicationContainer = getDeviceFor(ref).applicationContainer(plistEntry.bundleId)
+        val path = File(plistEntry.file_name).toPath()
+
+        when (plistEntry.command) {
+            "set" -> applicationContainer.setPlistValue(path, plistEntry.key, plistEntry.value)
+            "add" -> {
+                val entryType = plistEntry.type
+                    ?: throw IllegalArgumentException("Unable to add new property ${plistEntry.key} as it requires value type (property_type).")
+                applicationContainer.addPlistValue(path, plistEntry.key, plistEntry.value, entryType)
+            }
+            else -> throw IllegalArgumentException("Unsupported operation: ${plistEntry.command}")
+        }
+    }
+
     override val remoteAddress: String get() = publicHostName
 
     private val logger = LoggerFactory.getLogger(javaClass.simpleName)
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
index e5f5d41c..a9877189 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
@@ -176,4 +176,8 @@ class DeviceManager(
     fun resetDiagnostic(ref: DeviceRef, type: DiagnosticType) {
         nodeRegistry.activeDevices.getNodeFor(ref).resetDiagnostic(ref, type)
     }
+
+    fun updateApplicationPlist(deviceRef: String, plistEntry: PlistEntryDTO) {
+        return nodeRegistry.activeDevices.getNodeFor(deviceRef).updateApplicationPlist(deviceRef, plistEntry)
+    }
 }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt
index 86782493..84e7dec1 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/ISimulator.kt
@@ -43,4 +43,5 @@ interface ISimulator {
     fun uninstallApplication(bundleId: String)
     fun deleteCrashLogs(): Boolean
     fun setEnvironmentVariables(envs: Map)
+    fun applicationContainer(bundleId: String): DataContainer
 }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
index 78155fb7..e92c073a 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
@@ -35,7 +35,6 @@ import java.net.URL
 import java.nio.file.Paths
 import java.time.Duration
 import java.util.concurrent.locks.ReentrantLock
-import javax.net.ssl.HostnameVerifier
 import kotlin.concurrent.withLock
 import kotlin.system.measureTimeMillis
 
@@ -714,6 +713,10 @@ class Simulator (
         return fileSystem.dataContainer(bundleId)
     }
 
+    override fun applicationContainer(bundleId: String): DataContainer {
+        return fileSystem.applicationContainer(bundleId)
+    }
+
     //endregion
     override fun uninstallApplication(bundleId: String) {
         logger.debug(logMarker, "Uninstalling application $bundleId from Simulator $this")
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainer.kt
index 11b30397..622256bc 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainer.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainer.kt
@@ -1,16 +1,23 @@
 package com.badoo.automation.deviceserver.ios.simulator.data
 
+import com.badoo.automation.deviceserver.LogMarkers
 import com.badoo.automation.deviceserver.command.ShellUtils
 import com.badoo.automation.deviceserver.host.IRemote
+import net.logstash.logback.marker.MapEntriesAppendingMarker
+import org.slf4j.LoggerFactory
 import java.io.File
 import java.lang.RuntimeException
 import java.nio.file.Path
 
 class DataContainer(
     private val remote: IRemote,
-    internal val basePath: Path,
+    internal val basePath: File,
     private val bundleId: String
-    ) {
+) {
+    private val logger = LoggerFactory.getLogger(DataContainer::class.java.simpleName)
+    private val logMarker = MapEntriesAppendingMarker(mapOf(
+        LogMarkers.HOSTNAME to remote.hostName
+    ))
 
     fun listFiles(path: Path): List {
         val expandedPath = sshNoEscapingWorkaround(expandPath(path).toString())
@@ -33,6 +40,16 @@ class DataContainer(
         }
     }
 
+    fun setPlistValue(path: Path, key: String, value: String) {
+        val expandedPath = sshNoEscapingWorkaround(expandPath(path).toString())
+        remote.shell("/usr/libexec/PlistBuddy -c 'Set $key $value' $expandedPath", false)
+    }
+
+    fun addPlistValue(path: Path, key: String, value: String, type: String) {
+        val expandedPath = sshNoEscapingWorkaround(expandPath(path).toString())
+        remote.shell("/usr/libexec/PlistBuddy -c 'Add $key $type $value' $expandedPath", false)
+    }
+
     private fun sshNoEscapingWorkaround(path: String): String {
         // FIXME: fix escaping on ssh side and remove workarounds
         return when {
@@ -42,8 +59,8 @@ class DataContainer(
     }
 
     private fun expandPath(path: Path): Path {
-        val expanded = basePath.resolve(path).normalize()
-        if (!expanded.startsWith(basePath)) {
+        val expanded = basePath.toPath().resolve(path).normalize()
+        if (!expanded.startsWith(basePath.absolutePath)) {
             throw DataContainerException("$path points outside the container of $bundleId")
         }
 
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/FileSystem.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/FileSystem.kt
index d73312a8..d050fcda 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/FileSystem.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/FileSystem.kt
@@ -1,24 +1,63 @@
 package com.badoo.automation.deviceserver.ios.simulator.data
 
+import com.badoo.automation.deviceserver.LogMarkers
 import com.badoo.automation.deviceserver.data.UDID
 import com.badoo.automation.deviceserver.host.IRemote
-import java.nio.file.Paths
+import com.badoo.automation.deviceserver.util.deviceRefFromUDID
+import net.logstash.logback.marker.MapEntriesAppendingMarker
+import org.slf4j.LoggerFactory
+import java.io.File
+import java.io.FileNotFoundException
 
 class FileSystem(
     private val remote: IRemote,
     private val udid: UDID
 ) {
-    fun dataContainer(bundleId: String): DataContainer {
-        val app = remote.fbsimctl.listApps(udid).find { it.bundle.bundle_id == bundleId }
+    private val logger = LoggerFactory.getLogger(DataContainer::class.java.simpleName)
+    private val deviceRef = deviceRefFromUDID(udid, remote.publicHostName)
+    private val logMarker = MapEntriesAppendingMarker(mapOf(
+        LogMarkers.HOSTNAME to remote.hostName,
+        LogMarkers.UDID to udid,
+        LogMarkers.DEVICE_REF to deviceRef
+    ))
 
-        if (app?.data_container == null) {
-            throw(IllegalArgumentException("No data container for $bundleId"))
-        }
+    fun applicationContainer(bundleId: String): DataContainer {
+        return DataContainer(
+            remote,
+            getContainerPath(bundleId, "app"),
+            bundleId
+        )
+    }
 
+    fun dataContainer(bundleId: String): DataContainer {
         return DataContainer(
             remote,
-            Paths.get(app.data_container),
+            getContainerPath(bundleId, "data"),
             bundleId
         )
     }
+
+    private fun getContainerPath(bundleId: String, containerType: String): File {
+        val result = remote.exec(
+            command = listOf(
+                "/usr/bin/xcrun",
+                "simctl",
+                "get_app_container",
+                udid,
+                bundleId,
+                containerType
+            ),
+            env = mapOf(),
+            returnFailure = true,
+            timeOutSeconds = 30
+        )
+
+        return if (result.isSuccess) {
+            File(result.stdOut.trim())
+        } else {
+            val message = "Failed to get container for $containerType for bundle id $bundleId on simulator $udid"
+            logger.error(logMarker, message)
+            throw FileNotFoundException(message)
+        }
+    }
 }
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainerTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainerTest.kt
index 8629ff0e..e65ed764 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainerTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainerTest.kt
@@ -19,7 +19,7 @@ class DataContainerTest {
     private val fbsimctl: FBSimctl = mockThis()
 
     private val containerPathStub =
-        "/Users/qa/Library/Developer/CoreSimulator/Devices/UDID/data/Containers/Data/Application/A2C79BEC-FD2C-4676-BA9B-B6A62AFE193A"
+        File("/Users/qa/Library/Developer/CoreSimulator/Devices/UDID/data/Containers/Data/Application/A2C79BEC-FD2C-4676-BA9B-B6A62AFE193A")
 
     @Before
     fun setUp() {
@@ -40,7 +40,7 @@ class DataContainerTest {
 
         val container = DataContainer(
             remote = remote,
-            basePath = Paths.get(containerPathStub),
+            basePath = containerPathStub,
             bundleId = "test.bundle"
         )
         val actual = container.listFiles(Paths.get("Library/Caches"))
@@ -68,7 +68,7 @@ class DataContainerTest {
 
         val container = DataContainer(
             remote = remote,
-            basePath = Paths.get(containerPathStub),
+            basePath = containerPathStub,
             bundleId = "test.bundle"
         )
         val actual = container.listFiles(Paths.get("Library/Caches"))
@@ -83,7 +83,7 @@ class DataContainerTest {
 
         val container = DataContainer(
             remote = remote,
-            basePath = Paths.get(containerPathStub),
+            basePath = containerPathStub,
             bundleId = "test.bundle"
         )
         val actual = container.readFile(Paths.get("Library/Caches/file.txt"))
@@ -95,7 +95,7 @@ class DataContainerTest {
     fun shouldRejectPathOutsideContainer() {
         val container = DataContainer(
             remote = remote,
-            basePath = Paths.get(containerPathStub),
+            basePath = containerPathStub,
             bundleId = "test.bundle"
         )
 
diff --git a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/FileSystemTest.kt b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/FileSystemTest.kt
index 1716d9ef..d262255a 100644
--- a/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/FileSystemTest.kt
+++ b/device-server/src/test/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/FileSystemTest.kt
@@ -1,53 +1,32 @@
 package com.badoo.automation.deviceserver.ios.simulator.data
 
+import com.badoo.automation.deviceserver.command.CommandResult
 import com.badoo.automation.deviceserver.data.UDID
 import com.badoo.automation.deviceserver.host.IRemote
-import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctl
-import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlAppInfo
-import com.badoo.automation.deviceserver.ios.fbsimctl.FBSimctlAppInfoBundle
 import com.badoo.automation.deviceserver.mockThis
+import com.nhaarman.mockito_kotlin.any
 import com.nhaarman.mockito_kotlin.whenever
 import org.junit.Before
 import org.junit.Test
-import java.nio.file.Paths
+import java.io.File
 import kotlin.test.assertEquals
-import kotlin.test.assertFails
 
 class FileSystemTest {
-
     private val udid: UDID = "udid"
     private val remote: IRemote = mockThis()
-    private val fbsimctl: FBSimctl = mockThis()
-
-    private val containerPathStub =
-        "/Users/qa/Library/Developer/CoreSimulator/Devices/UDID/data/Containers/Data/Application/A2C79BEC-FD2C-4676-BA9B-B6A62AFE193A/"
-    private val bundleInfoStub = FBSimctlAppInfo(
-        containerPathStub,
-        FBSimctlAppInfoBundle(null, "test.bundle", null, null),
-        null
-    )
+    private val containerPathStub = File("/Users/qa/Library/Developer/CoreSimulator/Devices/UDID/data/Containers/Data/Application/A2C79BEC-FD2C-4676-BA9B-B6A62AFE193A/")
 
     @Before
     fun setUp() {
-        whenever(remote.fbsimctl).thenReturn(fbsimctl)
+        whenever(remote.publicHostName).thenReturn("asdf")
     }
 
     @Test
     fun shouldCreateDataContainer() {
-        whenever(fbsimctl.listApps(udid)).thenReturn(listOf(bundleInfoStub))
+        whenever(remote.exec(any(),any(),any(),any())).thenReturn(CommandResult(stdOut = containerPathStub.absolutePath, stdErr = "", exitCode = 0, cmd = listOf(), pid = 1L))
 
         val container = FileSystem(remote, udid).dataContainer("test.bundle")
 
-        assertEquals(Paths.get(containerPathStub), container.basePath)
-    }
-
-    @Test
-    fun shouldFailOnNonExistingBundleId() {
-        whenever(fbsimctl.listApps(udid)).thenReturn(listOf(bundleInfoStub))
-
-
-        assertFails {
-            FileSystem(remote, udid).dataContainer("non-existing.bundle.id")
-        }
+        assertEquals(containerPathStub, container.basePath)
     }
 }

From e46ac8de2853a3496bcd40bd9a45477f75c9d51d Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Fri, 14 Feb 2020 23:44:24 +0000
Subject: [PATCH 130/131] Push files to application's data container on
 Simulator

---
 .../automation/deviceserver/DeviceServer.kt   | 10 ++++++++
 .../controllers/DevicesController.kt          |  5 ++++
 .../automation/deviceserver/data/FileDto.kt   |  5 +++-
 .../deviceserver/host/DevicesNode.kt          |  4 ++++
 .../deviceserver/host/ISimulatorsNode.kt      |  1 +
 .../deviceserver/host/SimulatorsNode.kt       |  4 ++++
 .../host/management/DeviceManager.kt          |  4 ++++
 .../ios/simulator/data/DataContainer.kt       | 23 +++++++++++++++++++
 8 files changed, 55 insertions(+), 1 deletion(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
index ad01ca21..ccb90da8 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/DeviceServer.kt
@@ -225,6 +225,16 @@ fun Application.module() {
                         val dataPath = jsonContent(call)
                         call.respond(devicesController.pullFile(ref, dataPath))
                     }
+                    post("push_file") {
+                        val ref = param(call, "ref")
+                        val dataPath = jsonContent(call)
+
+                        if (dataPath.bundleId == null) {
+                            throw IllegalArgumentException("Bundle id is not set. Set 'bundle_id' to appropriate value.")
+                        }
+
+                        call.respond(devicesController.pushFile(ref, dataPath.file_name, dataPath.data, dataPath.bundleId))
+                    }
                     post("list_files") {
                         val ref = param(call, "ref")
                         val dataPath = jsonContent(call)
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt
index 630c0805..64100f55 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/controllers/DevicesController.kt
@@ -150,6 +150,11 @@ class DevicesController(private val deviceManager: DeviceManager) {
         return deviceManager.pullFile(ref, dataPath)
     }
 
+    fun pushFile(ref: DeviceRef, fileName: String, data: ByteArray, bundleId: String): EmptyMap {
+        deviceManager.pushFile(ref, fileName, data, bundleId)
+        return happy
+    }
+
     fun openUrl(ref: DeviceRef, url: String) {
         return deviceManager.openUrl(ref, url)
     }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/FileDto.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/FileDto.kt
index 2d8cc466..7a94d985 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/FileDto.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/data/FileDto.kt
@@ -7,5 +7,8 @@ data class FileDto(
     val file_name: String,
 
     @JsonProperty("data")
-    val data: ByteArray
+    val data: ByteArray,
+
+    @JsonProperty("bundle_id")
+    val bundleId: String? = null
 )
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
index 3eea9b7f..56f1b133 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/DevicesNode.kt
@@ -64,6 +64,10 @@ class DevicesNode(
         throw(NotImplementedError("Diagnostic is not supported by physical devices"))
     }
 
+    override fun pushFile(ref: DeviceRef, fileName: String, data: ByteArray, bundleId: String) {
+        throw(NotImplementedError("Push file is not supported by physical devices"))
+    }
+
     override val remoteAddress: String get() = remote.hostName
 
     private val deviceInfoProvider = DeviceInfoProvider(remote)
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
index ba932984..e95effc9 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/ISimulatorsNode.kt
@@ -50,5 +50,6 @@ interface ISimulatorsNode {
     fun uninstallApplication(deviceRef: DeviceRef, bundleId: String)
     fun setEnvironmentVariables(deviceRef: DeviceRef, envs: Map)
     fun updateApplicationPlist(ref: DeviceRef, plistEntry: PlistEntryDTO)
+    fun pushFile(ref: DeviceRef, fileName: String, data: ByteArray, bundleId: String)
     val publicHostName: String
 }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
index afa7d667..12fb31d9 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/SimulatorsNode.kt
@@ -295,6 +295,10 @@ class SimulatorsNode(
         return getDeviceFor(deviceRef).dataContainer(dataPath.bundleId).readFile(dataPath.path)
     }
 
+    override fun pushFile(ref: DeviceRef, fileName: String, data: ByteArray, bundleId: String) {
+        getDeviceFor(ref).dataContainer(bundleId).writeFile(File(fileName), data)
+    }
+
     override fun openUrl(deviceRef: DeviceRef, url: String) {
         getDeviceFor(deviceRef).openUrl(url)
     }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
index a9877189..dc9368ba 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/DeviceManager.kt
@@ -153,6 +153,10 @@ class DeviceManager(
         return nodeRegistry.activeDevices.getNodeFor(ref).pullFile(ref, dataPath)
     }
 
+    fun pushFile(ref: DeviceRef, fileName: String, data: ByteArray, bundleId: String) {
+        nodeRegistry.activeDevices.getNodeFor(ref).pushFile(ref, fileName, data, bundleId)
+    }
+
     fun setEnvironmentVariables(ref: DeviceRef, envs: Map) {
         nodeRegistry.activeDevices.getNodeFor(ref).setEnvironmentVariables(ref, envs)
     }
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainer.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainer.kt
index 622256bc..ef752333 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainer.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/data/DataContainer.kt
@@ -8,6 +8,7 @@ import org.slf4j.LoggerFactory
 import java.io.File
 import java.lang.RuntimeException
 import java.nio.file.Path
+import java.time.Duration
 
 class DataContainer(
     private val remote: IRemote,
@@ -50,6 +51,28 @@ class DataContainer(
         remote.shell("/usr/libexec/PlistBuddy -c 'Add $key $type $value' $expandedPath", false)
     }
 
+    fun writeFile(file: File, data: ByteArray) {
+        val dataContainerFile = File(basePath.absolutePath, file.name)
+
+        if (remote.isLocalhost()) {
+            dataContainerFile.writeBytes(data)
+            logger.debug(logMarker, "Successfully wrote data to file ${dataContainerFile.absolutePath}")
+        } else {
+            writeRemoteFile(file, data, dataContainerFile)
+        }
+    }
+
+    private fun writeRemoteFile(file: File, data: ByteArray, dataContainerFile: File) {
+        val tmpFile = File.createTempFile("${file.nameWithoutExtension}.", ".${file.extension}")
+        try {
+            tmpFile.writeBytes(data)
+            remote.scpToRemoteHost(tmpFile.absolutePath, dataContainerFile.absolutePath, Duration.ofMinutes(1))
+            logger.debug(logMarker, "Successfully wrote data to remote file ${dataContainerFile.absolutePath}")
+        } finally {
+            tmpFile.delete()
+        }
+    }
+
     private fun sshNoEscapingWorkaround(path: String): String {
         // FIXME: fix escaping on ssh side and remove workarounds
         return when {

From 2569119e7d074356a320f5440470f74e1f41e4f3 Mon Sep 17 00:00:00 2001
From: Vyacheslav Frolov 
Date: Mon, 17 Feb 2020 20:55:47 +0000
Subject: [PATCH 131/131] Use xcrun simctl to shutdown simulators

---
 .../host/management/ISimulatorHostChecker.kt             | 4 ++--
 .../automation/deviceserver/ios/fbsimctl/FBSimctl.kt     | 9 +++++----
 .../automation/deviceserver/ios/fbsimctl/IFBSimctl.kt    | 7 ++++---
 .../automation/deviceserver/ios/simulator/Simulator.kt   | 7 ++++++-
 4 files changed, 17 insertions(+), 10 deletions(-)

diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt
index 24b46025..c11775ac 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/host/management/ISimulatorHostChecker.kt
@@ -86,7 +86,7 @@ class SimulatorHostChecker(
             logger.info(logMarker, "Will kill abandoned long living fbsimctl processes")
             remote.pkill("/usr/local/bin/fbsimctl", true)
             logger.info(logMarker, "Will shutdown booted simulators")
-            remote.fbsimctl.shutdownAllBooted()
+            remote.fbsimctl.shutdownAll()
             logger.info(logMarker, "Done shutting down booted simulators")
         } catch (e: Exception) {
             logger.warn(logMarker, "Failed to shutdown simulator because: ${e.javaClass}: message: [${e.message}]")
@@ -149,4 +149,4 @@ class SimulatorHostChecker(
         //  ShowChrome = 1;
         //  SlowMotionAnimation = 0;
     }
-}
\ No newline at end of file
+}
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt
index 6a95f5a1..5feef19c 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/FBSimctl.kt
@@ -102,11 +102,13 @@ class FBSimctl(
         return parser.parseDiagnosticInfo(fbsimctl(cmd = "diagnose", udid = udid))
     }
 
-    override fun shutdown(udid: UDID) {
-        fbsimctl("shutdown", udid, timeOut = SIMULATOR_SHUTDOWN_TIMEOUT, raiseOnError = false)
+    override fun shutdown(udid: UDID): CommandResult {
+        return shellCommand.exec(listOf("/usr/bin/xcrun", "simctl", "shutdown", udid), timeOut = SIMULATOR_SHUTDOWN_TIMEOUT, returnFailure = true)
     }
 
-    override fun shutdownAllBooted() = fbsimctl("--simulators --state=booted shutdown")
+    override fun shutdownAll(): CommandResult {
+        return shellCommand.exec(listOf("/usr/bin/xcrun", "simctl", "shutdown", "all"), timeOut = Duration.ofMinutes(3), returnFailure = true)
+    }
 
     override fun delete(udid: UDID) = fbsimctl("delete", udid)
 
@@ -138,7 +140,6 @@ class FBSimctl(
         timeOut: Duration = Duration.ofSeconds(30),
         raiseOnError: Boolean = false
     ): String {
-
         val fbsimctlCommand = buildFbsimctlCommand(jsonFormat, udid, cmd)
 
         var result = executeCommand(fbsimctlCommand, timeOut)
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctl.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctl.kt
index 99180f6c..715371f2 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctl.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/fbsimctl/IFBSimctl.kt
@@ -1,5 +1,6 @@
 package com.badoo.automation.deviceserver.ios.fbsimctl
 
+import com.badoo.automation.deviceserver.command.CommandResult
 import com.badoo.automation.deviceserver.data.UDID
 import java.io.File
 
@@ -30,9 +31,9 @@ interface IFBSimctl {
     fun eraseSimulator(udid: UDID): String
     fun create(model: String?, os: String?, transitional: Boolean): FBSimctlDevice
     fun diagnose(udid: UDID): FBSimctlDeviceDiagnosticInfo
-    fun shutdown(udid: UDID)
-    fun shutdownAllBooted(): String
+    fun shutdown(udid: UDID): CommandResult
+    fun shutdownAll(): CommandResult
     fun delete(udid: UDID): String
     fun terminateApp(udid: UDID, bundleId: String, raiseOnError: Boolean = false): String
     fun uninstallApp(udid: UDID, bundleId: String)
-}
\ No newline at end of file
+}
diff --git a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
index e92c073a..be5a2866 100644
--- a/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
+++ b/device-server/src/main/kotlin/com/badoo/automation/deviceserver/ios/simulator/Simulator.kt
@@ -287,7 +287,12 @@ class Simulator (
 
     private fun shutdown() {
         logger.info(logMarker, "Shutting down ${this@Simulator}")
-        remote.fbsimctl.shutdown(udid)
+        val result = remote.fbsimctl.shutdown(udid)
+
+        if (!result.isSuccess && !result.stdErr.contains("current state: Shutdown") && !result.stdOut.contains("current state: Shutdown")) {
+            logger.debug(logMarker, "Error occurred while shutting down simulator $udid. Command exit code: ${result.exitCode}. Result stdErr: ${result.stdErr}")
+        }
+
         ignoringErrors { fbsimctlProc.kill() }
 
         pollFor(