1 /* 2 * Copyright (C) 2016 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.tradefed.device; 17 18 import com.android.ddmlib.IDevice; 19 import com.android.tradefed.device.TestDeviceOptions.InstanceType; 20 import com.android.tradefed.log.LogUtil.CLog; 21 import com.android.tradefed.util.CommandResult; 22 import com.android.tradefed.util.CommandStatus; 23 import com.android.tradefed.util.FileUtil; 24 import com.android.tradefed.util.IRunUtil; 25 import com.android.tradefed.util.RunUtil; 26 27 import java.io.File; 28 import java.io.IOException; 29 import java.util.regex.Matcher; 30 import java.util.regex.Pattern; 31 32 /** 33 * Implementation of a {@link ITestDevice} for a full stack android device connected via 34 * adb connect. 35 * Assume the device serial will be in the format <hostname>:<portnumber> in adb. 36 */ 37 public class RemoteAndroidDevice extends TestDevice { 38 public static final long WAIT_FOR_ADB_CONNECT = 2 * 60 * 1000; 39 40 protected static final long RETRY_INTERVAL_MS = 5000; 41 protected static final int MAX_RETRIES = 5; 42 protected static final long DEFAULT_SHORT_CMD_TIMEOUT = 20 * 1000; 43 44 private static final String ADB_SUCCESS_CONNECT_TAG = "connected to"; 45 private static final String ADB_ALREADY_CONNECTED_TAG = "already"; 46 private static final String ADB_CONN_REFUSED = "Connection refused"; 47 48 private static final Pattern IP_PATTERN = 49 Pattern.compile(ManagedTestDeviceFactory.IPADDRESS_PATTERN); 50 51 private File mAdbConnectLogs = null; 52 53 /** 54 * Creates a {@link RemoteAndroidDevice}. 55 * 56 * @param device the associated {@link IDevice} 57 * @param stateMonitor the {@link IDeviceStateMonitor} mechanism to use 58 * @param allocationMonitor the {@link IDeviceMonitor} to inform of allocation state changes. 59 */ RemoteAndroidDevice(IDevice device, IDeviceStateMonitor stateMonitor, IDeviceMonitor allocationMonitor)60 public RemoteAndroidDevice(IDevice device, IDeviceStateMonitor stateMonitor, 61 IDeviceMonitor allocationMonitor) { 62 super(device, stateMonitor, allocationMonitor); 63 } 64 65 @Override postInvocationTearDown()66 public void postInvocationTearDown() { 67 super.postInvocationTearDown(); 68 FileUtil.deleteFile(mAdbConnectLogs); 69 } 70 71 /** 72 * {@inheritDoc} 73 */ 74 @Override postAdbRootAction()75 public void postAdbRootAction() throws DeviceNotAvailableException { 76 // attempt to reconnect first to make sure we didn't loose the connection because of 77 // adb root. 78 adbTcpConnect(getHostName(), getPortNum()); 79 waitForAdbConnect(WAIT_FOR_ADB_CONNECT); 80 } 81 82 /** 83 * {@inheritDoc} 84 */ 85 @Override postAdbUnrootAction()86 public void postAdbUnrootAction() throws DeviceNotAvailableException { 87 // attempt to reconnect first to make sure we didn't loose the connection because of 88 // adb unroot. 89 adbTcpConnect(getHostName(), getPortNum()); 90 waitForAdbConnect(WAIT_FOR_ADB_CONNECT); 91 } 92 93 /** {@inheritDoc} */ 94 @Override postAdbReboot()95 protected void postAdbReboot() throws DeviceNotAvailableException { 96 super.postAdbReboot(); 97 // A remote nested device does not loose the ssh bridge when rebooted only adb connect is 98 // required. 99 InstanceType type = mOptions.getInstanceType(); 100 if (InstanceType.CUTTLEFISH.equals(type) 101 || InstanceType.REMOTE_NESTED_AVD.equals(type) 102 || InstanceType.EMULATOR.equals(type)) { 103 adbTcpConnect(getHostName(), getPortNum()); 104 waitForAdbConnect(WAIT_FOR_ADB_CONNECT); 105 } 106 } 107 108 /** {@inheritDoc} */ 109 @Override recoverDevice()110 public void recoverDevice() throws DeviceNotAvailableException { 111 // If device is not in use (TcpDevice) do not attempt reconnection, it will fail 112 // device in use will not be of the TcpDevice type. 113 if (!(getIDevice() instanceof TcpDevice)) { 114 // Before attempting standard recovery, reconnect the device. 115 adbTcpConnect(getHostName(), getPortNum()); 116 waitForAdbConnect(WAIT_FOR_ADB_CONNECT); 117 } 118 // Standard recovery 119 super.recoverDevice(); 120 } 121 122 /** 123 * Return the hostname associated with the device. Extracted from the serial. 124 */ getHostName()125 public String getHostName() { 126 if (!checkSerialFormatValid(getSerialNumber())) { 127 throw new RuntimeException( 128 String.format("Serial Format is unexpected: %s " 129 + "should look like <hostname>:<port>", getSerialNumber())); 130 } 131 return getSerialNumber().split(":")[0]; 132 } 133 134 /** 135 * Return the port number asociated with the device. Extracted from the serial. 136 */ getPortNum()137 public String getPortNum() { 138 if (!checkSerialFormatValid(getSerialNumber())) { 139 throw new RuntimeException( 140 String.format("Serial Format is unexpected: %s " 141 + "should look like <hostname>:<port>", getSerialNumber())); 142 } 143 return getSerialNumber().split(":")[1]; 144 } 145 146 /** 147 * Check if the format of the serial is as expected <hostname>:port 148 * 149 * @return true if the format is valid, false otherwise. 150 */ checkSerialFormatValid(String serialString)151 public static boolean checkSerialFormatValid(String serialString) { 152 String[] serial = serialString.split(":"); 153 if (serial.length == 2) { 154 // Check first part is an IP 155 Matcher match = IP_PATTERN.matcher(serial[0]); 156 if (!match.find()) { 157 return false; 158 } 159 // Check second part if a port 160 try { 161 Integer.parseInt(serial[1]); 162 return true; 163 } catch (NumberFormatException nfe) { 164 return false; 165 } 166 } 167 return false; 168 } 169 170 /** 171 * Helper method to adb connect to a given tcp ip Android device 172 * 173 * @param host the hostname/ip of a tcp/ip Android device 174 * @param port the port number of a tcp/ip device 175 * @return true if we successfully connected to the device, false 176 * otherwise. 177 */ adbTcpConnect(String host, String port)178 public boolean adbTcpConnect(String host, String port) { 179 for (int i = 0; i < MAX_RETRIES; i++) { 180 CommandResult result = adbConnect(host, port); 181 if (CommandStatus.SUCCESS.equals(result.getStatus()) && 182 result.getStdout().contains(ADB_SUCCESS_CONNECT_TAG)) { 183 CLog.d( 184 "adb connect output: status: %s stdout: %s", 185 result.getStatus(), result.getStdout()); 186 187 // It is possible to get a positive result without it being connected because of 188 // the ssh bridge. Retrying to get confirmation, and expecting "already connected". 189 if(confirmAdbTcpConnect(host, port)) { 190 return true; 191 } 192 } else if (CommandStatus.SUCCESS.equals(result.getStatus()) && 193 result.getStdout().contains(ADB_CONN_REFUSED)) { 194 // If we find "Connection Refused", we bail out directly as more connect won't help 195 return false; 196 } 197 CLog.d("adb connect output: status: %s stdout: %s stderr: %s, retrying.", 198 result.getStatus(), result.getStdout(), result.getStderr()); 199 getRunUtil().sleep((i + 1) * RETRY_INTERVAL_MS); 200 } 201 return false; 202 } 203 confirmAdbTcpConnect(String host, String port)204 private boolean confirmAdbTcpConnect(String host, String port) { 205 CommandResult resultConfirmation = adbConnect(host, port); 206 if (CommandStatus.SUCCESS.equals(resultConfirmation.getStatus()) 207 && resultConfirmation.getStdout().contains(ADB_ALREADY_CONNECTED_TAG)) { 208 CLog.d("adb connect confirmed:\nstdout: %s\n", resultConfirmation.getStdout()); 209 return true; 210 } else { 211 CLog.d("adb connect confirmation failed:\nstatus:%s\nstdout: %s\nsterr: %s", 212 resultConfirmation.getStatus(), resultConfirmation.getStdout(), 213 resultConfirmation.getStderr()); 214 } 215 return false; 216 } 217 218 /** 219 * Helper method to adb disconnect from a given tcp ip Android device 220 * 221 * @param host the hostname/ip of a tcp/ip Android device 222 * @param port the port number of a tcp/ip device 223 * @return true if we successfully disconnected to the device, false 224 * otherwise. 225 */ adbTcpDisconnect(String host, String port)226 public boolean adbTcpDisconnect(String host, String port) { 227 CommandResult result = getRunUtil().runTimedCmd(DEFAULT_SHORT_CMD_TIMEOUT, "adb", 228 "disconnect", 229 String.format("%s:%s", host, port)); 230 return CommandStatus.SUCCESS.equals(result.getStatus()); 231 } 232 233 /** 234 * Check if the adb connection is enabled. 235 */ waitForAdbConnect(final long waitTime)236 public void waitForAdbConnect(final long waitTime) throws DeviceNotAvailableException { 237 CLog.i("Waiting %d ms for adb connection.", waitTime); 238 long startTime = System.currentTimeMillis(); 239 while (System.currentTimeMillis() - startTime < waitTime) { 240 if (confirmAdbTcpConnect(getHostName(), getPortNum())) { 241 CLog.d("Adb connection confirmed."); 242 return; 243 } 244 getRunUtil().sleep(RETRY_INTERVAL_MS); 245 } 246 throw new DeviceNotAvailableException( 247 String.format("No adb connection after %sms.", waitTime), getSerialNumber()); 248 } 249 250 /** 251 * {@inheritDoc} 252 */ 253 @Override isEncryptionSupported()254 public boolean isEncryptionSupported() { 255 // Prevent device from being encrypted since we won't have a way to decrypt on Remote 256 // devices since fastboot cannot be use remotely 257 return false; 258 } 259 260 /** 261 * Give a receiver file where we can store all the adb connection logs for debugging purpose. 262 */ setAdbLogFile(File adbLogFile)263 public void setAdbLogFile(File adbLogFile) { 264 mAdbConnectLogs = adbLogFile; 265 } 266 267 /** 268 * {@inheritDoc} 269 */ 270 @Override getMacAddress()271 public String getMacAddress() { 272 return null; 273 } 274 275 /** Run adb connect. */ adbConnect(String host, String port)276 private CommandResult adbConnect(String host, String port) { 277 IRunUtil runUtil = getRunUtil(); 278 if (mAdbConnectLogs != null) { 279 runUtil = new RunUtil(); 280 runUtil.setEnvVariable("ADB_TRACE", "1"); 281 } 282 CommandResult result = 283 runUtil.runTimedCmd( 284 DEFAULT_SHORT_CMD_TIMEOUT, 285 "adb", 286 "connect", 287 String.format("%s:%s", host, port)); 288 if (mAdbConnectLogs != null) { 289 try { 290 FileUtil.writeToFile(result.getStderr(), mAdbConnectLogs, true); 291 FileUtil.writeToFile( 292 "\n======= SEPARATOR OF ATTEMPTS =====\n", mAdbConnectLogs, true); 293 } catch (IOException e) { 294 CLog.e(e); 295 } 296 } 297 return result; 298 } 299 } 300