SDK Manager: reuse complete downloads, retry Windows locks.
This resolves 2 main issues with the SDK updater:
- Completed downloads are not cleared till the install is successful
- They are also stored in SDK/temp rather than the real Windows TEMP
folder, making them more discoverable for savvy users.
- There's a retry loop on failed install when due to a directory
being locked.
- The retry loop comes with the a Big Fat Warning[tm] in a modal
dialog box. You can't miss it. And it explicitly mentions the
antivirus software can be the root cause.
SDK BUG 2235058
Change-Id: Id49751ebd67e7291a0e7005136b22576335729c1
diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Archive.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Archive.java
index e7239d3..e314bbe 100755
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Archive.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/Archive.java
@@ -23,6 +23,7 @@
import org.apache.commons.compress.archivers.zip.ZipFile;
import java.io.File;
+import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
@@ -369,42 +370,38 @@
Package pkg = getParentPackage();
File archiveFile = null;
- try {
- String name = pkg.getShortDescription();
+ String name = pkg.getShortDescription();
- if (pkg instanceof ExtraPackage && !((ExtraPackage) pkg).isPathValid()) {
- monitor.setResult("Skipping %1$s: %2$s is not a valid install path.",
- name,
- ((ExtraPackage) pkg).getPath());
- return false;
+ if (pkg instanceof ExtraPackage && !((ExtraPackage) pkg).isPathValid()) {
+ monitor.setResult("Skipping %1$s: %2$s is not a valid install path.",
+ name,
+ ((ExtraPackage) pkg).getPath());
+ return false;
+ }
+
+ if (isLocal()) {
+ // This should never happen.
+ monitor.setResult("Skipping already installed archive: %1$s for %2$s",
+ name,
+ getOsDescription());
+ return false;
+ }
+
+ if (!isCompatible()) {
+ monitor.setResult("Skipping incompatible archive: %1$s for %2$s",
+ name,
+ getOsDescription());
+ return false;
+ }
+
+ archiveFile = downloadFile(osSdkRoot, monitor, forceHttp);
+ if (archiveFile != null) {
+ if (unarchive(osSdkRoot, archiveFile, sdkManager, monitor)) {
+ monitor.setResult("Installed %1$s", name);
+ // Delete the temp archive if it exists, only on success
+ deleteFileOrFolder(archiveFile);
+ return true;
}
-
- if (isLocal()) {
- // This should never happen.
- monitor.setResult("Skipping already installed archive: %1$s for %2$s",
- name,
- getOsDescription());
- return false;
- }
-
- if (!isCompatible()) {
- monitor.setResult("Skipping incompatible archive: %1$s for %2$s",
- name,
- getOsDescription());
- return false;
- }
-
- archiveFile = downloadFile(monitor, forceHttp);
- if (archiveFile != null) {
- if (unarchive(osSdkRoot, archiveFile, sdkManager, monitor)) {
- monitor.setResult("Installed %1$s", name);
- return true;
- }
- }
-
- } finally {
- // Delete the temp archive if it exists
- deleteFileOrFolder(archiveFile);
}
return false;
@@ -414,57 +411,134 @@
* Downloads an archive and returns the temp file with it.
* Caller is responsible with deleting the temp file when done.
*/
- private File downloadFile(ITaskMonitor monitor, boolean forceHttp) {
+ private File downloadFile(String osSdkRoot, ITaskMonitor monitor, boolean forceHttp) {
- File tmpFileToDelete = null;
- try {
- File tmpFile = File.createTempFile("sdkupload", ".bin"); //$NON-NLS-1$ //$NON-NLS-2$
- tmpFileToDelete = tmpFile;
+ String name = getParentPackage().getShortDescription();
+ String desc = String.format("Downloading %1$s", name);
+ monitor.setDescription(desc);
+ monitor.setResult(desc);
- String name = getParentPackage().getShortDescription();
- String desc = String.format("Downloading %1$s", name);
- monitor.setDescription(desc);
- monitor.setResult(desc);
-
- String link = getUrl();
- if (!link.startsWith("http://") //$NON-NLS-1$
- && !link.startsWith("https://") //$NON-NLS-1$
- && !link.startsWith("ftp://")) { //$NON-NLS-1$
- // Make the URL absolute by prepending the source
- Package pkg = getParentPackage();
- RepoSource src = pkg.getParentSource();
- if (src == null) {
- monitor.setResult("Internal error: no source for archive %1$s", name);
- return null;
- }
-
- // take the URL to the repository.xml and remove the last component
- // to get the base
- String repoXml = src.getUrl();
- int pos = repoXml.lastIndexOf('/');
- String base = repoXml.substring(0, pos + 1);
-
- link = base + link;
+ String link = getUrl();
+ if (!link.startsWith("http://") //$NON-NLS-1$
+ && !link.startsWith("https://") //$NON-NLS-1$
+ && !link.startsWith("ftp://")) { //$NON-NLS-1$
+ // Make the URL absolute by prepending the source
+ Package pkg = getParentPackage();
+ RepoSource src = pkg.getParentSource();
+ if (src == null) {
+ monitor.setResult("Internal error: no source for archive %1$s", name);
+ return null;
}
- if (forceHttp) {
- link = link.replaceAll("https://", "http://"); //$NON-NLS-1$ //$NON-NLS-2$
- }
+ // take the URL to the repository.xml and remove the last component
+ // to get the base
+ String repoXml = src.getUrl();
+ int pos = repoXml.lastIndexOf('/');
+ String base = repoXml.substring(0, pos + 1);
- if (fetchUrl(tmpFile, link, desc, monitor)) {
- // Fetching was successful, don't delete the temp file here!
- tmpFileToDelete = null;
+ link = base + link;
+ }
+
+ if (forceHttp) {
+ link = link.replaceAll("https://", "http://"); //$NON-NLS-1$ //$NON-NLS-2$
+ }
+
+ // Get the basename of the file we're downloading, i.e. the last component
+ // of the URL
+ int pos = link.lastIndexOf('/');
+ String base = link.substring(pos + 1);
+
+ // Rather than create a real temp file in the system, we simply use our
+ // temp folder (in the SDK base folder) and use the archive name for the
+ // download. This allows us to reuse or continue downloads.
+
+ File tmpFile = new File(getTempFolder(osSdkRoot), base);
+
+ // if the file exists, check if its checksum & size. Use it if complete
+ if (tmpFile.exists()) {
+ if (tmpFile.length() == getSize() &&
+ fileChecksum(tmpFile, monitor).equalsIgnoreCase(getChecksum())) {
+ // File is good, let's use it.
return tmpFile;
}
- } catch (IOException e) {
+ // Existing file is either of different size or content.
+ // TODO: continue download when we support continue mode.
+ // Right now, let's simply remove the file and start over.
+ deleteFileOrFolder(tmpFile);
+ }
+
+ if (fetchUrl(tmpFile, link, desc, monitor)) {
+ // Fetching was successful, let's use this file.
+ return tmpFile;
+ } else {
+ // Delete the temp file if we aborted the download
+ // TODO: disable this when we want to support partial downloads!
+ deleteFileOrFolder(tmpFile);
+ return null;
+ }
+ }
+
+ /**
+ * Computes the SHA-1 checksum of the content of the given file.
+ * Returns an empty string on error (rather than null).
+ */
+ private String fileChecksum(File tmpFile, ITaskMonitor monitor) {
+ InputStream is = null;
+ try {
+ is = new FileInputStream(tmpFile);
+
+ MessageDigest digester = getChecksumType().getMessageDigest();
+
+ byte[] buf = new byte[65536];
+ int n;
+
+ while ((n = is.read(buf)) >= 0) {
+ if (n > 0) {
+ digester.update(buf, 0, n);
+ }
+ }
+
+ return getDigestChecksum(digester);
+
+ } catch (FileNotFoundException e) {
+ // The FNF message is just the URL. Make it a bit more useful.
+ monitor.setResult("File not found: %1$s", e.getMessage());
+
+ } catch (Exception e) {
monitor.setResult(e.getMessage());
} finally {
- deleteFileOrFolder(tmpFileToDelete);
+ if (is != null) {
+ try {
+ is.close();
+ } catch (IOException e) {
+ // pass
+ }
+ }
}
- return null;
+ return ""; //$NON-NLS-1$
+ }
+
+ /**
+ * Returns the SHA-1 from a {@link MessageDigest} as an hex string
+ * that can be compared with {@link #getChecksum()}.
+ */
+ private String getDigestChecksum(MessageDigest digester) {
+ int n;
+ // Create an hex string from the digest
+ byte[] digest = digester.digest();
+ n = digest.length;
+ String hex = "0123456789abcdef"; //$NON-NLS-1$
+ char[] hexDigest = new char[n * 2];
+ for (int i = 0; i < n; i++) {
+ int b = digest[i] & 0x0FF;
+ hexDigest[i*2 + 0] = hex.charAt(b >>> 4);
+ hexDigest[i*2 + 1] = hex.charAt(b & 0x0f);
+ }
+
+ return new String(hexDigest);
}
/**
@@ -554,18 +628,8 @@
}
// Create an hex string from the digest
- byte[] digest = digester.digest();
- n = digest.length;
- String hex = "0123456789abcdef"; //$NON-NLS-1$
- char[] hexDigest = new char[n * 2];
- for (int i = 0; i < n; i++) {
- int b = digest[i] & 0x0FF;
- hexDigest[i*2 + 0] = hex.charAt(b >>> 4);
- hexDigest[i*2 + 1] = hex.charAt(b & 0x0f);
- }
-
+ String actual = getDigestChecksum(digester);
String expected = getChecksum();
- String actual = new String(hexDigest);
if (!actual.equalsIgnoreCase(expected)) {
monitor.setResult("Download finished with wrong checksum. Expected %1$s, got %2$s.",
expected, actual);
@@ -627,7 +691,7 @@
try {
// Find a new temp folder that doesn't exist yet
- unzipDestFolder = findTempFolder(osSdkRoot, pkgKind, "new"); //$NON-NLS-1$
+ unzipDestFolder = createTempFolder(osSdkRoot, pkgKind, "new"); //$NON-NLS-1$
if (unzipDestFolder == null) {
// this should not seriously happen.
@@ -662,39 +726,72 @@
}
// Swap the old folder by the new one.
- File renameFailedForDir = null;
- if (destFolder.isDirectory()) {
- renamedDestFolder = findTempFolder(osSdkRoot, pkgKind, "old"); //$NON-NLS-1$
- if (renamedDestFolder == null) {
- // this should not seriously happen.
- monitor.setResult("Failed to find a temp directory in %1$s.", osSdkRoot);
+ // We have 2 "folder rename" (aka moves) to do.
+ // They must both succeed in the right order.
+ boolean move1done = false;
+ boolean move2done = false;
+ while (!move1done || !move2done) {
+ File renameFailedForDir = null;
+
+ // Case where the dest dir already exists
+ if (!move1done) {
+ if (destFolder.isDirectory()) {
+ // Create a new temp/old dir
+ if (renamedDestFolder == null) {
+ renamedDestFolder = createTempFolder(osSdkRoot, pkgKind, "old"); //$NON-NLS-1$
+ }
+ if (renamedDestFolder == null) {
+ // this should not seriously happen.
+ monitor.setResult("Failed to find a temp directory in %1$s.", osSdkRoot);
+ return false;
+ }
+
+ // try to move the current dest dir to the temp/old one
+ if (!destFolder.renameTo(renamedDestFolder)) {
+ monitor.setResult("Failed to rename directory %1$s to %2$s.",
+ destFolder.getPath(), renamedDestFolder.getPath());
+ renameFailedForDir = destFolder;
+ }
+ }
+
+ move1done = (renameFailedForDir == null);
+ }
+
+ // Case where there's no dest dir or we successfully moved it to temp/old
+ // We not try to move the temp/unzip to the dest dir
+ if (move1done && !move2done) {
+ if (renameFailedForDir == null && !unzipDestFolder.renameTo(destFolder)) {
+ monitor.setResult("Failed to rename directory %1$s to %2$s",
+ unzipDestFolder.getPath(), destFolder.getPath());
+ renameFailedForDir = unzipDestFolder;
+ }
+
+ move2done = (renameFailedForDir == null);
+ }
+
+ if (renameFailedForDir != null) {
+ if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) {
+
+ String msg = String.format(
+ "-= Warning ! =-\n" +
+ "A folder failed to be renamed or moved. On Windows this " +
+ "typically means that a program is using that folder (for example " +
+ "Windows Explorer or your anti-virus software.)\n" +
+ "Please momentarily deactivate your anti-virus software.\n" +
+ "Please also close any running programs that may be accessing " +
+ "the directory '%1$s'.\n" +
+ "When ready, press YES to try again.",
+ renameFailedForDir.getPath());
+
+ if (monitor.displayPrompt("SDK Manager: failed to install", msg)) {
+ // loop, trying to rename the temp dir into the destination
+ continue;
+ }
+
+ }
return false;
}
-
- if (!destFolder.renameTo(renamedDestFolder)) {
- monitor.setResult("Failed to rename directory %1$s to %2$s",
- destFolder.getPath(), renamedDestFolder.getPath());
- renameFailedForDir = destFolder;
- }
- }
-
- if (renameFailedForDir == null && !unzipDestFolder.renameTo(destFolder)) {
- monitor.setResult("Failed to rename directory %1$s to %2$s",
- unzipDestFolder.getPath(), destFolder.getPath());
- renameFailedForDir = unzipDestFolder;
- }
-
- if (renameFailedForDir != null) {
- if (SdkConstants.CURRENT_PLATFORM == SdkConstants.PLATFORM_WINDOWS) {
- monitor.setResult(
- "-= Warning ! =-\n" +
- "A folder failed to be renamed or moved. On Windows this " +
- "typically means that a program is using that folder (for example " +
- "Windows Explorer.) Please close all running programs that may be " +
- "locking the directory '%1$s' and try again.",
- renameFailedForDir.getPath());
- }
- return false;
+ break;
}
unzipDestFolder = null;
@@ -850,7 +947,7 @@
}
/**
- * Finds a temp folder in the form of osBasePath/temp/prefix.suffixNNN.
+ * Creates a temp folder in the form of osBasePath/temp/prefix.suffixNNN.
* <p/>
* This operation is not atomic so there's no guarantee the folder can't get
* created in between. This is however unlikely and the caller can assume the
@@ -859,8 +956,8 @@
* Returns null if no such folder can be found (e.g. if all candidates exist,
* which is rather unlikely) or if the base temp folder cannot be created.
*/
- private File findTempFolder(String osBasePath, String prefix, String suffix) {
- File baseTempFolder = new File(osBasePath, "temp");
+ private File createTempFolder(String osBasePath, String prefix, String suffix) {
+ File baseTempFolder = getTempFolder(osBasePath);
if (!baseTempFolder.isDirectory()) {
if (!baseTempFolder.mkdirs()) {
@@ -879,6 +976,15 @@
}
/**
+ * Returns the temp folder used by the SDK Manager.
+ * This folder is always at osBasePath/temp.
+ */
+ private File getTempFolder(String osBasePath) {
+ File baseTempFolder = new File(osBasePath, "temp"); //$NON-NLS-1$
+ return baseTempFolder;
+ }
+
+ /**
* Deletes a file or a directory.
* Directories are deleted recursively.
* The argument can be null.
diff --git a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ITaskMonitor.java b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ITaskMonitor.java
index 85596ba..e08e27c 100755
--- a/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ITaskMonitor.java
+++ b/tools/sdkmanager/libs/sdklib/src/com/android/sdklib/internal/repository/ITaskMonitor.java
@@ -81,4 +81,17 @@
* tickCount must be 1 or more.
*/
public ITaskMonitor createSubMonitor(int tickCount);
+
+ /**
+ * Display a yes/no question dialog box.
+ *
+ * Implementations MUST allow this to be called from any thread, e.g. by
+ * making sure the dialog is opened synchronously in the ui thread.
+ *
+ * @param title The title of the dialog box
+ * @param message The error message
+ * @return true if YES was clicked.
+ */
+ public boolean displayPrompt(final String title, final String message);
+
}
diff --git a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/tasks/ProgressTask.java b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/tasks/ProgressTask.java
index 9ed8a01..f958a40 100755
--- a/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/tasks/ProgressTask.java
+++ b/tools/sdkmanager/libs/sdkuilib/src/com/android/sdkuilib/internal/tasks/ProgressTask.java
@@ -19,6 +19,8 @@
import com.android.sdklib.internal.repository.ITask;
import com.android.sdklib.internal.repository.ITaskMonitor;
+import org.eclipse.jface.dialogs.MessageDialog;
+import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.ProgressBar;
import org.eclipse.swt.widgets.Shell;
@@ -143,6 +145,30 @@
}
/**
+ * Display a yes/no question dialog box.
+ *
+ * This implementation allow this to be called from any thread, it
+ * makes sure the dialog is opened synchronously in the ui thread.
+ *
+ * @param title The title of the dialog box
+ * @param message The error message
+ * @return true if YES was clicked.
+ */
+ public boolean displayPrompt(final String title, final String message) {
+ final Shell shell = mDialog.getParent();
+ Display display = shell.getDisplay();
+
+ // we need to ask the user what he wants to do.
+ final boolean[] result = new boolean[] { false };
+ display.syncExec(new Runnable() {
+ public void run() {
+ result[0] = MessageDialog.openQuestion(shell, title, message);
+ }
+ });
+ return result[0];
+ }
+
+ /**
* Creates a sub-monitor that will use up to tickCount on the progress bar.
* tickCount must be 1 or more.
*/
@@ -222,6 +248,10 @@
}
}
+ public boolean displayPrompt(String title, String message) {
+ return mRoot.displayPrompt(title, message);
+ }
+
public ITaskMonitor createSubMonitor(int tickCount) {
assert mSubCoef > 0;
assert tickCount > 0;