1 /*
2  * Copyright (C) 2010 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.AdbCommandRejectedException;
19 import com.android.ddmlib.IDevice;
20 import com.android.ddmlib.Log;
21 import com.android.ddmlib.TimeoutException;
22 import com.android.tradefed.config.Option;
23 import com.android.tradefed.log.LogUtil.CLog;
24 import com.android.tradefed.util.CommandResult;
25 import com.android.tradefed.util.CommandStatus;
26 import com.android.tradefed.util.IRunUtil;
27 import com.android.tradefed.util.RunUtil;
28 
29 import java.io.IOException;
30 import java.util.concurrent.ExecutionException;
31 
32 /**
33  * A simple implementation of a {@link IDeviceRecovery} that waits for device to be online and
34  * respond to simple commands.
35  */
36 public class WaitDeviceRecovery implements IDeviceRecovery {
37 
38     private static final String LOG_TAG = "WaitDeviceRecovery";
39 
40     /** the time in ms to wait before beginning recovery attempts */
41     protected static final long INITIAL_PAUSE_TIME = 5 * 1000;
42 
43     /**
44      * The number of attempts to check if device is in bootloader.
45      * <p/>
46      * Exposed for unit testing
47      */
48     public static final int BOOTLOADER_POLL_ATTEMPTS = 3;
49 
50     // TODO: add a separate configurable timeout per operation
51     @Option(name="online-wait-time",
52             description="maximum time in ms to wait for device to come online.")
53     protected long mOnlineWaitTime = 60 * 1000;
54     @Option(name="device-wait-time",
55             description="maximum time in ms to wait for a single device recovery command.")
56     protected long mWaitTime = 4 * 60 * 1000;
57 
58     @Option(name="bootloader-wait-time",
59             description="maximum time in ms to wait for device to be in fastboot.")
60     protected long mBootloaderWaitTime = 30 * 1000;
61 
62     @Option(name="shell-wait-time",
63             description="maximum time in ms to wait for device shell to be responsive.")
64     protected long mShellWaitTime = 30 * 1000;
65 
66     @Option(name="fastboot-wait-time",
67             description="maximum time in ms to wait for a fastboot command result.")
68     protected long mFastbootWaitTime = 30 * 1000;
69 
70     @Option(name = "min-battery-after-recovery",
71             description = "require a min battery level after successful recovery, " +
72                           "default to 0 for ignoring.")
73     protected int mRequiredMinBattery = 0;
74 
75     @Option(name = "disable-unresponsive-reboot",
76             description = "If this is set, we will not attempt to reboot an unresponsive device" +
77             "that is in userspace.  Note that this will have no effect if the device is in " +
78             "fastboot or is expected to be in fastboot.")
79     protected boolean mDisableUnresponsiveReboot = false;
80 
81     private String mFastbootPath = "fastboot";
82 
83     /**
84      * Get the {@link RunUtil} instance to use.
85      * <p/>
86      * Exposed for unit testing.
87      */
getRunUtil()88     protected IRunUtil getRunUtil() {
89         return RunUtil.getDefault();
90     }
91 
92     /**
93      * Sets the maximum time in ms to wait for a single device recovery command.
94      */
setWaitTime(long waitTime)95     void setWaitTime(long waitTime) {
96         mWaitTime = waitTime;
97     }
98 
99     /**
100      * {@inheritDoc}
101      */
102     @Override
setFastbootPath(String fastbootPath)103     public void setFastbootPath(String fastbootPath) {
104         mFastbootPath = fastbootPath;
105     }
106 
107     /**
108      * {@inheritDoc}
109      */
110     @Override
recoverDevice(IDeviceStateMonitor monitor, boolean recoverUntilOnline)111     public void recoverDevice(IDeviceStateMonitor monitor, boolean recoverUntilOnline)
112             throws DeviceNotAvailableException {
113         // device may have just gone offline
114         // sleep a small amount to give ddms state a chance to settle
115         // TODO - see if there is better way to handle this
116         Log.i(LOG_TAG, String.format("Pausing for %d for %s to recover",
117                 INITIAL_PAUSE_TIME, monitor.getSerialNumber()));
118         getRunUtil().sleep(INITIAL_PAUSE_TIME);
119 
120         // ensure bootloader state is updated
121         monitor.waitForDeviceBootloaderStateUpdate();
122 
123         if (monitor.getDeviceState().equals(TestDeviceState.FASTBOOT)) {
124             Log.i(LOG_TAG, String.format(
125                     "Found device %s in fastboot but expected online. Rebooting...",
126                     monitor.getSerialNumber()));
127             // TODO: retry if failed
128             getRunUtil().runTimedCmd(mFastbootWaitTime, mFastbootPath, "-s",
129                     monitor.getSerialNumber(), "reboot");
130         }
131 
132         // wait for device online
133         IDevice device = monitor.waitForDeviceOnline(mOnlineWaitTime);
134         if (device == null) {
135             handleDeviceNotAvailable(monitor, recoverUntilOnline);
136             // function returning implies that recovery is successful, check battery level here
137             checkMinBatteryLevel(getDeviceAfterRecovery(monitor));
138             return;
139         }
140         // occasionally device is erroneously reported as online - double check that we can shell
141         // into device
142         if (!monitor.waitForDeviceShell(mShellWaitTime)) {
143             // treat this as a not available device
144             handleDeviceNotAvailable(monitor, recoverUntilOnline);
145             checkMinBatteryLevel(getDeviceAfterRecovery(monitor));
146             return;
147         }
148 
149         if (!recoverUntilOnline) {
150             if (monitor.waitForDeviceAvailable(mWaitTime) == null) {
151                 // device is online but not responsive
152                 handleDeviceUnresponsive(device, monitor);
153             }
154         }
155         // do a final check here when all previous if blocks are skipped or the last
156         // handleDeviceUnresponsive was successful
157         checkMinBatteryLevel(getDeviceAfterRecovery(monitor));
158     }
159 
getDeviceAfterRecovery(IDeviceStateMonitor monitor)160     private IDevice getDeviceAfterRecovery(IDeviceStateMonitor monitor)
161             throws DeviceNotAvailableException {
162         IDevice device = monitor.waitForDeviceOnline(mOnlineWaitTime);
163         if (device == null) {
164             throw new DeviceNotAvailableException(
165                     "Device still not online after successful recovery", monitor.getSerialNumber());
166         }
167         return device;
168     }
169 
170     /**
171      * Checks if device battery level meets min requirement
172      * @param device
173      * @throws DeviceNotAvailableException if battery level cannot be read or lower than min
174      */
checkMinBatteryLevel(IDevice device)175     protected void checkMinBatteryLevel(IDevice device) throws DeviceNotAvailableException {
176         if (mRequiredMinBattery <= 0) {
177             // don't do anything if check is not required
178             return;
179         }
180         try {
181             Integer level = device.getBattery().get();
182             if (level == null) {
183                 // can't read battery level but we are requiring a min, reject
184                 // device
185                 throw new DeviceNotAvailableException(
186                         "Cannot read battery level but a min is required",
187                         device.getSerialNumber());
188             } else if (level < mRequiredMinBattery) {
189                 throw new DeviceNotAvailableException(String.format(
190                         "After recovery, device battery level %d is lower than required minimum %d",
191                         level, mRequiredMinBattery), device.getSerialNumber());
192             }
193             return;
194         } catch (InterruptedException | ExecutionException e) {
195             throw new DeviceNotAvailableException("exception while reading battery level", e,
196                     device.getSerialNumber());
197         }
198     }
199 
200     /**
201      * Handle situation where device is online but unresponsive.
202      * @param monitor
203      * @throws DeviceNotAvailableException
204      */
handleDeviceUnresponsive(IDevice device, IDeviceStateMonitor monitor)205     protected void handleDeviceUnresponsive(IDevice device, IDeviceStateMonitor monitor)
206             throws DeviceNotAvailableException {
207         if (!mDisableUnresponsiveReboot) {
208             Log.i(LOG_TAG, String.format(
209                     "Device %s unresponsive. Rebooting...", monitor.getSerialNumber()));
210             rebootDevice(device);
211             IDevice newdevice = monitor.waitForDeviceOnline(mOnlineWaitTime);
212             if (newdevice == null) {
213                 handleDeviceNotAvailable(monitor, false);
214                 return;
215             }
216             if (monitor.waitForDeviceAvailable(mWaitTime) != null) {
217                 return;
218             }
219         }
220         // If no reboot was done, waitForDeviceAvailable has already been checked.
221         throw new DeviceUnresponsiveException(String.format(
222                 "Device %s is online but unresponsive", monitor.getSerialNumber()),
223                 monitor.getSerialNumber());
224     }
225 
226     /**
227      * Handle situation where device is not available.
228      *
229      * @param monitor the {@link IDeviceStateMonitor}
230      * @param recoverTillOnline if true this method should return if device is online, and not
231      * check for responsiveness
232      * @throws DeviceNotAvailableException
233      */
handleDeviceNotAvailable(IDeviceStateMonitor monitor, boolean recoverTillOnline)234     protected void handleDeviceNotAvailable(IDeviceStateMonitor monitor, boolean recoverTillOnline)
235             throws DeviceNotAvailableException {
236         throw new DeviceNotAvailableException(String.format("Could not find device %s",
237                 monitor.getSerialNumber()), monitor.getSerialNumber());
238     }
239 
240     /**
241      * {@inheritDoc}
242      */
243     @Override
recoverDeviceBootloader(final IDeviceStateMonitor monitor)244     public void recoverDeviceBootloader(final IDeviceStateMonitor monitor)
245             throws DeviceNotAvailableException {
246         // device may have just gone offline
247         // wait a small amount to give device state a chance to settle
248         // TODO - see if there is better way to handle this
249         Log.i(LOG_TAG, String.format("Pausing for %d for %s to recover",
250                 INITIAL_PAUSE_TIME, monitor.getSerialNumber()));
251         getRunUtil().sleep(INITIAL_PAUSE_TIME);
252 
253         // poll and wait for device to return to valid state
254         long pollTime = mBootloaderWaitTime / BOOTLOADER_POLL_ATTEMPTS;
255         for (int i=0; i < BOOTLOADER_POLL_ATTEMPTS; i++) {
256             if (monitor.waitForDeviceBootloader(pollTime)) {
257                 handleDeviceBootloaderUnresponsive(monitor);
258                 // passed above check, abort
259                 return;
260             } else if (monitor.getDeviceState() == TestDeviceState.ONLINE) {
261                 handleDeviceOnlineExpectedBootloader(monitor);
262                 return;
263             }
264         }
265         handleDeviceBootloaderNotAvailable(monitor);
266     }
267 
268     /**
269      * Handle condition where device is online, but should be in bootloader state.
270      * <p/>
271      * If this method
272      * @param monitor
273      * @throws DeviceNotAvailableException
274      */
handleDeviceOnlineExpectedBootloader(final IDeviceStateMonitor monitor)275     protected void handleDeviceOnlineExpectedBootloader(final IDeviceStateMonitor monitor)
276             throws DeviceNotAvailableException {
277         Log.i(LOG_TAG, String.format("Found device %s online but expected fastboot.",
278             monitor.getSerialNumber()));
279         // call waitForDeviceOnline to get handle to IDevice
280         IDevice device = monitor.waitForDeviceOnline(mOnlineWaitTime);
281         if (device == null) {
282             handleDeviceBootloaderNotAvailable(monitor);
283             return;
284         }
285         rebootDeviceIntoBootloader(device);
286         if (!monitor.waitForDeviceBootloader(mBootloaderWaitTime)) {
287             throw new DeviceNotAvailableException(String.format(
288                     "Device %s not in bootloader after reboot", monitor.getSerialNumber()),
289                     monitor.getSerialNumber());
290         }
291     }
292 
293     /**
294      * @param monitor
295      * @throws DeviceNotAvailableException
296      */
handleDeviceBootloaderUnresponsive(IDeviceStateMonitor monitor)297     protected void handleDeviceBootloaderUnresponsive(IDeviceStateMonitor monitor)
298             throws DeviceNotAvailableException {
299         CLog.i("Found device %s in fastboot but potentially unresponsive.",
300                 monitor.getSerialNumber());
301         // TODO: retry reboot
302         getRunUtil().runTimedCmd(mFastbootWaitTime, mFastbootPath, "-s", monitor.getSerialNumber(),
303                 "reboot-bootloader");
304         // wait for device to reboot
305         monitor.waitForDeviceNotAvailable(20*1000);
306         if (!monitor.waitForDeviceBootloader(mBootloaderWaitTime)) {
307             throw new DeviceNotAvailableException(String.format(
308                     "Device %s not in bootloader after reboot", monitor.getSerialNumber()),
309                     monitor.getSerialNumber());
310         }
311         // running a meaningless command just to see whether the device is responsive.
312         CommandResult result = getRunUtil().runTimedCmd(mFastbootWaitTime, mFastbootPath, "-s",
313                 monitor.getSerialNumber(), "getvar", "product");
314         if (result.getStatus().equals(CommandStatus.TIMED_OUT)) {
315             throw new DeviceNotAvailableException(String.format(
316                     "Device %s is in fastboot but unresponsive", monitor.getSerialNumber()),
317                     monitor.getSerialNumber());
318         }
319     }
320 
321     /**
322      * Reboot device into bootloader.
323      *
324      * @param device the {@link IDevice} to reboot.
325      */
rebootDeviceIntoBootloader(IDevice device)326     protected void rebootDeviceIntoBootloader(IDevice device) {
327         try {
328             device.reboot("bootloader");
329         } catch (IOException e) {
330             Log.w(LOG_TAG, String.format("failed to reboot %s: %s", device.getSerialNumber(),
331                     e.getMessage()));
332         } catch (TimeoutException e) {
333             Log.w(LOG_TAG, String.format("failed to reboot %s: timeout", device.getSerialNumber()));
334         } catch (AdbCommandRejectedException e) {
335             Log.w(LOG_TAG, String.format("failed to reboot %s: %s", device.getSerialNumber(),
336                     e.getMessage()));
337         }
338     }
339 
340     /**
341      * Reboot device into bootloader.
342      *
343      * @param device the {@link IDevice} to reboot.
344      */
rebootDevice(IDevice device)345     protected void rebootDevice(IDevice device) {
346         try {
347             device.reboot(null);
348         } catch (IOException e) {
349             Log.w(LOG_TAG, String.format("failed to reboot %s: %s", device.getSerialNumber(),
350                     e.getMessage()));
351         } catch (TimeoutException e) {
352             Log.w(LOG_TAG, String.format("failed to reboot %s: timeout", device.getSerialNumber()));
353         } catch (AdbCommandRejectedException e) {
354             Log.w(LOG_TAG, String.format("failed to reboot %s: %s", device.getSerialNumber(),
355                     e.getMessage()));
356         }
357     }
358 
359     /**
360      * Handle situation where device is not available when expected to be in bootloader.
361      *
362      * @param monitor the {@link IDeviceStateMonitor}
363      * @throws DeviceNotAvailableException
364      */
handleDeviceBootloaderNotAvailable(final IDeviceStateMonitor monitor)365     protected void handleDeviceBootloaderNotAvailable(final IDeviceStateMonitor monitor)
366             throws DeviceNotAvailableException {
367         throw new DeviceNotAvailableException(String.format(
368                 "Could not find device %s in bootloader", monitor.getSerialNumber()),
369                 monitor.getSerialNumber());
370     }
371 
372     /**
373      * {@inheritDoc}
374      */
375     @Override
recoverDeviceRecovery(IDeviceStateMonitor monitor)376     public void recoverDeviceRecovery(IDeviceStateMonitor monitor)
377             throws DeviceNotAvailableException {
378         throw new DeviceNotAvailableException("device recovery not implemented",
379                 monitor.getSerialNumber());
380     }
381 }
382