aboutsummaryrefslogtreecommitdiff
path: root/src/com/android/tradefed/targetprep/DefaultTestsZipInstaller.java
blob: 96b4f15163a002c55d0fe7f27032b6e10bbc1d1e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
/*
 * Copyright (C) 2011 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.tradefed.targetprep;

import com.android.ddmlib.FileListingService;
import com.android.tradefed.build.IDeviceBuildInfo;
import com.android.tradefed.device.DeviceNotAvailableException;
import com.android.tradefed.device.IFileEntry;
import com.android.tradefed.device.ITestDevice;
import com.android.tradefed.device.ITestDevice.RecoveryMode;
import com.android.tradefed.log.LogUtil.CLog;
import com.android.tradefed.util.ArrayUtil;
import com.android.tradefed.util.FileUtil;
import com.android.tradefed.util.IRunUtil;
import com.android.tradefed.util.RunUtil;

import java.io.File;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Set;


/**
 * A default implementation of tests zip installer.
 */
public class DefaultTestsZipInstaller implements ITestsZipInstaller {
    private static final int RM_ATTEMPTS = 3;
    private static final String DEVICE_DATA_PATH = buildAbsPath(FileListingService.DIRECTORY_DATA);
    private static final File DEVICE_DATA_FILE = new File(DEVICE_DATA_PATH);

    /**
     * A list of /data subdirectories to NOT wipe when doing UserDataFlashOption.TESTS_ZIP
     */
    private Set<String> mDataWipeSkipList;

    /**
     * Default constructor.
     */
    public DefaultTestsZipInstaller() {
    }

    /**
     * This convenience constructor allows the caller to set the skip list directly, rather than
     * needing to call {@link #setDataWipeSkipList} separately.
     *
     * @param skipList The collection of paths under {@code /data} to keep when clearing the
     * filesystem @see #setDataWipeSkipList
     */
    public DefaultTestsZipInstaller(Collection<String> skipList) {
        setDataWipeSkipList(skipList);
    }

