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