Home | History | Annotate | Download | only in archives
      1 /*
      2  * Copyright (C) 2010 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.sdklib.internal.repository.archives;
     18 
     19 import com.android.annotations.VisibleForTesting;
     20 import com.android.annotations.VisibleForTesting.Visibility;
     21 import com.android.sdklib.SdkConstants;
     22 import com.android.sdklib.SdkManager;
     23 import com.android.sdklib.internal.repository.DownloadCache;
     24 import com.android.sdklib.internal.repository.ITaskMonitor;
     25 import com.android.sdklib.internal.repository.packages.Package;
     26 import com.android.sdklib.internal.repository.sources.SdkSource;
     27 import com.android.sdklib.io.FileOp;
     28 import com.android.sdklib.io.IFileOp;
     29 import com.android.sdklib.repository.RepoConstants;
     30 import com.android.sdklib.util.GrabProcessOutput;
     31 import com.android.sdklib.util.GrabProcessOutput.IProcessOutput;
     32 import com.android.sdklib.util.GrabProcessOutput.Wait;
     33 
     34 import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
     35 import org.apache.commons.compress.archivers.zip.ZipFile;
     36 
     37 import java.io.EOFException;
     38 import java.io.File;
     39 import java.io.FileInputStream;
     40 import java.io.FileNotFoundException;
     41 import java.io.FileOutputStream;
     42 import java.io.IOException;
     43 import java.io.InputStream;
     44 import java.io.OutputStream;
     45 import java.security.MessageDigest;
     46 import java.security.NoSuchAlgorithmException;
     47 import java.util.Arrays;
     48 import java.util.Enumeration;
     49 import java.util.HashSet;
     50 import java.util.Properties;
     51 import java.util.Set;
     52 import java.util.TreeSet;
     53 import java.util.regex.Pattern;
     54 
     55 /**
     56  * Performs the work of installing a given {@link Archive}.
     57  */
     58 public class ArchiveInstaller {
     59 
     60     public static final String ENV_VAR_IGNORE_COMPAT = "ANDROID_SDK_IGNORE_COMPAT";
     61 
     62     public static final int NUM_MONITOR_INC = 100;
     63 
     64     /** The current {@link FileOp} to use. Never null. */
     65     private final IFileOp mFileOp;
     66 
     67     /**
     68      * Generates an {@link ArchiveInstaller} that relies on the default {@link FileOp}.
     69      */
     70     public ArchiveInstaller() {
     71         mFileOp = new FileOp();
     72     }
     73 
     74     /**
     75      * Generates an {@link ArchiveInstaller} that relies on the given {@link FileOp}.
     76      *
     77      * @param fileUtils An alternate version of {@link FileOp} to use for file operations.
     78      */
     79     protected ArchiveInstaller(IFileOp fileUtils) {
     80         mFileOp = fileUtils;
     81     }
     82 
     83     /** Returns current {@link FileOp} to use. Never null. */
     84     protected IFileOp getFileOp() {
     85         return mFileOp;
     86     }
     87 
     88     /**
     89      * Install this {@link ArchiveReplacement}s.
     90      * A "replacement" is composed of the actual new archive to install
     91      * (c.f. {@link ArchiveReplacement#getNewArchive()} and an <em>optional</em>
     92      * archive being replaced (c.f. {@link ArchiveReplacement#getReplaced()}.
     93      * In the case of a new install, the later should be null.
     94      * <p/>
     95      * The new archive to install will be skipped if it is incompatible.
     96      *
     97      * @return True if the archive was installed, false otherwise.
     98      */
     99     public boolean install(ArchiveReplacement archiveInfo,
    100             String osSdkRoot,
    101             boolean forceHttp,
    102             SdkManager sdkManager,
    103             DownloadCache cache,
    104             ITaskMonitor monitor) {
    105 
    106         Archive newArchive = archiveInfo.getNewArchive();
    107         Package pkg = newArchive.getParentPackage();
    108 
    109         File archiveFile = null;
    110         String name = pkg.getShortDescription();
    111 
    112         if (newArchive.isLocal()) {
    113             // This should never happen.
    114             monitor.log("Skipping already installed archive: %1$s for %2$s",
    115                     name,
    116                     newArchive.getOsDescription());
    117             return false;
    118         }
    119 
    120         // In detail mode, give us a way to force install of incompatible archives.
    121         boolean checkIsCompatible = System.getenv(ENV_VAR_IGNORE_COMPAT) == null;
    122 
    123         if (checkIsCompatible && !newArchive.isCompatible()) {
    124             monitor.log("Skipping incompatible archive: %1$s for %2$s",
    125                     name,
    126                     newArchive.getOsDescription());
    127             return false;
    128         }
    129 
    130         archiveFile = downloadFile(newArchive, osSdkRoot, cache, monitor, forceHttp);
    131         if (archiveFile != null) {
    132             // Unarchive calls the pre/postInstallHook methods.
    133             if (unarchive(archiveInfo, osSdkRoot, archiveFile, sdkManager, monitor)) {
    134                 monitor.log("Installed %1$s", name);
    135                 // Delete the temp archive if it exists, only on success
    136                 mFileOp.deleteFileOrFolder(archiveFile);
    137                 return true;
    138             }
    139         }
    140 
    141         return false;
    142     }
    143 
    144     /**
    145      * Downloads an archive and returns the temp file with it.
    146      * Caller is responsible with deleting the temp file when done.
    147      */
    148     @VisibleForTesting(visibility=Visibility.PRIVATE)
    149     protected File downloadFile(Archive archive,
    150             String osSdkRoot,
    151             DownloadCache cache,
    152             ITaskMonitor monitor,
    153             boolean forceHttp) {
    154 
    155         String pkgName = archive.getParentPackage().getShortDescription();
    156         monitor.setDescription("Downloading %1$s", pkgName);
    157         monitor.log("Downloading %1$s", pkgName);
    158 
    159         String link = archive.getUrl();
    160         if (!link.startsWith("http://")                          //$NON-NLS-1$
    161                 && !link.startsWith("https://")                  //$NON-NLS-1$
    162                 && !link.startsWith("ftp://")) {                 //$NON-NLS-1$
    163             // Make the URL absolute by prepending the source
    164             Package pkg = archive.getParentPackage();
    165             SdkSource src = pkg.getParentSource();
    166             if (src == null) {
    167                 monitor.logError("Internal error: no source for archive %1$s", pkgName);
    168                 return null;
    169             }
    170 
    171             // take the URL to the repository.xml and remove the last component
    172             // to get the base
    173             String repoXml = src.getUrl();
    174             int pos = repoXml.lastIndexOf('/');
    175             String base = repoXml.substring(0, pos + 1);
    176 
    177             link = base + link;
    178         }
    179 
    180         if (forceHttp) {
    181             link = link.replaceAll("https://", "http://");  //$NON-NLS-1$ //$NON-NLS-2$
    182         }
    183 
    184         // Get the basename of the file we're downloading, i.e. the last component
    185         // of the URL
    186         int pos = link.lastIndexOf('/');
    187         String base = link.substring(pos + 1);
    188 
    189         // Rather than create a real temp file in the system, we simply use our
    190         // temp folder (in the SDK base folder) and use the archive name for the
    191         // download. This allows us to reuse or continue downloads.
    192 
    193         File tmpFolder = getTempFolder(osSdkRoot);
    194         if (!mFileOp.isDirectory(tmpFolder)) {
    195             if (mFileOp.isFile(tmpFolder)) {
    196                 mFileOp.deleteFileOrFolder(tmpFolder);
    197             }
    198             if (!mFileOp.mkdirs(tmpFolder)) {
    199                 monitor.logError("Failed to create directory %1$s", tmpFolder.getPath());
    200                 return null;
    201             }
    202         }
    203         File tmpFile = new File(tmpFolder, base);
    204 
    205         // if the file exists, check its checksum & size. Use it if complete
    206         if (mFileOp.exists(tmpFile)) {
    207             if (mFileOp.length(tmpFile) == archive.getSize()) {
    208                 String chksum = "";                             //$NON-NLS-1$
    209                 try {
    210                     chksum = fileChecksum(archive.getChecksumType().getMessageDigest(),
    211                                           tmpFile,
    212                                           monitor);
    213                 } catch (NoSuchAlgorithmException e) {
    214                     // Ignore.
    215                 }
    216                 if (chksum.equalsIgnoreCase(archive.getChecksum())) {
    217                     // File is good, let's use it.
    218                     return tmpFile;
    219                 }
    220             }
    221 
    222             // Existing file is either of different size or content.
    223             // TODO: continue download when we support continue mode.
    224             // Right now, let's simply remove the file and start over.
    225             mFileOp.deleteFileOrFolder(tmpFile);
    226         }
    227 
    228         if (fetchUrl(archive, tmpFile, link, pkgName, cache, monitor)) {
    229             // Fetching was successful, let's use this file.
    230             return tmpFile;
    231         } else {
    232             // Delete the temp file if we aborted the download
    233             // TODO: disable this when we want to support partial downloads.
    234             mFileOp.deleteFileOrFolder(tmpFile);
    235             return null;
    236         }
    237     }
    238 
    239     /**
    240      * Computes the SHA-1 checksum of the content of the given file.
    241      * Returns an empty string on error (rather than null).
    242      */
    243     private String fileChecksum(MessageDigest digester, File tmpFile, ITaskMonitor monitor) {
    244         InputStream is = null;
    245         try {
    246             is = new FileInputStream(tmpFile);
    247 
    248             byte[] buf = new byte[65536];
    249             int n;
    250 
    251             while ((n = is.read(buf)) >= 0) {
    252                 if (n > 0) {
    253                     digester.update(buf, 0, n);
    254                 }
    255             }
    256 
    257             return getDigestChecksum(digester);
    258 
    259         } catch (FileNotFoundException e) {
    260             // The FNF message is just the URL. Make it a bit more useful.
    261             monitor.logError("File not found: %1$s", e.getMessage());
    262 
    263         } catch (Exception e) {
    264             monitor.logError("%1$s", e.getMessage());   //$NON-NLS-1$
    265 
    266         } finally {
    267             if (is != null) {
    268                 try {
    269                     is.close();
    270                 } catch (IOException e) {
    271                     // pass
    272                 }
    273             }
    274         }
    275 
    276         return "";  //$NON-NLS-1$
    277     }
    278 
    279     /**
    280      * Returns the SHA-1 from a {@link MessageDigest} as an hex string
    281      * that can be compared with {@link Archive#getChecksum()}.
    282      */
    283     private String getDigestChecksum(MessageDigest digester) {
    284         int n;
    285         // Create an hex string from the digest
    286         byte[] digest = digester.digest();
    287         n = digest.length;
    288         String hex = "0123456789abcdef";                     //$NON-NLS-1$
    289         char[] hexDigest = new char[n * 2];
    290         for (int i = 0; i < n; i++) {
    291             int b = digest[i] & 0x0FF;
    292             hexDigest[i*2 + 0] = hex.charAt(b >>> 4);
    293             hexDigest[i*2 + 1] = hex.charAt(b & 0x0f);
    294         }
    295 
    296         return new String(hexDigest);
    297     }
    298 
    299     /**
    300      * Actually performs the download.
    301      * Also computes the SHA1 of the file on the fly.
    302      * <p/>
    303      * Success is defined as downloading as many bytes as was expected and having the same
    304      * SHA1 as expected. Returns true on success or false if any of those checks fail.
    305      * <p/>
    306      * Increments the monitor by {@link #NUM_MONITOR_INC}.
    307      */
    308     private boolean fetchUrl(Archive archive,
    309             File tmpFile,
    310             String urlString,
    311             String pkgName,
    312             DownloadCache cache,
    313             ITaskMonitor monitor) {
    314 
    315         FileOutputStream os = null;
    316         InputStream is = null;
    317         try {
    318             is = cache.openDirectUrl(urlString, monitor);
    319             os = new FileOutputStream(tmpFile);
    320 
    321             MessageDigest digester = archive.getChecksumType().getMessageDigest();
    322 
    323             byte[] buf = new byte[65536];
    324             int n;
    325 
    326             long total = 0;
    327             long size = archive.getSize();
    328             long inc = size / NUM_MONITOR_INC;
    329             long next_inc = inc;
    330 
    331             long startMs = System.currentTimeMillis();
    332             long nextMs = startMs + 2000;  // start update after 2 seconds
    333 
    334             while ((n = is.read(buf)) >= 0) {
    335                 if (n > 0) {
    336                     os.write(buf, 0, n);
    337                     digester.update(buf, 0, n);
    338                 }
    339 
    340                 long timeMs = System.currentTimeMillis();
    341 
    342                 total += n;
    343                 if (total >= next_inc) {
    344                     monitor.incProgress(1);
    345                     next_inc += inc;
    346                 }
    347 
    348                 if (timeMs > nextMs) {
    349                     long delta = timeMs - startMs;
    350                     if (total > 0 && delta > 0) {
    351                         // percent left to download
    352                         int percent = (int) (100 * total / size);
    353                         // speed in KiB/s
    354                         float speed = (float)total / (float)delta * (1000.f / 1024.f);
    355                         // time left to download the rest at the current KiB/s rate
    356                         int timeLeft = (speed > 1e-3) ?
    357                                                (int)(((size - total) / 1024.0f) / speed) :
    358                                                0;
    359                         String timeUnit = "seconds";
    360                         if (timeLeft > 120) {
    361                             timeUnit = "minutes";
    362                             timeLeft /= 60;
    363                         }
    364 
    365                         monitor.setDescription(
    366                                 "Downloading %1$s (%2$d%%, %3$.0f KiB/s, %4$d %5$s left)",
    367                                 pkgName,
    368                                 percent,
    369                                 speed,
    370                                 timeLeft,
    371                                 timeUnit);
    372                     }
    373                     nextMs = timeMs + 1000;  // update every second
    374                 }
    375 
    376                 if (monitor.isCancelRequested()) {
    377                     monitor.log("Download aborted by user at %1$d bytes.", total);
    378                     return false;
    379                 }
    380 
    381             }
    382 
    383             if (total != size) {
    384                 monitor.logError(
    385                         "Download finished with wrong size. Expected %1$d bytes, got %2$d bytes.",
    386                         size, total);
    387                 return false;
    388             }
    389 
    390             // Create an hex string from the digest
    391             String actual   = getDigestChecksum(digester);
    392             String expected = archive.getChecksum();
    393             if (!actual.equalsIgnoreCase(expected)) {
    394                 monitor.logError("Download finished with wrong checksum. Expected %1$s, got %2$s.",
    395                         expected, actual);
    396                 return false;
    397             }
    398 
    399             return true;
    400 
    401         } catch (FileNotFoundException e) {
    402             // The FNF message is just the URL. Make it a bit more useful.
    403             monitor.logError("File not found: %1$s", e.getMessage());
    404 
    405         } catch (Exception e) {
    406             monitor.logError("%1$s", e.getMessage());   //$NON-NLS-1$
    407 
    408         } finally {
    409             if (os != null) {
    410                 try {
    411                     os.close();
    412                 } catch (IOException e) {
    413                     // pass
    414                 }
    415             }
    416 
    417             if (is != null) {
    418                 try {
    419                     is.close();
    420                 } catch (IOException e) {
    421                     // pass
    422                 }
    423             }
    424         }
    425 
    426         return false;
    427     }
    428 
    429     /**
    430      * Install the given archive in the given folder.
    431      */
    432     private boolean unarchive(ArchiveReplacement archiveInfo,
    433             String osSdkRoot,
    434             File archiveFile,
    435             SdkManager sdkManager,
    436             ITaskMonitor monitor) {
    437         boolean success = false;
    438         Archive newArchive = archiveInfo.getNewArchive();
    439         Package pkg = newArchive.getParentPackage();
    440         String pkgName = pkg.getShortDescription();
    441         monitor.setDescription("Installing %1$s", pkgName);
    442         monitor.log("Installing %1$s", pkgName);
    443 
    444         // Ideally we want to always unzip in a temp folder which name depends on the package
    445         // type (e.g. addon, tools, etc.) and then move the folder to the destination folder.
    446         // If the destination folder exists, it will be renamed and deleted at the very
    447         // end if everything succeeded. This provides a nice atomic swap and should leave the
    448         // original folder untouched in case something wrong (e.g. program crash) in the
    449         // middle of the unzip operation.
    450         //
    451         // However that doesn't work on Windows, we always end up not being able to move the
    452         // new folder. There are actually 2 cases:
    453         // A- A process such as a the explorer is locking the *old* folder or a file inside
    454         //    (e.g. adb.exe)
    455         //    In this case we really shouldn't be tried to work around it and we need to let
    456         //    the user know and let it close apps that access that folder.
    457         // B- A process is locking the *new* folder. Very often this turns to be a file indexer
    458         //    or an anti-virus that is busy scanning the new folder that we just unzipped.
    459         //
    460         // So we're going to change the strategy:
    461         // 1- Try to move the old folder to a temp/old folder. This might fail in case of issue A.
    462         //    Note: for platform-tools, we can try killing adb first.
    463         //    If it still fails, we do nothing and ask the user to terminate apps that can be
    464         //    locking that folder.
    465         // 2- Once the old folder is out of the way, we unzip the archive directly into the
    466         //    optimal new location. We no longer unzip it in a temp folder and move it since we
    467         //    know that's what fails in most of the cases.
    468         // 3- If the unzip fails, remove everything and try to restore the old folder by doing
    469         //    a *copy* in place and not a folder move (which will likely fail too).
    470 
    471         String pkgKind = pkg.getClass().getSimpleName();
    472 
    473         File destFolder = null;
    474         File oldDestFolder = null;
    475 
    476         try {
    477             // -0- Compute destination directory and check install pre-conditions
    478 
    479             destFolder = pkg.getInstallFolder(osSdkRoot, sdkManager);
    480 
    481             if (destFolder == null) {
    482                 // this should not seriously happen.
    483                 monitor.log("Failed to compute installation directory for %1$s.", pkgName);
    484                 return false;
    485             }
    486 
    487             if (!pkg.preInstallHook(newArchive, monitor, osSdkRoot, destFolder)) {
    488                 monitor.log("Skipping archive: %1$s", pkgName);
    489                 return false;
    490             }
    491 
    492             // -1- move old folder.
    493 
    494             if (mFileOp.exists(destFolder)) {
    495                 // Create a new temp/old dir
    496                 if (oldDestFolder == null) {
    497                     oldDestFolder = getNewTempFolder(osSdkRoot, pkgKind, "old");  //$NON-NLS-1$
    498                 }
    499                 if (oldDestFolder == null) {
    500                     // this should not seriously happen.
    501                     monitor.logError("Failed to find a temp directory in %1$s.", osSdkRoot);
    502                     return false;
    503                 }
    504 
    505                 // Try to move the current dest dir to the temp/old one. Tell the user if it failed.
    506                 while(true) {
    507                     if (!moveFolder(destFolder, oldDestFolder)) {
    508                         monitor.logError("Failed to rename directory %1$s to %2$s.",
    509                                 destFolder.getPath(), oldDestFolder.getPath());
    510 
    511                         if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) {
    512                             boolean tryAgain = true;
    513 
    514                             tryAgain = windowsDestDirLocked(osSdkRoot, destFolder, monitor);
    515 
    516                             if (tryAgain) {
    517                                 // loop, trying to rename the temp dir into the destination
    518                                 continue;
    519                             } else {
    520                                 return false;
    521                             }
    522                         }
    523                     }
    524                     break;
    525                 }
    526             }
    527 
    528             assert !mFileOp.exists(destFolder);
    529 
    530             // -2- Unzip new content directly in place.
    531 
    532             if (!mFileOp.mkdirs(destFolder)) {
    533                 monitor.logError("Failed to create directory %1$s", destFolder.getPath());
    534                 return false;
    535             }
    536 
    537             if (!unzipFolder(archiveInfo,
    538                              archiveFile,
    539                              destFolder,
    540                              monitor)) {
    541                 return false;
    542             }
    543 
    544             if (!generateSourceProperties(newArchive, destFolder)) {
    545                 monitor.logError("Failed to generate source.properties in directory %1$s",
    546                         destFolder.getPath());
    547                 return false;
    548             }
    549 
    550             // In case of success, if we were replacing an archive
    551             // and the older one had a different path, remove it now.
    552             Archive oldArchive = archiveInfo.getReplaced();
    553             if (oldArchive != null && oldArchive.isLocal()) {
    554                 String oldPath = oldArchive.getLocalOsPath();
    555                 File oldFolder = oldPath == null ? null : new File(oldPath);
    556                 if (oldFolder == null && oldArchive.getParentPackage() != null) {
    557                     oldFolder = oldArchive.getParentPackage().getInstallFolder(
    558                             osSdkRoot, sdkManager);
    559                 }
    560                 if (oldFolder != null && mFileOp.exists(oldFolder) &&
    561                         !oldFolder.equals(destFolder)) {
    562                     monitor.logVerbose("Removing old archive at %1$s", oldFolder.getAbsolutePath());
    563                     mFileOp.deleteFileOrFolder(oldFolder);
    564                 }
    565             }
    566 
    567             success = true;
    568             pkg.postInstallHook(newArchive, monitor, destFolder);
    569             return true;
    570 
    571         } finally {
    572             if (!success) {
    573                 // In case of failure, we try to restore the old folder content.
    574                 if (oldDestFolder != null) {
    575                     restoreFolder(oldDestFolder, destFolder);
    576                 }
    577 
    578                 // We also call the postInstallHool with a null directory to give a chance
    579                 // to the archive to cleanup after preInstallHook.
    580                 pkg.postInstallHook(newArchive, monitor, null /*installDir*/);
    581             }
    582 
    583             // Cleanup if the unzip folder is still set.
    584             mFileOp.deleteFileOrFolder(oldDestFolder);
    585         }
    586     }
    587 
    588     private boolean windowsDestDirLocked(
    589             String osSdkRoot,
    590             File destFolder,
    591             final ITaskMonitor monitor) {
    592         String msg = null;
    593 
    594         assert SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS;
    595 
    596         File findLockExe = FileOp.append(
    597                 osSdkRoot, SdkConstants.FD_TOOLS, SdkConstants.FD_LIB, SdkConstants.FN_FIND_LOCK);
    598 
    599         if (mFileOp.exists(findLockExe)) {
    600             try {
    601                 final StringBuilder result = new StringBuilder();
    602                 String command[] = new String[] {
    603                         findLockExe.getAbsolutePath(),
    604                         destFolder.getAbsolutePath()
    605                 };
    606                 Process process = Runtime.getRuntime().exec(command);
    607                 int retCode = GrabProcessOutput.grabProcessOutput(
    608                         process,
    609                         Wait.WAIT_FOR_READERS,
    610                         new IProcessOutput() {
    611                             @Override
    612                             public void out(String line) {
    613                                 if (line != null) {
    614                                     result.append(line).append("\n");
    615                                 }
    616                             }
    617 
    618                             @Override
    619                             public void err(String line) {
    620                                 if (line != null) {
    621                                     monitor.logError("[find_lock] Error: %1$s", line);
    622                                 }
    623                             }
    624                         });
    625 
    626                 if (retCode == 0 && result.length() > 0) {
    627                     // TODO create a better dialog
    628 
    629                     String found = result.toString().trim();
    630                     monitor.logError("[find_lock] Directory locked by %1$s", found);
    631 
    632                     TreeSet<String> apps = new TreeSet<String>(Arrays.asList(
    633                             found.split(Pattern.quote(";"))));  //$NON-NLS-1$
    634                     StringBuilder appStr = new StringBuilder();
    635                     for (String app : apps) {
    636                         appStr.append("\n  - ").append(app.trim());                //$NON-NLS-1$
    637                     }
    638 
    639                     msg = String.format(
    640                             "-= Warning ! =-\n" +
    641                             "The following processes: %1$s\n" +
    642                             "are locking the following directory: \n" +
    643                             "  %2$s\n" +
    644                             "Please close these applications so that the installation can continue.\n" +
    645                             "When ready, press YES to try again.",
    646                             appStr.toString(),
    647                             destFolder.getPath());
    648                 }
    649 
    650             } catch (Exception e) {
    651                 monitor.error(e, "[find_lock failed]");
    652             }
    653 
    654 
    655         }
    656 
    657         if (msg == null) {
    658             // Old way: simply display a generic text and let user figure it out.
    659             msg = String.format(
    660                 "-= Warning ! =-\n" +
    661                 "A folder failed to be moved. On Windows this " +
    662                 "typically means that a program is using that folder (for " +
    663                 "example Windows Explorer or your anti-virus software.)\n" +
    664                 "Please momentarily deactivate your anti-virus software or " +
    665                 "close any running programs that may be accessing the " +
    666                 "directory '%1$s'.\n" +
    667                 "When ready, press YES to try again.",
    668                 destFolder.getPath());
    669         }
    670 
    671         boolean tryAgain = monitor.displayPrompt("SDK Manager: failed to install", msg);
    672         return tryAgain;
    673     }
    674 
    675     /**
    676      * Tries to rename/move a folder.
    677      * <p/>
    678      * Contract:
    679      * <ul>
    680      * <li> When we start, oldDir must exist and be a directory. newDir must not exist. </li>
    681      * <li> On successful completion, oldDir must not exists.
    682      *      newDir must exist and have the same content. </li>
    683      * <li> On failure completion, oldDir must have the same content as before.
    684      *      newDir must not exist. </li>
    685      * </ul>
    686      * <p/>
    687      * The simple "rename" operation on a folder can typically fail on Windows for a variety
    688      * of reason, in fact as soon as a single process holds a reference on a directory. The
    689      * most common case are the Explorer, the system's file indexer, Tortoise SVN cache or
    690      * an anti-virus that are busy indexing a new directory having been created.
    691      *
    692      * @param oldDir The old location to move. It must exist and be a directory.
    693      * @param newDir The new location where to move. It must not exist.
    694      * @return True if the move succeeded. On failure, we try hard to not have touched the old
    695      *  directory in order not to loose its content.
    696      */
    697     private boolean moveFolder(File oldDir, File newDir) {
    698         // This is a simple folder rename that works on Linux/Mac all the time.
    699         //
    700         // On Windows this might fail if an indexer is busy looking at a new directory
    701         // (e.g. right after we unzip our archive), so it fails let's be nice and give
    702         // it a bit of time to succeed.
    703         for (int i = 0; i < 5; i++) {
    704             if (mFileOp.renameTo(oldDir, newDir)) {
    705                 return true;
    706             }
    707             try {
    708                 Thread.sleep(500 /*ms*/);
    709             } catch (InterruptedException e) {
    710                 // ignore
    711             }
    712         }
    713 
    714         return false;
    715     }
    716 
    717     /**
    718      * Unzips a zip file into the given destination directory.
    719      *
    720      * The archive file MUST have a unique "root" folder.
    721      * This root folder is skipped when unarchiving.
    722      */
    723     @SuppressWarnings("unchecked")
    724     @VisibleForTesting(visibility=Visibility.PRIVATE)
    725     protected boolean unzipFolder(
    726             ArchiveReplacement archiveInfo,
    727             File archiveFile,
    728             File unzipDestFolder,
    729             ITaskMonitor monitor) {
    730 
    731         Archive newArchive = archiveInfo.getNewArchive();
    732         Package pkg = newArchive.getParentPackage();
    733         String pkgName = pkg.getShortDescription();
    734         long compressedSize = newArchive.getSize();
    735 
    736         ZipFile zipFile = null;
    737         try {
    738             zipFile = new ZipFile(archiveFile);
    739 
    740             // To advance the percent and the progress bar, we don't know the number of
    741             // items left to unzip. However we know the size of the archive and the size of
    742             // each uncompressed item. The zip file format overhead is negligible so that's
    743             // a good approximation.
    744             long incStep = compressedSize / NUM_MONITOR_INC;
    745             long incTotal = 0;
    746             long incCurr = 0;
    747             int lastPercent = 0;
    748 
    749             byte[] buf = new byte[65536];
    750 
    751             Enumeration<ZipArchiveEntry> entries = zipFile.getEntries();
    752             while (entries.hasMoreElements()) {
    753                 ZipArchiveEntry entry = entries.nextElement();
    754 
    755                 String name = entry.getName();
    756 
    757                 // ZipFile entries should have forward slashes, but not all Zip
    758                 // implementations can be expected to do that.
    759                 name = name.replace('\\', '/');
    760 
    761                 // Zip entries are always packages in a top-level directory
    762                 // (e.g. docs/index.html). However we want to use our top-level
    763                 // directory so we drop the first segment of the path name.
    764                 int pos = name.indexOf('/');
    765                 if (pos < 0 || pos == name.length() - 1) {
    766                     continue;
    767                 } else {
    768                     name = name.substring(pos + 1);
    769                 }
    770 
    771                 File destFile = new File(unzipDestFolder, name);
    772 
    773                 if (name.endsWith("/")) {  //$NON-NLS-1$
    774                     // Create directory if it doesn't exist yet. This allows us to create
    775                     // empty directories.
    776                     if (!mFileOp.isDirectory(destFile) && !mFileOp.mkdirs(destFile)) {
    777                         monitor.logError("Failed to create directory %1$s",
    778                                 destFile.getPath());
    779                         return false;
    780                     }
    781                     continue;
    782                 } else if (name.indexOf('/') != -1) {
    783                     // Otherwise it's a file in a sub-directory.
    784                     // Make sure the parent directory has been created.
    785                     File parentDir = destFile.getParentFile();
    786                     if (!mFileOp.isDirectory(parentDir)) {
    787                         if (!mFileOp.mkdirs(parentDir)) {
    788                             monitor.logError("Failed to create directory %1$s",
    789                                     parentDir.getPath());
    790                             return false;
    791                         }
    792                     }
    793                 }
    794 
    795                 FileOutputStream fos = null;
    796                 long remains = entry.getSize();
    797                 try {
    798                     fos = new FileOutputStream(destFile);
    799 
    800                     // Java bug 4040920: do not rely on the input stream EOF and don't
    801                     // try to read more than the entry's size.
    802                     InputStream entryContent = zipFile.getInputStream(entry);
    803                     int n;
    804                     while (remains > 0 &&
    805                             (n = entryContent.read(
    806                                     buf, 0, (int) Math.min(remains, buf.length))) != -1) {
    807                         remains -= n;
    808                         if (n > 0) {
    809                             fos.write(buf, 0, n);
    810                         }
    811                     }
    812                 } catch (EOFException e) {
    813                     monitor.logError("Error uncompressing file %s. Size: %d bytes, Unwritten: %d bytes.",
    814                             entry.getName(), entry.getSize(), remains);
    815                     throw e;
    816                 } finally {
    817                     if (fos != null) {
    818                         fos.close();
    819                     }
    820                 }
    821 
    822                 pkg.postUnzipFileHook(newArchive, monitor, mFileOp, destFile, entry);
    823 
    824                 // Increment progress bar to match. We update only between files.
    825                 for(incTotal += entry.getCompressedSize(); incCurr < incTotal; incCurr += incStep) {
    826                     monitor.incProgress(1);
    827                 }
    828 
    829                 int percent = (int) (100 * incTotal / compressedSize);
    830                 if (percent != lastPercent) {
    831                     monitor.setDescription("Unzipping %1$s (%2$d%%)", pkgName, percent);
    832                     lastPercent = percent;
    833                 }
    834 
    835                 if (monitor.isCancelRequested()) {
    836                     return false;
    837                 }
    838             }
    839 
    840             return true;
    841 
    842         } catch (IOException e) {
    843             monitor.logError("Unzip failed: %1$s", e.getMessage());
    844 
    845         } finally {
    846             if (zipFile != null) {
    847                 try {
    848                     zipFile.close();
    849                 } catch (IOException e) {
    850                     // pass
    851                 }
    852             }
    853         }
    854 
    855         return false;
    856     }
    857 
    858     /**
    859      * Returns an unused temp folder path in the form of osBasePath/temp/prefix.suffixNNN.
    860      * <p/>
    861      * This does not actually <em>create</em> the folder. It just scan the base path for
    862      * a free folder name to use and returns the file to use to reference it.
    863      * <p/>
    864      * This operation is not atomic so there's no guarantee the folder can't get
    865      * created in between. This is however unlikely and the caller can assume the
    866      * returned folder does not exist yet.
    867      * <p/>
    868      * Returns null if no such folder can be found (e.g. if all candidates exist,
    869      * which is rather unlikely) or if the base temp folder cannot be created.
    870      */
    871     private File getNewTempFolder(String osBasePath, String prefix, String suffix) {
    872         File baseTempFolder = getTempFolder(osBasePath);
    873 
    874         if (!mFileOp.isDirectory(baseTempFolder)) {
    875             if (mFileOp.isFile(baseTempFolder)) {
    876                 mFileOp.deleteFileOrFolder(baseTempFolder);
    877             }
    878             if (!mFileOp.mkdirs(baseTempFolder)) {
    879                 return null;
    880             }
    881         }
    882 
    883         for (int i = 1; i < 100; i++) {
    884             File folder = new File(baseTempFolder,
    885                     String.format("%1$s.%2$s%3$02d", prefix, suffix, i));  //$NON-NLS-1$
    886             if (!mFileOp.exists(folder)) {
    887                 return folder;
    888             }
    889         }
    890         return null;
    891     }
    892 
    893     /**
    894      * Returns the single fixed "temp" folder used by the SDK Manager.
    895      * This folder is always at osBasePath/temp.
    896      * <p/>
    897      * This does not actually <em>create</em> the folder.
    898      */
    899     private File getTempFolder(String osBasePath) {
    900         File baseTempFolder = new File(osBasePath, RepoConstants.FD_TEMP);
    901         return baseTempFolder;
    902     }
    903 
    904     /**
    905      * Generates a source.properties in the destination folder that contains all the infos
    906      * relevant to this archive, this package and the source so that we can reload them
    907      * locally later.
    908      */
    909     @VisibleForTesting(visibility=Visibility.PRIVATE)
    910     protected boolean generateSourceProperties(Archive archive, File unzipDestFolder) {
    911         Properties props = new Properties();
    912 
    913         archive.saveProperties(props);
    914 
    915         Package pkg = archive.getParentPackage();
    916         if (pkg != null) {
    917             pkg.saveProperties(props);
    918         }
    919 
    920         OutputStream fos = null;
    921         try {
    922             File f = new File(unzipDestFolder, SdkConstants.FN_SOURCE_PROP);
    923 
    924             fos = mFileOp.newFileOutputStream(f);
    925 
    926             props.store(fos, "## Android Tool: Source of this archive.");  //$NON-NLS-1$
    927 
    928             return true;
    929         } catch (IOException e) {
    930             e.printStackTrace();
    931         } finally {
    932             if (fos != null) {
    933                 try {
    934                     fos.close();
    935                 } catch (IOException e) {
    936                 }
    937             }
    938         }
    939 
    940         return false;
    941     }
    942 
    943     /**
    944      * Recursively restore srcFolder into destFolder by performing a copy of the file
    945      * content rather than rename/moves.
    946      *
    947      * @param srcFolder The source folder to restore.
    948      * @param destFolder The destination folder where to restore.
    949      * @return True if the folder was successfully restored, false if it was not at all or
    950      *         only partially restored.
    951      */
    952     private boolean restoreFolder(File srcFolder, File destFolder) {
    953         boolean result = true;
    954 
    955         // Process sub-folders first
    956         File[] srcFiles = mFileOp.listFiles(srcFolder);
    957         if (srcFiles == null) {
    958             // Source does not exist. That is quite odd.
    959             return false;
    960         }
    961 
    962         if (mFileOp.isFile(destFolder)) {
    963             if (!mFileOp.delete(destFolder)) {
    964                 // There's already a file in there where we want a directory and
    965                 // we can't delete it. This is rather unexpected. Just give up on
    966                 // that folder.
    967                 return false;
    968             }
    969         } else if (!mFileOp.isDirectory(destFolder)) {
    970             mFileOp.mkdirs(destFolder);
    971         }
    972 
    973         // Get all the files and dirs of the current destination.
    974         // We are not going to clean up the destination first.
    975         // Instead we'll copy over and just remove any remaining files or directories.
    976         Set<File> destDirs = new HashSet<File>();
    977         Set<File> destFiles = new HashSet<File>();
    978         File[] files = mFileOp.listFiles(destFolder);
    979         if (files != null) {
    980             for (File f : files) {
    981                 if (mFileOp.isDirectory(f)) {
    982                     destDirs.add(f);
    983                 } else {
    984                     destFiles.add(f);
    985                 }
    986             }
    987         }
    988 
    989         // First restore all source directories.
    990         for (File dir : srcFiles) {
    991             if (mFileOp.isDirectory(dir)) {
    992                 File d = new File(destFolder, dir.getName());
    993                 destDirs.remove(d);
    994                 if (!restoreFolder(dir, d)) {
    995                     result = false;
    996                 }
    997             }
    998         }
    999 
   1000         // Remove any remaining directories not processed above.
   1001         for (File dir : destDirs) {
   1002             mFileOp.deleteFileOrFolder(dir);
   1003         }
   1004 
   1005         // Copy any source files over to the destination.
   1006         for (File file : srcFiles) {
   1007             if (mFileOp.isFile(file)) {
   1008                 File f = new File(destFolder, file.getName());
   1009                 destFiles.remove(f);
   1010                 try {
   1011                     mFileOp.copyFile(file, f);
   1012                 } catch (IOException e) {
   1013                     result = false;
   1014                 }
   1015             }
   1016         }
   1017 
   1018         // Remove any remaining files not processed above.
   1019         for (File file : destFiles) {
   1020             mFileOp.deleteFileOrFolder(file);
   1021         }
   1022 
   1023         return result;
   1024     }
   1025 }
   1026