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