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