1 /*
2  * Copyright (C) 2019 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.cloud;
17 
18 import com.android.ddmlib.IDevice;
19 import com.android.tradefed.build.IBuildInfo;
20 import com.android.tradefed.device.DeviceNotAvailableException;
21 import com.android.tradefed.device.IDeviceMonitor;
22 import com.android.tradefed.device.IDeviceStateMonitor;
23 import com.android.tradefed.device.TestDevice;
24 import com.android.tradefed.device.cloud.CommonLogRemoteFileUtil.KnownLogFileEntry;
25 import com.android.tradefed.invoker.RemoteInvocationExecution;
26 import com.android.tradefed.log.ITestLogger;
27 import com.android.tradefed.log.LogUtil.CLog;
28 import com.android.tradefed.result.ByteArrayInputStreamSource;
29 import com.android.tradefed.result.FileInputStreamSource;
30 import com.android.tradefed.result.InputStreamSource;
31 import com.android.tradefed.result.LogDataType;
32 import com.android.tradefed.targetprep.TargetSetupError;
33 import com.android.tradefed.util.CommandResult;
34 import com.android.tradefed.util.CommandStatus;
35 
36 import java.io.File;
37 import java.text.SimpleDateFormat;
38 import java.util.Date;
39 import java.util.HashMap;
40 import java.util.List;
41 import java.util.Map;
42 
43 /**
44  * Representation of the device running inside a remote Cuttlefish VM. It will alter the local
45  * device {@link TestDevice} behavior in some cases to take advantage of the setup.
46  */
47 public class NestedRemoteDevice extends TestDevice {
48 
49     // TODO: Improve the way we associate nested device with their user
50     private static final Map<String, String> IP_TO_USER = new HashMap<>();
51 
52     static {
53         IP_TO_USER.put("127.0.0.1:6520", "vsoc-01");
54         IP_TO_USER.put("127.0.0.1:6521", "vsoc-02");
55         IP_TO_USER.put("127.0.0.1:6522", "vsoc-03");
56         IP_TO_USER.put("127.0.0.1:6523", "vsoc-04");
57         IP_TO_USER.put("127.0.0.1:6524", "vsoc-05");
58         IP_TO_USER.put("127.0.0.1:6525", "vsoc-06");
59         IP_TO_USER.put("127.0.0.1:6526", "vsoc-07");
60         IP_TO_USER.put("127.0.0.1:6527", "vsoc-08");
61         IP_TO_USER.put("127.0.0.1:6528", "vsoc-09");
62         IP_TO_USER.put("127.0.0.1:6529", "vsoc-10");
63         IP_TO_USER.put("127.0.0.1:6530", "vsoc-11");
64         IP_TO_USER.put("127.0.0.1:6531", "vsoc-12");
65         IP_TO_USER.put("127.0.0.1:6532", "vsoc-13");
66         IP_TO_USER.put("127.0.0.1:6533", "vsoc-14");
67         IP_TO_USER.put("127.0.0.1:6534", "vsoc-15");
68     }
69 
70     /** When calling launch_cvd, the launcher.log is populated. */
71     private static final String LAUNCHER_LOG_PATH = "/home/%s/cuttlefish_runtime/launcher.log";
72 
73     /**
74      * Creates a {@link NestedRemoteDevice}.
75      *
76      * @param device the associated {@link IDevice}
77      * @param stateMonitor the {@link IDeviceStateMonitor} mechanism to use
78      * @param allocationMonitor the {@link IDeviceMonitor} to inform of allocation state changes.
79      */
NestedRemoteDevice( IDevice device, IDeviceStateMonitor stateMonitor, IDeviceMonitor allocationMonitor)80     public NestedRemoteDevice(
81             IDevice device, IDeviceStateMonitor stateMonitor, IDeviceMonitor allocationMonitor) {
82         super(device, stateMonitor, allocationMonitor);
83         // TODO: Use IDevice directly
84         if (stateMonitor instanceof NestedDeviceStateMonitor) {
85             ((NestedDeviceStateMonitor) stateMonitor).setDevice(this);
86         }
87     }
88 
89     /** Teardown and restore the virtual device so testing can proceed. */
resetVirtualDevice( ITestLogger logger, IBuildInfo info, boolean resetDueToFailure)90     public final boolean resetVirtualDevice(
91             ITestLogger logger, IBuildInfo info, boolean resetDueToFailure)
92             throws DeviceNotAvailableException {
93         String username = IP_TO_USER.get(getSerialNumber());
94         // stop_cvd
95         String stopCvdCommand = String.format("sudo runuser -l %s -c 'stop_cvd'", username);
96         CommandResult stopCvdRes = getRunUtil().runTimedCmd(60000L, stopCvdCommand.split(" "));
97         if (!CommandStatus.SUCCESS.equals(stopCvdRes.getStatus())) {
98             CLog.e("%s", stopCvdRes.getStderr());
99             // Log 'adb devices' to confirm device is gone
100             CommandResult printAdbDevices = getRunUtil().runTimedCmd(60000L, "adb", "devices");
101             CLog.e("%s\n%s", printAdbDevices.getStdout(), printAdbDevices.getStderr());
102             // Proceed here, device could have been already gone.
103         }
104         // Synchronize this so multiple reset do not occur at the same time inside one VM.
105         synchronized (NestedRemoteDevice.class) {
106             if (resetDueToFailure) {
107                 // Log the common files before restarting otherwise they are lost
108                 logDebugFiles(logger, username);
109             }
110             // Restart the device without re-creating the data partitions.
111             List<String> createCommand =
112                     LaunchCvdHelper.createSimpleDeviceCommand(username, true, false, false);
113             CommandResult createRes =
114                     getRunUtil()
115                             .runTimedCmd(
116                                     RemoteInvocationExecution.LAUNCH_EXTRA_DEVICE,
117                                     "sh",
118                                     "-c",
119                                     String.join(" ", createCommand));
120             if (!CommandStatus.SUCCESS.equals(createRes.getStatus())) {
121                 CLog.e("%s", createRes.getStderr());
122                 captureLauncherLog(username, logger);
123                 return false;
124             }
125             // Wait for the device to start for real.
126             getRunUtil().sleep(5000);
127             waitForDeviceAvailable();
128             // Re-init the freshly started device.
129             return reInitDevice(info);
130         }
131     }
132 
133     /**
134      * Log the runtime files of the virtual device before resetting it since they will be deleted.
135      */
logDebugFiles(ITestLogger logger, String username)136     private void logDebugFiles(ITestLogger logger, String username) {
137         List<KnownLogFileEntry> toFetch =
138                 CommonLogRemoteFileUtil.KNOWN_FILES_TO_FETCH.get(getOptions().getInstanceType());
139         if (toFetch != null) {
140             SimpleDateFormat formatter = new SimpleDateFormat("HH:mm:ss");
141             for (KnownLogFileEntry entry : toFetch) {
142                 File toLog = new File(String.format(entry.path, username));
143                 if (!toLog.exists()) {
144                     continue;
145                 }
146                 try (FileInputStreamSource source = new FileInputStreamSource(toLog)) {
147                     logger.testLog(
148                             String.format(
149                                     "before_reset_%s_%s_%s",
150                                     toLog.getName(), username, formatter.format(new Date())),
151                             entry.type,
152                             source);
153                 }
154             }
155         }
156         logBugreport(String.format("before_reset_%s_bugreport", username), logger);
157     }
158 
159     /** TODO: Re-run the target_preparation. */
reInitDevice(IBuildInfo info)160     private boolean reInitDevice(IBuildInfo info) throws DeviceNotAvailableException {
161         // Reset recovery since it's a new device
162         setRecoveryMode(RecoveryMode.AVAILABLE);
163         try {
164             preInvocationSetup(info);
165         } catch (TargetSetupError e) {
166             CLog.e("Failed to re-init the device %s", getSerialNumber());
167             CLog.e(e);
168             return false;
169         }
170         // Success
171         return true;
172     }
173 
174     /** Capture and log the launcher.log to debug why the device didn't start properly. */
captureLauncherLog(String username, ITestLogger logger)175     private void captureLauncherLog(String username, ITestLogger logger) {
176         String logName = String.format("launcher_log_failure_%s", username);
177         File launcherLog = new File(String.format(LAUNCHER_LOG_PATH, username));
178         if (!launcherLog.exists()) {
179             CLog.e("%s doesn't exists, skip logging it.", launcherLog.getAbsolutePath());
180             return;
181         }
182         // TF runs as the primary user and may get a permission denied to read the launcher.log of
183         // other users. So use the shell to cat the file content.
184         CommandResult readLauncherLogRes =
185                 getRunUtil()
186                         .runTimedCmd(
187                                 60000L,
188                                 "sudo",
189                                 "runuser",
190                                 "-l",
191                                 username,
192                                 "-c",
193                                 String.format("'cat %s'", launcherLog.getAbsolutePath()));
194         if (!CommandStatus.SUCCESS.equals(readLauncherLogRes.getStatus())) {
195             CLog.e(
196                     "Failed to read Launcher.log content due to: %s",
197                     readLauncherLogRes.getStderr());
198             return;
199         }
200         String content = readLauncherLogRes.getStdout();
201         try (InputStreamSource source = new ByteArrayInputStreamSource(content.getBytes())) {
202             logger.testLog(logName, LogDataType.TEXT, source);
203         }
204         CLog.d("See %s for the launcher.log that failed to start.", logName);
205     }
206 }
207