    /**
     * This convenience constructor allows the caller to set the skip list directly, rather than
     * needing to call {@link #setDataWipeSkipList} separately.
     *
     * @param skipList The collection of paths under {@code /data} to keep when clearing the
     * filesystem @see #setDataWipeSkipList
     */
    public DefaultTestsZipInstaller(String... skipList) {
        setDataWipeSkipList(skipList);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setDataWipeSkipList(Collection<String> skipList) {
        mDataWipeSkipList = new HashSet<String>(skipList.size());
        mDataWipeSkipList.addAll(skipList);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void setDataWipeSkipList(String... skipList) {
        mDataWipeSkipList = new HashSet<String>(skipList.length);
        mDataWipeSkipList.addAll(Arrays.asList(skipList));
    }

    /**
     * Get the directory of directories to wipe, used for testing only.
     * @return the set of directories to skip when wiping a directory
     */
    public Set<String> getDataWipeSkipList() {
        return mDataWipeSkipList;
    }

    /**
     * {@inheritDoc}
     * <p>
     * This implementation will reboot the device into userland before
     * proceeding. It will also stop the Android runtime and leave it down upon return
     */
    @Override
    public void pushTestsZipOntoData(ITestDevice device, IDeviceBuildInfo deviceBuild)
            throws DeviceNotAvailableException, TargetSetupError {
        CLog.i(String.format("Pushing test zips content onto userdata on %s",
                device.getSerialNumber()));

        RecoveryMode cachedRecoveryMode = device.getRecoveryMode();
        device.setRecoveryMode(RecoveryMode.ONLINE);

        doDeleteData(device);

        CLog.d("Syncing test files/apks");
        File hostDir = new File(deviceBuild.getTestsDir(), "DATA");

        File[] hostDataFiles = getTestsZipDataFiles(hostDir);
        for (File hostSubDir : hostDataFiles) {
            device.syncFiles(hostSubDir, DEVICE_DATA_PATH);
        }

        // FIXME: this may end up mixing host slashes and device slashes
        for (File dir : findDirs(hostDir, DEVICE_DATA_FILE)) {
            device.executeShellCommand("chown system.system " + dir.getPath());
        }

        device.setRecoveryMode(cachedRecoveryMode);
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public void deleteData(ITestDevice device) throws DeviceNotAvailableException,
            TargetSetupError {
        RecoveryMode cachedRecoveryMode = device.getRecoveryMode();
        device.setRecoveryMode(RecoveryMode.ONLINE);

        doDeleteData(device);

        device.setRecoveryMode(cachedRecoveryMode);
    }

    /**
     * Deletes userdata from device without toggling {@link RecoveryMode}.
     * <p/>
     * Expects callers to have set device to {@link RecoveryMode#ONLINE}.
     */
    private void doDeleteData(ITestDevice device) throws DeviceNotAvailableException,
            TargetSetupError {
        // Stop the runtime, so it doesn't notice us mucking with the filesystem
        device.executeShellCommand("stop");
        // Stop installd to prevent it from writing to /data/data
        device.executeShellCommand("stop installd");

        CLog.d("clearing " + FileListingService.DIRECTORY_DATA + " directory on device "
                + device.getSerialNumber());

        // Touch a file so that we can make sure the filesystem is mounted and r/w and usable.  If
        // this method is a no-op, then the filesystem might be corrupt and mounted r/o, or might
        // not be mounted at all.
        String turtlePath = buildRelPath(DEVICE_DATA_PATH,
                String.format("turtles-%d.txt", System.currentTimeMillis()));
        boolean yayTurtle = device.pushString("I like turtles", turtlePath);
        if (!yayTurtle) {
            throw new TargetSetupError(String.format("Failed userdata write check on device %s",
                    device.getSerialNumber()));
        }

        IFileEntry dataEntry = device.getFileEntry(FileListingService.DIRECTORY_DATA);
        if (dataEntry == null) {
            throw new TargetSetupError(String.format("Could not find %s folder on %s",
                    FileListingService.DIRECTORY_DATA, device.getSerialNumber()));
        }
        for (IFileEntry dataSubDir : dataEntry.getChildren(false)) {
            if (!mDataWipeSkipList.contains(dataSubDir.getName())) {
                deleteDir(device, dataSubDir.getFullEscapedPath());
            }
        }
    }

    /**
     * @param fullEscapedPath
     * @throws DeviceNotAvailableException
     * @throws TargetSetupError
     */
    private void deleteDir(ITestDevice device, String fullEscapedPath)
            throws DeviceNotAvailableException, TargetSetupError {
        String result = "unknown";
        for (int i = 1; i <= RM_ATTEMPTS; i++) {
            result = device.executeShellCommand(String.format("rm -r %s", fullEscapedPath));
            if (!device.doesFileExist(fullEscapedPath)) {
                return;
            }
            CLog.d("Failed to delete dir %s on device %s on attempt %d of %d: stdout: %s",
                    fullEscapedPath, device.getSerialNumber(), i, RM_ATTEMPTS, result);
            // do exponential backoff
            getRunUtil().sleep(1000 * i * i);
        }
        throw new TargetSetupError(String.format("Failed to delete dir %s. rm output: %s",
                fullEscapedPath, result));
    }

    /**
     * Get the {@link IRunUtil} object to use.
     * <p/>
     * Exposed so unit tests can mock.
     */
    IRunUtil getRunUtil() {
        return RunUtil.getDefault();
    }

    private static String buildRelPath(String... parts) {
        return ArrayUtil.join(FileListingService.FILE_SEPARATOR, (Object[]) parts);
    }

    private static String buildAbsPath(String... parts) {
        return FileListingService.FILE_SEPARATOR + buildRelPath(parts);
    }

    /**
     * Retrieves the set of files contained in given tests.zip/DATA directory.
     * <p/>
     * Exposed so unit tests can mock.
     *
     * @param hostDir the {@link File} directory, representing the local path extracted tests.zip
     *            contents 'DATA' sub-folder
     * @return array of {@link File}
     */
    File[] getTestsZipDataFiles(File hostDir) throws TargetSetupError {
        if (!hostDir.isDirectory()) {
            throw new TargetSetupError("Unrecognized tests.zip content: missing DATA folder");
        }
        File[] childFiles = hostDir.listFiles();
        if (childFiles == null || childFiles.length <= 0) {
            throw new TargetSetupError(
                    "Unrecognized tests.zip content: DATA folder has no content");
        }
        return childFiles;
    }

    /**
     * Indirection to {@link FileUtil#findDirsUnder(File, File)} to allow for unit testing.
     */
    Set<File> findDirs(File hostDir, File deviceRootPath) {
        return FileUtil.findDirsUnder(hostDir, deviceRootPath);
    }
}