1 /*
2  * Copyright (C) 2012 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 
17 package com.android.tradefed.targetprep;
18 
19 import static com.android.tradefed.targetprep.UserHelper.RUN_TESTS_AS_USER_KEY;
20 
21 import com.android.annotations.VisibleForTesting;
22 import com.android.tradefed.config.Option;
23 import com.android.tradefed.config.OptionClass;
24 import com.android.tradefed.device.BackgroundDeviceAction;
25 import com.android.tradefed.device.CollectingOutputReceiver;
26 import com.android.tradefed.device.DeviceNotAvailableException;
27 import com.android.tradefed.device.ITestDevice;
28 import com.android.tradefed.invoker.TestInformation;
29 import com.android.tradefed.log.LogUtil.CLog;
30 import com.android.tradefed.result.error.DeviceErrorIdentifier;
31 import com.android.tradefed.util.CommandResult;
32 import com.android.tradefed.util.CommandStatus;
33 import com.android.tradefed.util.RunUtil;
34 
35 import java.util.ArrayList;
36 import java.util.HashMap;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.concurrent.TimeUnit;
40 
41 @OptionClass(alias = "run-command")
42 public class RunCommandTargetPreparer extends BaseTargetPreparer {
43 
44     @Option(name = "run-command", description = "adb shell command to run")
45     private List<String> mCommands = new ArrayList<String>();
46 
47     @Option(name = "run-bg-command", description = "Command to run repeatedly in the"
48             + " device background. Can be repeated to run multiple commands"
49             + " in the background.")
50     private List<String> mBgCommands = new ArrayList<String>();
51 
52     @Option(name = "hide-bg-output", description = "if true, don't log background command output")
53     private boolean mHideBgOutput = false;
54 
55     @Option(name = "teardown-command", description = "adb shell command to run at teardown time")
56     private List<String> mTeardownCommands = new ArrayList<String>();
57 
58     @Option(name = "delay-after-commands",
59             description = "Time to delay after running commands, in msecs")
60     private long mDelayMsecs = 0;
61 
62     @Option(name = "run-command-timeout",
63             description = "Timeout for execute shell command",
64             isTimeVal = true)
65     private long mRunCmdTimeout = 0;
66 
67     @Option(
68             name = "teardown-command-timeout",
69             description = "Timeout for execute shell teardown command",
70             isTimeVal = true)
71     private long mTeardownCmdTimeout = 0;
72 
73     @Option(name = "throw-if-cmd-fail", description = "Whether or not to throw if a command fails")
74     private boolean mThrowIfFailed = false;
75 
76     @Option(
77             name = "log-command-output",
78             description = "Whether or not to always log the commands output")
79     private boolean mLogOutput = false;
80 
81     @Option(
82             name = "test-user-token",
83             description =
84                     "When set, that token will be replaced by the id of the user running the test."
85                         + " For example, if it's set as %TEST_USER%, a command that uses it could"
86                         + " be written as something like 'cmd self-destruct --user %TEST_USER%'.")
87     private String mTestUserToken = "";
88 
89     private Map<BackgroundDeviceAction, CollectingOutputReceiver> mBgDeviceActionsMap =
90             new HashMap<>();
91 
92     private final List<String> mExecutedCommands = new ArrayList<>();
93 
94     private String mTestUser;
95 
96     /** {@inheritDoc} */
97     @Override
setUp(TestInformation testInfo)98     public void setUp(TestInformation testInfo)
99             throws TargetSetupError, DeviceNotAvailableException {
100         ITestDevice device = getDevice(testInfo);
101 
102         if (!mTestUserToken.isEmpty()) {
103             mTestUser = testInfo.properties().get(RUN_TESTS_AS_USER_KEY);
104             if (mTestUser == null || mTestUser.isEmpty()) {
105                 // Ideally it should be just "cur", but not all commands support that
106                 mTestUser = String.valueOf(device.getCurrentUser());
107             }
108             CLog.d("Will replace '%s' by '%s' on all commands", mTestUserToken, mTestUser);
109         }
110 
111         for (String rawCmd : mBgCommands) {
112             String bgCmd = resolveCommand(rawCmd);
113             mExecutedCommands.add(bgCmd);
114             CollectingOutputReceiver receiver = new CollectingOutputReceiver();
115             BackgroundDeviceAction mBgDeviceAction =
116                     new BackgroundDeviceAction(bgCmd, bgCmd, device, receiver, 0);
117             mBgDeviceAction.start();
118             mBgDeviceActionsMap.put(mBgDeviceAction, receiver);
119         }
120 
121         for (String rawCmd : mCommands) {
122             String cmd = resolveCommand(rawCmd);
123             mExecutedCommands.add(cmd);
124             CommandResult result;
125             // Shell v2 with command status checks
126             if (mRunCmdTimeout > 0) {
127                 result =
128                         device.executeShellV2Command(cmd, mRunCmdTimeout, TimeUnit.MILLISECONDS, 0);
129             } else {
130                 result = device.executeShellV2Command(cmd);
131             }
132             // Ensure the command ran successfully.
133             if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
134                 if (mThrowIfFailed) {
135                     throw new TargetSetupError(
136                             String.format(
137                                     "Failed to run '%s' without error. stdout: '%s'\nstderr: '%s'",
138                                     cmd, result.getStdout(), result.getStderr()),
139                             device.getDeviceDescriptor(),
140                             DeviceErrorIdentifier.SHELL_COMMAND_ERROR);
141                 } else {
142                     CLog.d(
143                             "cmd: '%s' failed, returned:\nstdout:%s\nstderr:%s",
144                             cmd, result.getStdout(), result.getStderr());
145                 }
146             } else if (mLogOutput) {
147                 CLog.d(
148                         "cmd: '%s', returned:\nstdout:%s\nstderr:%s",
149                         cmd, result.getStdout(), result.getStderr());
150             }
151         }
152 
153         if (mDelayMsecs > 0) {
154             CLog.d("Sleeping %d msecs on device %s", mDelayMsecs, device.getSerialNumber());
155             RunUtil.getDefault().sleep(mDelayMsecs);
156         }
157     }
158 
159     @Override
tearDown(TestInformation testInfo, Throwable e)160     public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
161         for (Map.Entry<BackgroundDeviceAction, CollectingOutputReceiver> bgAction :
162                 mBgDeviceActionsMap.entrySet()) {
163             if (!mHideBgOutput) {
164                 CLog.d("Background command output : %s", bgAction.getValue().getOutput());
165             }
166             bgAction.getKey().cancel();
167         }
168         if (e instanceof DeviceNotAvailableException) {
169             CLog.e("Skipping command teardown since exception was DeviceNotAvailable");
170             return;
171         }
172         for (String rawCmd : mTeardownCommands) {
173             String cmd = resolveCommand(rawCmd);
174             try {
175                 CommandResult result;
176                 if (mTeardownCmdTimeout > 0) {
177                     result =
178                             getDevice(testInfo)
179                                     .executeShellV2Command(
180                                             cmd, mTeardownCmdTimeout, TimeUnit.MILLISECONDS, 0);
181                 } else {
182                     result = getDevice(testInfo).executeShellV2Command(cmd);
183                 }
184                 if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
185                     CLog.d(
186                             "tearDown cmd: '%s' failed, returned:\nstdout:%s\nstderr:%s",
187                             cmd, result.getStdout(), result.getStderr());
188                 } else if (mLogOutput) {
189                     CLog.d(
190                             "tearDown cmd: '%s', returned:\nstdout:%s\nstderr:%s",
191                             cmd, result.getStdout(), result.getStderr());
192                 }
193             } catch (TargetSetupError tse) {
194                 CLog.e(tse);
195             }
196         }
197     }
198 
199     /** Add a command that will be run by the preparer. */
addRunCommand(String cmd)200     public final void addRunCommand(String cmd) {
201         mCommands.add(cmd);
202     }
203 
204     @VisibleForTesting
getCommands()205     public List<String> getCommands() {
206         return mCommands;
207     }
208 
209     @VisibleForTesting
getExecutedCommands()210     public List<String> getExecutedCommands() {
211         return mExecutedCommands;
212     }
213 
214     /**
215      * Returns the device to apply the preparer on.
216      *
217      * @param testInfo
218      * @return The device to apply the preparer on.
219      * @throws TargetSetupError
220      */
getDevice(TestInformation testInfo)221     protected ITestDevice getDevice(TestInformation testInfo) throws TargetSetupError {
222         return testInfo.getDevice();
223     }
224 
resolveCommand(String rawCmd)225     private String resolveCommand(String rawCmd) {
226         return mTestUser == null ? rawCmd : rawCmd.replace(mTestUserToken, mTestUser);
227     }
228 }
229 
230