1 /*
2  * Copyright (C) 2023 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.util.image;
17 
18 import static org.junit.Assert.assertTrue;
19 
20 import com.android.tradefed.build.IBuildInfo;
21 import com.android.tradefed.build.IDeviceBuildInfo;
22 import com.android.tradefed.device.DeviceNotAvailableException;
23 import com.android.tradefed.device.ITestDevice;
24 import com.android.tradefed.device.ITestDevice.RecoveryMode;
25 import com.android.tradefed.device.SnapuserdWaitPhase;
26 import com.android.tradefed.invoker.TestInformation;
27 import com.android.tradefed.invoker.logger.CurrentInvocation;
28 import com.android.tradefed.invoker.logger.InvocationMetricLogger;
29 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationGroupMetricKey;
30 import com.android.tradefed.invoker.logger.InvocationMetricLogger.InvocationMetricKey;
31 import com.android.tradefed.invoker.tracing.CloseableTraceScope;
32 import com.android.tradefed.invoker.tracing.TracePropagatingExecutorService;
33 import com.android.tradefed.log.LogUtil.CLog;
34 import com.android.tradefed.result.error.InfraErrorIdentifier;
35 import com.android.tradefed.targetprep.TargetSetupError;
36 import com.android.tradefed.util.CommandResult;
37 import com.android.tradefed.util.CommandStatus;
38 import com.android.tradefed.util.FileUtil;
39 import com.android.tradefed.util.IRunUtil;
40 import com.android.tradefed.util.RunUtil;
41 import com.android.tradefed.util.ZipUtil;
42 import com.android.tradefed.util.ZipUtil2;
43 import com.android.tradefed.util.executor.ParallelDeviceExecutor;
44 import com.android.tradefed.util.image.DeviceImageTracker.FileCacheTracker;
45 
46 import com.google.common.collect.ImmutableSet;
47 
48 import java.io.File;
49 import java.io.IOException;
50 import java.util.ArrayList;
51 import java.util.Arrays;
52 import java.util.HashMap;
53 import java.util.List;
54 import java.util.Map;
55 import java.util.Set;
56 import java.util.concurrent.Callable;
57 import java.util.concurrent.CompletableFuture;
58 import java.util.concurrent.ExecutionException;
59 import java.util.concurrent.Executors;
60 import java.util.concurrent.Future;
61 import java.util.concurrent.ThreadFactory;
62 import java.util.concurrent.TimeUnit;
63 import java.util.concurrent.atomic.AtomicInteger;
64 
65 /** A utility to leverage the incremental image and device update. */
66 public class IncrementalImageUtil {
67 
68     private static final AtomicInteger poolNumber = new AtomicInteger(1);
69 
70     public static final Set<String> DYNAMIC_PARTITIONS_TO_DIFF =
71             ImmutableSet.of(
72                     "product.img",
73                     "system.img",
74                     "system_dlkm.img",
75                     "system_ext.img",
76                     "vendor.img",
77                     "vendor_dlkm.img");
78 
79     private final File mSrcImage;
80     private final File mSrcBootloader;
81     private final File mSrcBaseband;
82     private final File mTargetImage;
83     private final ITestDevice mDevice;
84     private final File mCreateSnapshotBinary;
85     private final boolean mApplySnapshot;
86     private final SnapuserdWaitPhase mWaitPhase;
87 
88     private boolean mAllowSameBuildFlashing = false;
89     private boolean mAllowUnzipBaseline = false;
90     private boolean mBootloaderNeedsFlashing = false;
91     private boolean mBasebandNeedsFlashing = false;
92     private boolean mUpdateWasCompleted = false;
93     private File mSourceDirectory;
94     private File mTargetDirectory;
95 
96     private ParallelPreparation mParallelSetup;
97     private final IRunUtil mRunUtil;
98 
initialize( ITestDevice device, IDeviceBuildInfo build, File createSnapshot, boolean isIsolatedSetup, boolean allowCrossRelease, boolean applySnapshot, SnapuserdWaitPhase waitPhase)99     public static IncrementalImageUtil initialize(
100             ITestDevice device,
101             IDeviceBuildInfo build,
102             File createSnapshot,
103             boolean isIsolatedSetup,
104             boolean allowCrossRelease,
105             boolean applySnapshot,
106             SnapuserdWaitPhase waitPhase)
107             throws DeviceNotAvailableException {
108         // With apply snapshot, device reset is supported
109         if (isIsolatedSetup && !applySnapshot) {
110             CLog.d("test is configured with isolation grade, doesn't support incremental yet.");
111             return null;
112         }
113         FileCacheTracker tracker =
114                 DeviceImageTracker.getDefaultCache()
115                         .getBaselineDeviceImage(device.getSerialNumber());
116         if (tracker == null) {
117             CLog.d("Not tracking current baseline image.");
118             return null;
119         }
120         String deviceBuildId = device.getBuildId();
121         if (!tracker.buildId.equals(deviceBuildId)) {
122             CLog.d(
123                     "On-device build (id = %s) isn't matching the cache (id = %s).",
124                     deviceBuildId, tracker.buildId);
125             InvocationMetricLogger.addInvocationMetrics(
126                     InvocationMetricKey.DEVICE_IMAGE_CACHE_MISMATCH, 1);
127             return null;
128         }
129         if (!tracker.branch.equals(build.getBuildBranch())) {
130             CLog.d("Newer build is not on the same branch.");
131             return null;
132         }
133         boolean crossRelease = false;
134         if (!tracker.flavor.equals(build.getBuildFlavor())) {
135             if (allowCrossRelease) {
136                 CLog.d(
137                         "Allowing cross-flavor update from '%s' to '%s'",
138                         tracker.flavor, build.getBuildFlavor());
139                 crossRelease = true;
140             } else {
141                 CLog.d("Newer build is not on the build flavor.");
142                 return null;
143             }
144         }
145 
146         if (!isSnapshotSupported(device, applySnapshot)) {
147             CLog.d("Incremental flashing not supported.");
148             return null;
149         }
150 
151         String splTarget = getSplVersion(build);
152         String splBaseline = device.getProperty("ro.build.version.security_patch");
153         if (splTarget != null && !splBaseline.equals(splTarget)) {
154             CLog.d("Target SPL is '%s', while baseline is '%s", splTarget, splBaseline);
155             return null;
156         }
157         if (crossRelease) {
158             InvocationMetricLogger.addInvocationMetrics(
159                     InvocationMetricKey.INCREMENTAL_ACROSS_RELEASE_COUNT, 1);
160         }
161 
162         File deviceImage = null;
163         File bootloader = null;
164         File baseband = null;
165         try {
166             deviceImage = copyImage(tracker.zippedDeviceImage);
167             bootloader = copyImage(tracker.zippedBootloaderImage);
168             if (tracker.zippedBasebandImage != null) {
169                 baseband = copyImage(tracker.zippedBasebandImage);
170             }
171         } catch (IOException e) {
172             InvocationMetricLogger.addInvocationMetrics(
173                     InvocationMetricKey.DEVICE_IMAGE_CACHE_MISMATCH, 1);
174             CLog.e(e);
175             FileUtil.recursiveDelete(deviceImage);
176             FileUtil.deleteFile(bootloader);
177             FileUtil.deleteFile(baseband);
178             return null;
179         }
180         InvocationMetricLogger.addInvocationMetrics(
181                 InvocationMetricKey.DEVICE_IMAGE_CACHE_ORIGIN,
182                 String.format("%s:%s:%s", tracker.branch, tracker.buildId, tracker.flavor));
183         return new IncrementalImageUtil(
184                 device,
185                 deviceImage,
186                 bootloader,
187                 baseband,
188                 build.getDeviceImageFile(),
189                 createSnapshot,
190                 applySnapshot,
191                 waitPhase);
192     }
193 
IncrementalImageUtil( ITestDevice device, File deviceImage, File bootloader, File baseband, File targetImage, File createSnapshot, boolean applySnapshot, SnapuserdWaitPhase waitPhase)194     public IncrementalImageUtil(
195             ITestDevice device,
196             File deviceImage,
197             File bootloader,
198             File baseband,
199             File targetImage,
200             File createSnapshot,
201             boolean applySnapshot,
202             SnapuserdWaitPhase waitPhase) {
203         mDevice = device;
204         mSrcImage = deviceImage;
205         mSrcBootloader = bootloader;
206         mSrcBaseband = baseband;
207         mApplySnapshot = applySnapshot;
208         mWaitPhase = waitPhase;
209 
210         mTargetImage = targetImage;
211         mRunUtil = new RunUtil();
212         // TODO: clean up when docker image is updated
213         mRunUtil.setEnvVariable("LD_LIBRARY_PATH", "/tradefed/lib64");
214         if (createSnapshot != null) {
215             File snapshot = createSnapshot;
216             try {
217                 if (createSnapshot.getName().endsWith(".zip")
218                         && ZipUtil.isZipFileValid(createSnapshot, false)) {
219                     File destDir = ZipUtil2.extractZipToTemp(createSnapshot, "create_snapshot");
220                     snapshot = FileUtil.findFile(destDir, "create_snapshot");
221                 }
222             } catch (IOException e) {
223                 CLog.e(e);
224             }
225             mCreateSnapshotBinary = snapshot;
226             FileUtil.chmodGroupRWX(snapshot);
227         } else {
228             mCreateSnapshotBinary = null;
229         }
230         mParallelSetup =
231                 new ParallelPreparation(
232                         Thread.currentThread().getThreadGroup(), mSrcImage, mTargetImage);
233         mParallelSetup.start();
234     }
235 
copyImage(File originalImage)236     private static File copyImage(File originalImage) throws IOException {
237         if (originalImage.isDirectory()) {
238             CLog.d("Baseline was already unzipped for %s", originalImage);
239             File copy =
240                     FileUtil.createTempDir(
241                             FileUtil.getBaseName(originalImage.getName()),
242                             CurrentInvocation.getWorkFolder());
243             FileUtil.recursiveHardlink(originalImage, copy);
244             return copy;
245         } else {
246             File copy =
247                     FileUtil.createTempFile(
248                             FileUtil.getBaseName(originalImage.getName()),
249                             ".img",
250                             CurrentInvocation.getWorkFolder());
251             copy.delete();
252             FileUtil.hardlinkFile(originalImage, copy);
253             return copy;
254         }
255     }
256 
257     /** Returns whether or not we can use the snapshot logic to update the device */
isSnapshotSupported(ITestDevice device, boolean applySnapshot)258     public static boolean isSnapshotSupported(ITestDevice device, boolean applySnapshot)
259             throws DeviceNotAvailableException {
260         // Ensure snapshotctl exists
261         CommandResult whichOutput = device.executeShellV2Command("which snapshotctl");
262         CLog.d("stdout: %s, stderr: %s", whichOutput.getStdout(), whichOutput.getStderr());
263         if (!whichOutput.getStdout().contains("/system/bin/snapshotctl")) {
264             return false;
265         }
266         CommandResult helpOutput = device.executeShellV2Command("snapshotctl");
267         CLog.d("stdout: %s, stderr: %s", helpOutput.getStdout(), helpOutput.getStderr());
268         if (applySnapshot) {
269             if (helpOutput.getStdout().contains("apply-update")
270                     || helpOutput.getStderr().contains("apply-update")) {
271                 return true;
272             }
273         } else {
274             if (helpOutput.getStdout().contains("map-snapshots")
275                     || helpOutput.getStderr().contains("map-snapshots")) {
276                 return true;
277             }
278         }
279         return false;
280     }
281 
notifyBootloaderNeedsRevert()282     public void notifyBootloaderNeedsRevert() {
283         mBootloaderNeedsFlashing = true;
284     }
285 
notifyBasebadNeedsRevert()286     public void notifyBasebadNeedsRevert() {
287         mBasebandNeedsFlashing = true;
288     }
289 
allowSameBuildFlashing()290     public void allowSameBuildFlashing() {
291         mAllowSameBuildFlashing = true;
292     }
293 
isSameBuildFlashingAllowed()294     public boolean isSameBuildFlashingAllowed() {
295         return mAllowSameBuildFlashing;
296     }
297 
allowUnzipBaseline()298     public void allowUnzipBaseline() {
299         mAllowUnzipBaseline = true;
300     }
301 
302     /** Returns whether device is currently using snapshots or not. */
isSnapshotInUse(ITestDevice device)303     public static boolean isSnapshotInUse(ITestDevice device) throws DeviceNotAvailableException {
304         CommandResult dumpOutput = device.executeShellV2Command("snapshotctl dump");
305         CLog.d("stdout: %s, stderr: %s", dumpOutput.getStdout(), dumpOutput.getStderr());
306         if (dumpOutput.getStdout().contains("Using snapuserd: 0")) {
307             return false;
308         }
309         return true;
310     }
311 
312     /** Updates the device using the snapshot logic. */
updateDevice(File currentBootloader, File currentRadio)313     public void updateDevice(File currentBootloader, File currentRadio)
314             throws DeviceNotAvailableException, TargetSetupError {
315         if (mDevice.isStateBootloaderOrFastbootd()) {
316             mDevice.rebootUntilOnline();
317         }
318         if (!mDevice.enableAdbRoot()) {
319             throw new TargetSetupError(
320                     "Failed to obtain root, this is required for incremental update.",
321                     InfraErrorIdentifier.INCREMENTAL_FLASHING_ERROR);
322         }
323         try {
324             internalUpdateDevice(currentBootloader, currentRadio);
325         } catch (DeviceNotAvailableException | TargetSetupError | RuntimeException e) {
326             InvocationMetricLogger.addInvocationMetrics(
327                     InvocationMetricKey.INCREMENTAL_FLASHING_UPDATE_FAILURE, 1);
328             throw e;
329         }
330     }
331 
internalUpdateDevice(File currentBootloader, File currentRadio)332     private void internalUpdateDevice(File currentBootloader, File currentRadio)
333             throws DeviceNotAvailableException, TargetSetupError {
334         InvocationMetricLogger.addInvocationMetrics(
335                 InvocationMetricKey.INCREMENTAL_FLASHING_ATTEMPT_COUNT, 1);
336         // Join the unzip thread
337         long startWait = System.currentTimeMillis();
338         try {
339             mParallelSetup.join();
340         } catch (InterruptedException e) {
341             mParallelSetup.cleanUpFiles();
342             throw new RuntimeException(e);
343         } finally {
344             InvocationMetricLogger.addInvocationMetrics(
345                     InvocationMetricKey.INCREMENTAL_FLASHING_WAIT_PARALLEL_SETUP,
346                     System.currentTimeMillis() - startWait);
347         }
348         if (mParallelSetup.getError() != null) {
349             mParallelSetup.cleanUpFiles();
350             InvocationMetricLogger.addInvocationMetrics(
351                     InvocationMetricKey.INCREMENTAL_FALLBACK_REASON,
352                     mParallelSetup.getError().getMessage());
353             throw mParallelSetup.getError();
354         }
355         boolean bootComplete =
356                 mDevice.waitForBootComplete(mDevice.getOptions().getAvailableTimeout());
357         if (!bootComplete) {
358             mParallelSetup.cleanUpFiles();
359             throw new TargetSetupError(
360                     "Failed to boot within timeout.",
361                     InfraErrorIdentifier.INCREMENTAL_FLASHING_ERROR);
362         }
363         // We need a few seconds after boot complete for update_engine to finish
364         // TODO: we could improve by listening to some update_engine messages.
365         RunUtil.getDefault().sleep(5000L);
366         File srcDirectory = mParallelSetup.getSrcDirectory();
367         File targetDirectory = mParallelSetup.getTargetDirectory();
368         File workDir = mParallelSetup.getWorkDir();
369         try (CloseableTraceScope ignored = new CloseableTraceScope("update_device")) {
370             // Once block comparison is successful, log the information
371             logTargetInformation(targetDirectory);
372             logPatchesInformation(workDir);
373 
374             mDevice.executeShellV2Command("mkdir -p /data/ndb");
375             mDevice.executeShellV2Command("rm -rf /data/ndb/*.patch");
376 
377             mDevice.executeShellV2Command("snapshotctl unmap-snapshots");
378             mDevice.executeShellV2Command("snapshotctl delete-snapshots");
379 
380             RecoveryMode mode = mDevice.getRecoveryMode();
381             mDevice.setRecoveryMode(RecoveryMode.NONE);
382             try {
383                 List<Callable<Boolean>> pushTasks = new ArrayList<>();
384                 for (File f : workDir.listFiles()) {
385                     try (CloseableTraceScope push =
386                             new CloseableTraceScope("push:" + f.getName())) {
387                         pushTasks.add(
388                                 () -> {
389                                     boolean success;
390                                     if (f.isDirectory()) {
391                                         success = mDevice.pushDir(f, "/data/ndb/");
392                                     } else {
393                                         success = mDevice.pushFile(f, "/data/ndb/" + f.getName());
394                                     }
395                                     CLog.d(
396                                             "Push status: %s. %s->%s",
397                                             success, f, "/data/ndb/" + f.getName());
398                                     assertTrue(success);
399                                     return true;
400                                 });
401                     }
402                 }
403                 ParallelDeviceExecutor<Boolean> pushExec =
404                         new ParallelDeviceExecutor<Boolean>(pushTasks.size());
405                 pushExec.invokeAll(pushTasks, 0, TimeUnit.MINUTES);
406                 if (pushExec.hasErrors()) {
407                     for (Throwable err : pushExec.getErrors()) {
408                         InvocationMetricLogger.addInvocationMetrics(
409                                 InvocationMetricKey.INCREMENTAL_FALLBACK_REASON, err.getMessage());
410                         if (err instanceof DeviceNotAvailableException) {
411                             throw (DeviceNotAvailableException) err;
412                         }
413                     }
414                     throw new TargetSetupError(
415                             String.format("Failed to push patches."),
416                             pushExec.getErrors().get(0),
417                             InfraErrorIdentifier.INCREMENTAL_FLASHING_ERROR);
418                 }
419             } finally {
420                 mDevice.setRecoveryMode(mode);
421             }
422 
423             CommandResult listSnapshots = mDevice.executeShellV2Command("ls -l /data/ndb/");
424             CLog.d("stdout: %s, stderr: %s", listSnapshots.getStdout(), listSnapshots.getStderr());
425 
426             if (mApplySnapshot) {
427                 CommandResult mapOutput =
428                         mDevice.executeShellV2Command("snapshotctl apply-update /data/ndb/");
429                 CLog.d("stdout: %s, stderr: %s", mapOutput.getStdout(), mapOutput.getStderr());
430                 if (!CommandStatus.SUCCESS.equals(mapOutput.getStatus())) {
431                     InvocationMetricLogger.addInvocationMetrics(
432                             InvocationMetricKey.INCREMENTAL_FALLBACK_REASON, "Failed apply-update");
433                     throw new TargetSetupError(
434                             String.format(
435                                     "Failed to apply-update.\nstdout:%s\nstderr:%s",
436                                     mapOutput.getStdout(), mapOutput.getStderr()),
437                             InfraErrorIdentifier.INCREMENTAL_FLASHING_ERROR);
438                 }
439             } else {
440                 CommandResult mapOutput =
441                         mDevice.executeShellV2Command("snapshotctl map-snapshots /data/ndb/");
442                 CLog.d("stdout: %s, stderr: %s", mapOutput.getStdout(), mapOutput.getStderr());
443                 if (!CommandStatus.SUCCESS.equals(mapOutput.getStatus())) {
444                     InvocationMetricLogger.addInvocationMetrics(
445                             InvocationMetricKey.INCREMENTAL_FALLBACK_REASON,
446                             "Failed map-snapshots");
447                     throw new TargetSetupError(
448                             String.format(
449                                     "Failed to map the snapshots.\nstdout:%s\nstderr:%s",
450                                     mapOutput.getStdout(), mapOutput.getStderr()),
451                             InfraErrorIdentifier.INCREMENTAL_FLASHING_ERROR);
452                 }
453             }
454             if (mApplySnapshot) {
455                 attemptBootloaderAndRadioFlashing(true, currentBootloader, currentRadio);
456             }
457             flashStaticPartition(targetDirectory);
458             mSourceDirectory = srcDirectory;
459 
460             mDevice.enableAdbRoot();
461 
462             if (mApplySnapshot) {
463                 mDevice.notifySnapuserd(mWaitPhase);
464                 mDevice.waitForSnapuserd(SnapuserdWaitPhase.BLOCK_AFTER_UPDATE);
465             } else {
466                 // If patches are mounted, just print snapuserd once
467                 CommandResult psOutput = mDevice.executeShellV2Command("ps -ef | grep snapuserd");
468                 CLog.d("stdout: %s, stderr: %s", psOutput.getStdout(), psOutput.getStderr());
469             }
470             mTargetDirectory = targetDirectory;
471             mUpdateWasCompleted = true;
472         } catch (DeviceNotAvailableException | RuntimeException e) {
473             if (mSourceDirectory == null) {
474                 FileUtil.recursiveDelete(srcDirectory);
475             }
476             throw e;
477         } finally {
478             FileUtil.recursiveDelete(workDir);
479         }
480     }
481 
482     /** Returns whether update was completed or not. */
updateCompleted()483     public boolean updateCompleted() {
484         return mUpdateWasCompleted;
485     }
486 
getExtractedTargetDirectory()487     public File getExtractedTargetDirectory() {
488         return mTargetDirectory;
489     }
490 
491     /** When doing some of the apply logic, we can clean up files right after setup. */
cleanAfterSetup()492     public void cleanAfterSetup() {
493         if (!mApplySnapshot) {
494             return;
495         }
496         // Delete the copy we made to use the incremental update
497         FileUtil.recursiveDelete(mSourceDirectory);
498         FileUtil.recursiveDelete(mTargetDirectory);
499         FileUtil.recursiveDelete(mSrcImage);
500         FileUtil.deleteFile(mSrcBootloader);
501         FileUtil.deleteFile(mSrcBaseband);
502         // In case of same build flashing, we should clean the setup operation
503         if (mParallelSetup != null) {
504             try {
505                 mParallelSetup.join();
506             } catch (InterruptedException e) {
507                 CLog.e(e);
508             }
509             mParallelSetup.cleanUpFiles();
510         }
511     }
512 
513     /*
514      * Returns the device to its original state.
515      */
teardownDevice(TestInformation testInfo)516     public void teardownDevice(TestInformation testInfo) throws DeviceNotAvailableException {
517         try {
518             if (mApplySnapshot) {
519                 return;
520             }
521             try (CloseableTraceScope ignored = new CloseableTraceScope("teardownDevice")) {
522                 attemptBootloaderAndRadioFlashing(false, mSrcBootloader, mSrcBaseband);
523                 if (mDevice.isStateBootloaderOrFastbootd()) {
524                     mDevice.reboot();
525                 }
526                 mDevice.enableAdbRoot();
527                 CommandResult revertOutput =
528                         mDevice.executeShellV2Command(
529                                 "snapshotctl revert-snapshots", 60L, TimeUnit.SECONDS, 0);
530                 if (!CommandStatus.SUCCESS.equals(revertOutput.getStatus())) {
531                     CLog.d(
532                             "Failed revert-snapshots. stdout: %s, stderr: %s",
533                             revertOutput.getStdout(), revertOutput.getStderr());
534                     InvocationMetricLogger.addInvocationMetrics(
535                             InvocationMetricKey.INCREMENTAL_FLASHING_TEARDOWN_FAILURE, 1);
536                 }
537                 if (mSourceDirectory != null) {
538                     flashStaticPartition(mSourceDirectory);
539                 }
540                 if (mSourceDirectory != null && mAllowUnzipBaseline) {
541                     DeviceImageTracker.getDefaultCache()
542                             .trackUpdatedDeviceImage(
543                                     mDevice.getSerialNumber(),
544                                     mSourceDirectory,
545                                     mSrcBootloader,
546                                     mSrcBaseband,
547                                     testInfo.getBuildInfo().getBuildId(),
548                                     testInfo.getBuildInfo().getBuildBranch(),
549                                     testInfo.getBuildInfo().getBuildFlavor());
550                 }
551             } catch (DeviceNotAvailableException e) {
552                 InvocationMetricLogger.addInvocationMetrics(
553                         InvocationMetricKey.INCREMENTAL_FLASHING_TEARDOWN_FAILURE, 1);
554                 throw e;
555             }
556         } finally {
557             // Delete the copy we made to use the incremental update
558             FileUtil.recursiveDelete(mSourceDirectory);
559             FileUtil.recursiveDelete(mTargetDirectory);
560             FileUtil.recursiveDelete(mSrcImage);
561             FileUtil.deleteFile(mSrcBootloader);
562             FileUtil.deleteFile(mSrcBaseband);
563             // In case of same build flashing, we should clean the setup operation
564             if (mParallelSetup != null) {
565                 try {
566                     mParallelSetup.join();
567                 } catch (InterruptedException e) {
568                     CLog.e(e);
569                 }
570                 mParallelSetup.cleanUpFiles();
571             }
572         }
573     }
574 
attemptBootloaderAndRadioFlashing( boolean forceFlashing, File bootloader, File baseband)575     private void attemptBootloaderAndRadioFlashing(
576             boolean forceFlashing, File bootloader, File baseband)
577             throws DeviceNotAvailableException {
578         if (mBootloaderNeedsFlashing || forceFlashing) {
579             if (bootloader == null) {
580                 CLog.w("No bootloader file to flash.");
581             } else {
582                 mDevice.rebootIntoBootloader();
583 
584                 CommandResult bootloaderFlashTarget =
585                         mDevice.executeFastbootCommand(
586                                 "flash", "bootloader", bootloader.getAbsolutePath());
587                 CLog.d("Status: %s", bootloaderFlashTarget.getStatus());
588                 CLog.d("stdout: %s", bootloaderFlashTarget.getStdout());
589                 CLog.d("stderr: %s", bootloaderFlashTarget.getStderr());
590             }
591         }
592         if (mBasebandNeedsFlashing || forceFlashing) {
593             if (baseband == null) {
594                 CLog.w("No baseband file to flash");
595             } else {
596                 mDevice.rebootIntoBootloader();
597 
598                 CommandResult radioFlashTarget =
599                         mDevice.executeFastbootCommand(
600                                 "flash", "radio", baseband.getAbsolutePath());
601                 CLog.d("Status: %s", radioFlashTarget.getStatus());
602                 CLog.d("stdout: %s", radioFlashTarget.getStdout());
603                 CLog.d("stderr: %s", radioFlashTarget.getStderr());
604             }
605         }
606     }
607 
blockCompare(File srcImage, File targetImage, File workDir)608     private void blockCompare(File srcImage, File targetImage, File workDir) {
609         try (CloseableTraceScope ignored =
610                 new CloseableTraceScope("block_compare:" + srcImage.getName())) {
611             mRunUtil.setWorkingDir(workDir);
612 
613             String createSnapshot = "create_snapshot"; // Expected to be on PATH
614             if (mCreateSnapshotBinary != null && mCreateSnapshotBinary.exists()) {
615                 createSnapshot = mCreateSnapshotBinary.getAbsolutePath();
616             }
617             CommandResult result =
618                     mRunUtil.runTimedCmd(
619                             0L,
620                             createSnapshot,
621                             "--source=" + srcImage.getAbsolutePath(),
622                             "--target=" + targetImage.getAbsolutePath());
623             if (!CommandStatus.SUCCESS.equals(result.getStatus())) {
624                 throw new RuntimeException(
625                         String.format("%s\n%s", result.getStdout(), result.getStderr()));
626             }
627             File[] listFiles = workDir.listFiles();
628             CLog.d("%s", Arrays.asList(listFiles));
629         }
630     }
631 
flashStaticPartition(File imageDirectory)632     private boolean flashStaticPartition(File imageDirectory) throws DeviceNotAvailableException {
633         // flash all static partition in bootloader
634         mDevice.rebootIntoBootloader();
635         Map<String, String> envMap = new HashMap<>();
636         envMap.put("ANDROID_PRODUCT_OUT", imageDirectory.getAbsolutePath());
637         CommandResult fastbootResult =
638                 mDevice.executeLongFastbootCommand(
639                         envMap,
640                         "flashall",
641                         "--exclude-dynamic-partitions",
642                         "--disable-super-optimization");
643         CLog.d("Status: %s", fastbootResult.getStatus());
644         CLog.d("stdout: %s", fastbootResult.getStdout());
645         CLog.d("stderr: %s", fastbootResult.getStderr());
646         if (!CommandStatus.SUCCESS.equals(fastbootResult.getStatus())) {
647             return false;
648         }
649         mDevice.waitForDeviceAvailable(5 * 60 * 1000L);
650         return true;
651     }
652 
logPatchesInformation(File patchesDirectory)653     private void logPatchesInformation(File patchesDirectory) {
654         for (File patch : patchesDirectory.listFiles()) {
655             InvocationMetricLogger.addInvocationMetrics(
656                     InvocationGroupMetricKey.INCREMENTAL_FLASHING_PATCHES_SIZE,
657                     patch.getName(),
658                     patch.length());
659         }
660     }
661 
logTargetInformation(File targetDirectory)662     private void logTargetInformation(File targetDirectory) {
663         for (File patch : targetDirectory.listFiles()) {
664             if (DYNAMIC_PARTITIONS_TO_DIFF.contains(patch.getName())) {
665                 InvocationMetricLogger.addInvocationMetrics(
666                         InvocationGroupMetricKey.INCREMENTAL_FLASHING_TARGET_SIZE,
667                         patch.getName(),
668                         patch.length());
669             }
670         }
671     }
672 
getSplVersion(IBuildInfo build)673     private static String getSplVersion(IBuildInfo build) {
674         File buildProp = build.getFile("build.prop");
675         if (buildProp == null) {
676             CLog.d("No target build.prop found for comparison.");
677             return null;
678         }
679         try {
680             String props = FileUtil.readStringFromFile(buildProp);
681             for (String line : props.split("\n")) {
682                 if (line.startsWith("ro.build.version.security_patch=")) {
683                     return line.split("=")[1];
684                 }
685             }
686         } catch (IOException e) {
687             CLog.e(e);
688         }
689         return null;
690     }
691 
692     private class ParallelPreparation extends Thread {
693 
694         private final File mSetupSrcImage;
695         private final File mSetupTargetImage;
696 
697         private File mSrcDirectory;
698         private File mTargetDirectory;
699         private File mWorkDir;
700         private TargetSetupError mError;
701 
ParallelPreparation(ThreadGroup currentGroup, File srcImage, File targetImage)702         public ParallelPreparation(ThreadGroup currentGroup, File srcImage, File targetImage) {
703             super(currentGroup, "incremental-flashing-preparation");
704             setDaemon(true);
705             this.mSetupSrcImage = srcImage;
706             this.mSetupTargetImage = targetImage;
707         }
708 
709         @Override
run()710         public void run() {
711             ThreadGroup currentGroup = Thread.currentThread().getThreadGroup();
712             ThreadFactory factory =
713                     new ThreadFactory() {
714                         @Override
715                         public Thread newThread(Runnable r) {
716                             Thread t =
717                                     new Thread(
718                                             currentGroup,
719                                             r,
720                                             "unzip-pool-task-" + poolNumber.getAndIncrement());
721                             t.setDaemon(true);
722                             return t;
723                         }
724                     };
725             try (CloseableTraceScope ignored = new CloseableTraceScope("unzip_device_images")) {
726                 mSrcDirectory = FileUtil.createTempDir("incremental_src");
727                 mTargetDirectory = FileUtil.createTempDir("incremental_target");
728                 Future<Boolean> futureSrcDir =
729                         CompletableFuture.supplyAsync(
730                                 () -> {
731                                     try (CloseableTraceScope unzipBaseline =
732                                             new CloseableTraceScope("unzip_baseline")) {
733                                         if (mSetupSrcImage.isDirectory()) {
734                                             FileUtil.recursiveHardlink(
735                                                     mSetupSrcImage, mSrcDirectory);
736                                             return true;
737                                         }
738 
739                                         ZipUtil2.extractZip(mSetupSrcImage, mSrcDirectory);
740                                         return true;
741                                     } catch (IOException ioe) {
742                                         throw new RuntimeException(ioe);
743                                     }
744                                 },
745                                 TracePropagatingExecutorService.create(
746                                         Executors.newFixedThreadPool(1, factory)));
747                 Future<Boolean> futureTargetDir =
748                         CompletableFuture.supplyAsync(
749                                 () -> {
750                                     try (CloseableTraceScope unzipTarget =
751                                             new CloseableTraceScope("unzip_target")) {
752                                         if (mSetupTargetImage.isDirectory()) {
753                                             FileUtil.recursiveHardlink(
754                                                     mSetupTargetImage, mTargetDirectory);
755                                             return true;
756                                         }
757                                         ZipUtil2.extractZip(mSetupTargetImage, mTargetDirectory);
758                                         return true;
759                                     } catch (IOException ioe) {
760                                         throw new RuntimeException(ioe);
761                                     }
762                                 },
763                                 TracePropagatingExecutorService.create(
764                                         Executors.newFixedThreadPool(1, factory)));
765                 // Join the unzipping
766                 futureSrcDir.get();
767                 futureTargetDir.get();
768             } catch (InterruptedException | IOException | ExecutionException e) {
769                 FileUtil.recursiveDelete(mSrcDirectory);
770                 FileUtil.recursiveDelete(mTargetDirectory);
771                 mSrcDirectory = null;
772                 mTargetDirectory = null;
773                 mError =
774                         new TargetSetupError(
775                                 e.getMessage(), e, InfraErrorIdentifier.FAIL_TO_CREATE_FILE);
776                 return;
777             }
778 
779             try {
780                 mWorkDir = FileUtil.createTempDir("block_compare_workdir");
781             } catch (IOException e) {
782                 FileUtil.recursiveDelete(mWorkDir);
783                 FileUtil.recursiveDelete(mSrcDirectory);
784                 FileUtil.recursiveDelete(mTargetDirectory);
785                 mSrcDirectory = null;
786                 mTargetDirectory = null;
787                 mError =
788                         new TargetSetupError(
789                                 e.getMessage(), e, InfraErrorIdentifier.FAIL_TO_CREATE_FILE);
790                 return;
791             }
792 
793             List<Callable<Boolean>> callableTasks = new ArrayList<>();
794             for (String partition : mSrcDirectory.list()) {
795                 File possibleSrc = new File(mSrcDirectory, partition);
796                 File possibleTarget = new File(mTargetDirectory, partition);
797                 File workDirectory = mWorkDir;
798                 if (possibleSrc.exists() && possibleTarget.exists()) {
799                     if (DYNAMIC_PARTITIONS_TO_DIFF.contains(partition)) {
800                         callableTasks.add(
801                                 () -> {
802                                     blockCompare(possibleSrc, possibleTarget, workDirectory);
803                                     return true;
804                                 });
805                     }
806                 } else {
807                     CLog.e("Skipping %s no src or target", partition);
808                 }
809             }
810             ParallelDeviceExecutor<Boolean> executor =
811                     new ParallelDeviceExecutor<Boolean>(callableTasks.size());
812             executor.invokeAll(callableTasks, 0, TimeUnit.MINUTES);
813             if (executor.hasErrors()) {
814                 mError =
815                         new TargetSetupError(
816                                 executor.getErrors().get(0).getMessage(),
817                                 executor.getErrors().get(0),
818                                 InfraErrorIdentifier.BLOCK_COMPARE_ERROR);
819             }
820         }
821 
getSrcDirectory()822         public File getSrcDirectory() {
823             return mSrcDirectory;
824         }
825 
getTargetDirectory()826         public File getTargetDirectory() {
827             return mTargetDirectory;
828         }
829 
getWorkDir()830         public File getWorkDir() {
831             return mWorkDir;
832         }
833 
getError()834         public TargetSetupError getError() {
835             return mError;
836         }
837 
cleanUpFiles()838         public void cleanUpFiles() {
839             FileUtil.recursiveDelete(mSrcDirectory);
840             FileUtil.recursiveDelete(mTargetDirectory);
841             FileUtil.recursiveDelete(mWorkDir);
842         }
843     }
844 }
845