Merge change Id49751eb into eclair-sdk

* changes:
  SDK Manager: reuse complete downloads, retry Windows locks.
This commit is contained in:
Android (Google) Code Review
2009-11-08 13:34:15 -08:00
3 changed files with 267 additions and 118 deletions

View File

@@ -23,6 +23,7 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
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 @@ public class Archive implements IDescription {
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 @@ public class Archive implements IDescription {
* 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 @@ public class Archive implements IDescription {
}
// 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 @@ public class Archive implements IDescription {
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 @@ public class Archive implements IDescription {
}
// 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 @@ public class Archive implements IDescription {
}
/**
* 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 @@ public class Archive implements IDescription {
* 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()) {
@@ -878,6 +975,15 @@ public class Archive implements IDescription {
return null;
}
/**
* 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.

View File

@@ -81,4 +81,17 @@ public interface ITaskMonitor {
* 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);
}

View File

@@ -19,6 +19,8 @@ package com.android.sdkuilib.internal.tasks;
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;
@@ -142,6 +144,30 @@ public final class ProgressTask implements ITaskMonitor {
return null;
}
/**
* 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 final class ProgressTask implements ITaskMonitor {
}
}
public boolean displayPrompt(String title, String message) {
return mRoot.displayPrompt(title, message);
}
public ITaskMonitor createSubMonitor(int tickCount) {
assert mSubCoef > 0;
assert tickCount > 0;