1 /*
2  * Copyright (C) 2011 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 com.android.ddmlib.IDevice;
20 import com.android.ddmlib.Log;
21 import com.android.tradefed.build.BuildInfoKey.BuildInfoFileKey;
22 import com.android.tradefed.build.IBuildInfo;
23 import com.android.tradefed.build.IDeviceBuildInfo;
24 import com.android.tradefed.command.remote.DeviceDescriptor;
25 import com.android.tradefed.config.Option;
26 import com.android.tradefed.config.OptionClass;
27 import com.android.tradefed.device.DeviceNotAvailableException;
28 import com.android.tradefed.device.ITestDevice;
29 import com.android.tradefed.invoker.IInvocationContext;
30 import com.android.tradefed.invoker.TestInformation;
31 import com.android.tradefed.log.LogUtil.CLog;
32 import com.android.tradefed.testtype.IAbi;
33 import com.android.tradefed.testtype.IAbiReceiver;
34 import com.android.tradefed.testtype.IInvocationContextReceiver;
35 import com.android.tradefed.testtype.suite.ModuleDefinition;
36 import com.android.tradefed.util.AbiUtils;
37 import com.android.tradefed.util.FileUtil;
38 import com.android.tradefed.util.MultiMap;
39 
40 import java.io.File;
41 import java.io.IOException;
42 import java.util.ArrayList;
43 import java.util.Arrays;
44 import java.util.Collection;
45 import java.util.HashSet;
46 import java.util.LinkedHashMap;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.Set;
50 
51 /**
52  * A {@link ITargetPreparer} that attempts to push any number of files from any host path to any
53  * device path.
54  *
55  * <p>Should be performed *after* a new build is flashed, and *after* DeviceSetup is run (if
56  * enabled)
57  */
58 @OptionClass(alias = "push-file")
59 public class PushFilePreparer extends BaseTargetPreparer
60         implements IAbiReceiver, IInvocationContextReceiver {
61     private static final String LOG_TAG = "PushFilePreparer";
62     private static final String MEDIA_SCAN_INTENT =
63             "am broadcast -a android.intent.action.MEDIA_MOUNTED -d file://%s "
64                     + "--receiver-include-background";
65 
66     private IAbi mAbi;
67 
68     @Deprecated
69     @Option(
70         name = "push",
71         description =
72                 "Deprecated. Please use push-file instead. A push-spec, formatted as "
73                         + "'/localpath/to/srcfile.txt->/devicepath/to/destfile.txt' "
74                         + "or '/localpath/to/srcfile.txt->/devicepath/to/destdir/'. "
75                         + "May be repeated. The local path may be relative to the test cases "
76                         + "build out directories "
77                         + "($ANDROID_HOST_OUT_TESTCASES / $ANDROID_TARGET_OUT_TESTCASES)."
78     )
79     private Collection<String> mPushSpecs = new ArrayList<>();
80 
81     @Option(
82             name = "push-file",
83             description =
84                     "A push-spec, specifying the local file to the path where it should be pushed on "
85                             + "device. May be repeated. If multiple files are configured to be pushed "
86                             + "to the same remote path, the latest one will be pushed.")
87     private MultiMap<File, String> mPushFileSpecs = new MultiMap<>();
88 
89     @Option(
90             name = "backup-file",
91             description =
92                     "A key/value pair, the with key specifying a device file path to be backed up, "
93                             + "and the value a device file path indicating where to save the file. "
94                             + "During tear-down, the values will be executed in reverse, "
95                             + "restoring the backup file location to the initial location. "
96                             + "May be repeated.")
97     private Map<String, String> mBackupFileSpecs = new LinkedHashMap<>();
98 
99     @Option(name="post-push", description=
100             "A command to run on the device (with `adb shell (yourcommand)`) after all pushes " +
101             "have been attempted.  Will not be run if a push fails with abort-on-push-failure " +
102             "enabled.  May be repeated.")
103     private Collection<String> mPostPushCommands = new ArrayList<>();
104 
105     @Option(name="abort-on-push-failure", description=
106             "If false, continue if pushes fail.  If true, abort the Invocation on any failure.")
107     private boolean mAbortOnFailure = true;
108 
109     @Option(name="trigger-media-scan", description=
110             "After pushing files, trigger a media scan of external storage on device.")
111     private boolean mTriggerMediaScan = false;
112 
113     @Option(name="cleanup", description = "Whether files pushed onto device should be cleaned up "
114             + "after test. Note that the preparer does not verify that files/directories have "
115             + "been deleted.")
116     private boolean mCleanup = false;
117 
118     @Option(
119             name = "remount-system",
120             description =
121                     "Remounts system partition to be writable "
122                             + "so that files could be pushed there too")
123     private boolean mRemountSystem = false;
124 
125     @Option(
126             name = "remount-vendor",
127             description =
128                     "Remounts vendor partition to be writable "
129                             + "so that files could be pushed there too")
130     private boolean mRemountVendor = false;
131 
132     private Set<String> mFilesPushed = null;
133     /** If the preparer is part of a module, we can use the test module name as a search criteria */
134     private String mModuleName = null;
135 
136     /**
137      * Helper method to only throw if mAbortOnFailure is enabled. Callers should behave as if this
138      * method may return.
139      */
fail(String message, DeviceDescriptor descriptor)140     private void fail(String message, DeviceDescriptor descriptor) throws TargetSetupError {
141         if (shouldAbortOnFailure()) {
142             throw new TargetSetupError(message, descriptor);
143         } else {
144             // Log the error and return
145             Log.w(LOG_TAG, message);
146         }
147     }
148 
149     /** Create the list of files to be pushed. */
getPushSpecs(DeviceDescriptor descriptor)150     public final Map<String, File> getPushSpecs(DeviceDescriptor descriptor)
151             throws TargetSetupError {
152         Map<String, File> remoteToLocalMapping = new LinkedHashMap<>();
153         for (String pushspec : mPushSpecs) {
154             String[] pair = pushspec.split("->");
155             if (pair.length != 2) {
156                 fail(String.format("Invalid pushspec: '%s'", Arrays.asList(pair)), descriptor);
157                 continue;
158             }
159             remoteToLocalMapping.put(pair[1], new File(pair[0]));
160         }
161         // Push the file structure
162         for (File local : mPushFileSpecs.keySet()) {
163             for (String remoteLocation : mPushFileSpecs.get(local)) {
164                 remoteToLocalMapping.put(remoteLocation, local);
165             }
166         }
167         return remoteToLocalMapping;
168     }
169 
170     /** Whether or not to abort on push failure. */
shouldAbortOnFailure()171     public boolean shouldAbortOnFailure() {
172         return mAbortOnFailure;
173     }
174 
175     /** {@inheritDoc} */
176     @Override
setAbi(IAbi abi)177     public void setAbi(IAbi abi) {
178         mAbi = abi;
179     }
180 
181     /** {@inheritDoc} */
182     @Override
getAbi()183     public IAbi getAbi() {
184         return mAbi;
185     }
186 
187     /** {@inheritDoc} */
188     @Override
setInvocationContext(IInvocationContext invocationContext)189     public void setInvocationContext(IInvocationContext invocationContext) {
190         if (invocationContext.getAttributes().get(ModuleDefinition.MODULE_NAME) != null) {
191             // Only keep the module name
192             mModuleName =
193                     invocationContext.getAttributes().get(ModuleDefinition.MODULE_NAME).get(0);
194         }
195     }
196 
197     /**
198      * Resolve relative file path via {@link IBuildInfo} and test cases directories.
199      *
200      * @param buildInfo the build artifact information
201      * @param fileName relative file path to be resolved
202      * @return the file from the build info or test cases directories
203      */
resolveRelativeFilePath(IBuildInfo buildInfo, String fileName)204     public File resolveRelativeFilePath(IBuildInfo buildInfo, String fileName) {
205         File src = null;
206         if (buildInfo != null) {
207             src = buildInfo.getFile(fileName);
208             if (src != null && src.exists()) {
209                 return src;
210             }
211         }
212         if (buildInfo instanceof IDeviceBuildInfo) {
213             IDeviceBuildInfo deviceBuild = (IDeviceBuildInfo) buildInfo;
214             File testDir = deviceBuild.getTestsDir();
215             List<File> scanDirs = new ArrayList<>();
216             // If it exists, always look first in the ANDROID_TARGET_OUT_TESTCASES
217             File targetTestCases = deviceBuild.getFile(BuildInfoFileKey.TARGET_LINKED_DIR);
218             if (targetTestCases != null) {
219                 scanDirs.add(targetTestCases);
220             }
221             if (testDir != null) {
222                 scanDirs.add(testDir);
223             }
224 
225             if (mModuleName != null) {
226                 // Use module name as a discriminant to find some files
227                 if (testDir != null) {
228                     try {
229                         File moduleDir =
230                                 FileUtil.findDirectory(
231                                         mModuleName, scanDirs.toArray(new File[] {}));
232                         if (moduleDir != null) {
233                             // If the spec is pushing the module itself
234                             if (mModuleName.equals(fileName)) {
235                                 // If that's the main binary generated by the target, we push the
236                                 // full directory
237                                 return moduleDir;
238                             }
239                             // Search the module directory if it exists use it in priority
240                             src = FileUtil.findFile(fileName, null, moduleDir);
241                             if (src != null) {
242                                 // Search again with filtering on ABI
243                                 File srcWithAbi = FileUtil.findFile(fileName, mAbi, moduleDir);
244                                 if (srcWithAbi != null
245                                         && !srcWithAbi
246                                                 .getAbsolutePath()
247                                                 .startsWith(src.getAbsolutePath())) {
248                                     // When multiple matches are found, return the one with matching
249                                     // ABI unless src is its parent directory.
250                                     return srcWithAbi;
251                                 }
252                                 return src;
253                             }
254                         } else {
255                             CLog.d("Did not find any module directory for '%s'", mModuleName);
256                         }
257 
258                     } catch (IOException e) {
259                         CLog.w(
260                                 "Something went wrong while searching for the module '%s' "
261                                         + "directory.",
262                                 mModuleName);
263                     }
264                 }
265             }
266             // Search top-level matches
267             for (File searchDir : scanDirs) {
268                 try {
269                     Set<File> allMatch = FileUtil.findFilesObject(searchDir, fileName);
270                     if (allMatch.size() > 1) {
271                         CLog.d(
272                                 "Several match for filename '%s', searching for top-level match.",
273                                 fileName);
274                         for (File f : allMatch) {
275                             // Bias toward direct child / top level nodes
276                             if (f.getParent().equals(searchDir.getAbsolutePath())) {
277                                 return f;
278                             }
279                         }
280                     } else if (allMatch.size() == 1) {
281                         return allMatch.iterator().next();
282                     }
283                 } catch (IOException e) {
284                     CLog.w("Failed to find test files from directory.");
285                 }
286             }
287             // Fall-back to searching everything
288             try {
289                 // Search the full tests dir if no target dir is available.
290                 src = FileUtil.findFile(fileName, null, scanDirs.toArray(new File[] {}));
291                 if (src != null) {
292                     // Search again with filtering on ABI
293                     File srcWithAbi =
294                             FileUtil.findFile(fileName, mAbi, scanDirs.toArray(new File[] {}));
295                     if (srcWithAbi != null
296                             && !srcWithAbi.getAbsolutePath().startsWith(src.getAbsolutePath())) {
297                         // When multiple matches are found, return the one with matching
298                         // ABI unless src is its parent directory.
299                         return srcWithAbi;
300                     }
301                     return src;
302                 }
303             } catch (IOException e) {
304                 CLog.w("Failed to find test files from directory.");
305                 src = null;
306             }
307 
308             if (src == null && testDir != null) {
309                 // TODO(b/138416078): Once build dependency can be fixed and test required
310                 // APKs are all under the test module directory, we can remove this fallback
311                 // approach to do individual download from remote artifact.
312                 // Try to stage the files from remote zip files.
313                 src = buildInfo.stageRemoteFile(fileName, testDir);
314             }
315         }
316         return src;
317     }
318 
319     /** {@inheritDoc} */
320     @Override
setUp(TestInformation testInfo)321     public void setUp(TestInformation testInfo)
322             throws TargetSetupError, BuildError, DeviceNotAvailableException {
323         mFilesPushed = new HashSet<>();
324         ITestDevice device = testInfo.getDevice();
325         if (mRemountSystem) {
326             device.remountSystemWritable();
327         }
328         if (mRemountVendor) {
329             device.remountVendorWritable();
330         }
331 
332         // Backup files
333         for (Map.Entry<String, String> entry : mBackupFileSpecs.entrySet()) {
334             device.executeShellCommand(
335                     "mv \"" + entry.getKey() + "\" \"" + entry.getValue() + "\"");
336         }
337 
338         Map<String, File> remoteToLocalMapping = getPushSpecs(device.getDeviceDescriptor());
339         for (String remotePath : remoteToLocalMapping.keySet()) {
340             File local = remoteToLocalMapping.get(remotePath);
341             Log.d(
342                     LOG_TAG,
343                     String.format(
344                             "Trying to push local '%s' to remote '%s'",
345                             local.getPath(), remotePath));
346             evaluatePushingPair(device, testInfo.getBuildInfo(), local, remotePath);
347         }
348 
349         for (String command : mPostPushCommands) {
350             device.executeShellCommand(command);
351         }
352 
353         if (mTriggerMediaScan) {
354             String mountPoint = device.getMountPoint(IDevice.MNT_EXTERNAL_STORAGE);
355             device.executeShellCommand(String.format(MEDIA_SCAN_INTENT, mountPoint));
356         }
357     }
358 
359     /** {@inheritDoc} */
360     @Override
tearDown(TestInformation testInfo, Throwable e)361     public void tearDown(TestInformation testInfo, Throwable e) throws DeviceNotAvailableException {
362         ITestDevice device = testInfo.getDevice();
363         if (!(e instanceof DeviceNotAvailableException) && mCleanup && mFilesPushed != null) {
364             if (mRemountSystem) {
365                 device.remountSystemWritable();
366             }
367             if (mRemountVendor) {
368                 device.remountVendorWritable();
369             }
370             for (String devicePath : mFilesPushed) {
371                 device.deleteFile(devicePath);
372             }
373             // Restore files
374             for (Map.Entry<String, String> entry : mBackupFileSpecs.entrySet()) {
375                 device.executeShellCommand(
376                         "mv \"" + entry.getValue() + "\" \"" + entry.getKey() + "\"");
377             }
378         }
379     }
380 
evaluatePushingPair( ITestDevice device, IBuildInfo buildInfo, File src, String remotePath)381     private void evaluatePushingPair(
382             ITestDevice device, IBuildInfo buildInfo, File src, String remotePath)
383             throws TargetSetupError, DeviceNotAvailableException {
384         String localPath = src.getPath();
385         if (!src.isAbsolute()) {
386             src = resolveRelativeFilePath(buildInfo, localPath);
387         }
388         if (src == null || !src.exists()) {
389             fail(
390                     String.format("Local source file '%s' does not exist", localPath),
391                     device.getDeviceDescriptor());
392             return;
393         }
394         if (src.isDirectory()) {
395             boolean deleteContentOnly = true;
396             if (!device.doesFileExist(remotePath)) {
397                 device.executeShellCommand(String.format("mkdir -p \"%s\"", remotePath));
398                 deleteContentOnly = false;
399             } else if (!device.isDirectory(remotePath)) {
400                 // File exists and is not a directory
401                 throw new TargetSetupError(
402                         String.format(
403                                 "Attempting to push dir '%s' to an existing device file '%s'",
404                                 src.getAbsolutePath(), remotePath),
405                         device.getDeviceDescriptor());
406             }
407             Set<String> filter = new HashSet<>();
408             if (mAbi != null) {
409                 String currentArch = AbiUtils.getArchForAbi(mAbi.getName());
410                 filter.addAll(AbiUtils.getArchSupported());
411                 filter.remove(currentArch);
412             }
413             // TODO: Look into using syncFiles but that requires improving sync to work for unroot
414             if (!device.pushDir(src, remotePath, filter)) {
415                 fail(
416                         String.format(
417                                 "Failed to push local '%s' to remote '%s'", localPath, remotePath),
418                         device.getDeviceDescriptor());
419                 return;
420             } else {
421                 if (deleteContentOnly) {
422                     remotePath += "/*";
423                 }
424                 mFilesPushed.add(remotePath);
425             }
426         } else {
427             if (!device.pushFile(src, remotePath)) {
428                 fail(
429                         String.format(
430                                 "Failed to push local '%s' to remote '%s'", localPath, remotePath),
431                         device.getDeviceDescriptor());
432                 return;
433             } else {
434                 mFilesPushed.add(remotePath);
435             }
436         }
437     }
438 }
439