• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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      */
ArchiveInstaller()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      */
ArchiveInstaller(IFileOp fileUtils)79     protected ArchiveInstaller(IFileOp fileUtils) {
80         mFileOp = fileUtils;
81     }
82 
83     /** Returns current {@link FileOp} to use. Never null. */
getFileOp()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      */
install(ArchiveReplacement archiveInfo, String osSdkRoot, boolean forceHttp, SdkManager sdkManager, DownloadCache cache, ITaskMonitor monitor)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)
downloadFile(Archive archive, String osSdkRoot, DownloadCache cache, ITaskMonitor monitor, boolean forceHttp)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      */
fileChecksum(MessageDigest digester, File tmpFile, ITaskMonitor monitor)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      */
getDigestChecksum(MessageDigest digester)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      */
fetchUrl(Archive archive, File tmpFile, String urlString, String pkgName, DownloadCache cache, ITaskMonitor monitor)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      */
unarchive(ArchiveReplacement archiveInfo, String osSdkRoot, File archiveFile, SdkManager sdkManager, ITaskMonitor monitor)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 
windowsDestDirLocked( String osSdkRoot, File destFolder, final ITaskMonitor monitor)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      */
moveFolder(File oldDir, File newDir)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)
unzipFolder( ArchiveReplacement archiveInfo, File archiveFile, File unzipDestFolder, ITaskMonitor monitor)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      */
getNewTempFolder(String osBasePath, String prefix, String suffix)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      */
getTempFolder(String osBasePath)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)
generateSourceProperties(Archive archive, File unzipDestFolder)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      */
restoreFolder(File srcFolder, File destFolder)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