SDK Updater: Unzip archives.

This adds the following:
- unzip archives
- if dest dir already exists (typicaly update case),
  unzips in a temp dir then swap dirs then delete
  the old install. In case of error, the old archive
  has not been lost.
- computes stats for download: percentage, speed, time left.
- compute percent for install, on top of progress bar.

The install code will need to move somewhere
out of the window. I think I'll put in the Archive
itself with the window just looping on all archives
and doing some progress bar bookeeping.
This commit is contained in:
Raphael
2009-06-03 22:42:44 -07:00
parent ab16d9f7d5
commit f555bd3bff
5 changed files with 355 additions and 41 deletions

View File

@@ -54,10 +54,16 @@ public interface ITaskMonitor {
*/ */
public void incProgress(int delta); public void incProgress(int delta);
/**
* Returns the current value of the progress bar,
* between 0 and up to {@link #setProgressMax(int)} - 1.
*/
public int getProgress();
/** /**
* Returns true if the user requested to cancel the operation. * Returns true if the user requested to cancel the operation.
* It is up to the task thread to pool this and exit as soon * It is up to the task thread to pool this and exit as soon
* as possible. * as possible.
*/ */
public boolean cancelRequested(); public boolean isCancelRequested();
} }

View File

@@ -232,7 +232,7 @@ public class LocalPackagesPage extends Composite {
monitor.setProgressMax(100); monitor.setProgressMax(100);
int n = 0; int n = 0;
int d = 1; int d = 1;
while(!monitor.cancelRequested()) { while(!monitor.isCancelRequested()) {
monitor.incProgress(d); monitor.incProgress(d);
n += d; n += d;
if (n == 0 || n == 100) d = -d; if (n == 0 || n == 100) d = -d;

View File

@@ -148,7 +148,7 @@ final class ProgressDialog extends Dialog {
}); });
mResultText = new Text(mRootComposite, mResultText = new Text(mRootComposite,
SWT.BORDER | SWT.READ_ONLY | SWT.V_SCROLL | SWT.MULTI); SWT.BORDER | SWT.READ_ONLY | SWT.WRAP | SWT.H_SCROLL | SWT.V_SCROLL | SWT.CANCEL | SWT.MULTI);
mResultText.setEditable(true); mResultText.setEditable(true);
mResultText.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1)); mResultText.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 1));
} }
@@ -286,7 +286,15 @@ final class ProgressDialog extends Dialog {
public void run() { public void run() {
if (!mResultText.isDisposed()) { if (!mResultText.isDisposed()) {
mResultText.setVisible(true); mResultText.setVisible(true);
mResultText.setText(String.format(resultFormat, args)); String newText = String.format(resultFormat, args);
String lastText = mResultText.getText();
if (lastText != null &&
lastText.length() > 0 &&
!lastText.endsWith("\n") &&
!newText.startsWith("\n")) {
mResultText.append("\n");
}
mResultText.append(newText);
} }
} }
}); });
@@ -327,6 +335,27 @@ final class ProgressDialog extends Dialog {
} }
} }
/**
* Returns the current value of the progress bar,
* between 0 and up to {@link #setProgressMax(int)} - 1.
* This method can be invoked from a non-UI thread.
*/
public int getProgress() {
final int[] result = new int[] { 0 };
if (!mDialogShell.isDisposed()) {
mDialogShell.getDisplay().syncExec(new Runnable() {
public void run() {
if (!mProgressBar.isDisposed()) {
result[0] = mProgressBar.getSelection();
}
}
});
}
return result[0];
}
/** /**
* Starts the thread that runs the task. * Starts the thread that runs the task.
* This is deferred till the UI is created. * This is deferred till the UI is created.

View File

@@ -80,11 +80,21 @@ class ProgressTask implements ITaskMonitor {
mDialog.incProgress(delta); mDialog.incProgress(delta);
} }
/**
* Returns the current value of the progress bar,
* between 0 and up to {@link #setProgressMax(int)} - 1.
*
* This method can be invoked from a non-UI thread.
*/
public int getProgress() {
return mDialog.getProgress();
}
/** /**
* Returns true if the "Cancel" button was selected. * Returns true if the "Cancel" button was selected.
* It is up to the task thread to pool this and exit. * It is up to the task thread to pool this and exit.
*/ */
public boolean cancelRequested() { public boolean isCancelRequested() {
return mDialog.isCancelRequested(); return mDialog.isCancelRequested();
} }

View File

@@ -43,6 +43,7 @@ import org.eclipse.swt.widgets.List;
import org.eclipse.swt.widgets.Shell; import org.eclipse.swt.widgets.Shell;
import java.io.File; import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@@ -51,13 +52,15 @@ import java.net.URL;
import java.security.MessageDigest; import java.security.MessageDigest;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/** /**
* This is the private implementation of the UpdateWindow. * This is the private implementation of the UpdateWindow.
*/ */
public class UpdaterWindowImpl { public class UpdaterWindowImpl {
private static final int NUM_FETCH_URL_MONITOR_INC = 100; private static final int NUM_MONITOR_INC = 100;
/** Internal data shared between the window and its pages. */ /** Internal data shared between the window and its pages. */
private final UpdaterData mUpdaterData = new UpdaterData(); private final UpdaterData mUpdaterData = new UpdaterData();
@@ -338,49 +341,66 @@ public class UpdaterWindowImpl {
* @param archives The archives to install. Incompatible ones will be skipped. * @param archives The archives to install. Incompatible ones will be skipped.
*/ */
public void installArchives(final Collection<Archive> archives) { public void installArchives(final Collection<Archive> archives) {
// TODO filter the archive list to: a/ display a list of what is going to be installed,
// b/ display licenses and c/ check that the selected packages are actually upgrades
// or ask user to confirm downgrades. All this should be done in a separate class+window
// which will then call this method with the final list.
// TODO move most parts to SdkLib, maybe as part of Archive, making archives self-installing. // TODO move most parts to SdkLib, maybe as part of Archive, making archives self-installing.
mTaskFactory.start("Installing Archives", new ITask() { mTaskFactory.start("Installing Archives", new ITask() {
public void run(ITaskMonitor monitor) { public void run(ITaskMonitor monitor) {
monitor.setProgressMax(archives.size() * (NUM_FETCH_URL_MONITOR_INC + 10)); final int progressPerArchive = 2 * NUM_MONITOR_INC + 10;
monitor.setProgressMax(archives.size() * progressPerArchive);
monitor.setDescription("Preparing to install archives"); monitor.setDescription("Preparing to install archives");
int num_installed = 0; int numInstalled = 0;
for (Archive archive : archives) { for (Archive archive : archives) {
if (!archive.isCompatible()) { int nextProgress = monitor.getProgress() + progressPerArchive;
monitor.setResult("Skipping incompatible archive: %1$s",
archive.getParentPackage().getShortDescription());
monitor.incProgress(NUM_FETCH_URL_MONITOR_INC + 10);
continue;
}
File archiveFile = null; File archiveFile = null;
try { try {
if (monitor.isCancelRequested()) {
break;
}
String name = archive.getParentPackage().getShortDescription();
// TODO: we should not see this test fail if we had the filter UI above.
if (!archive.isCompatible()) {
monitor.setResult("Skipping incompatible archive: %1$s", name);
continue;
}
archiveFile = downloadArchive(archive, monitor); archiveFile = downloadArchive(archive, monitor);
if (archiveFile != null) { if (archiveFile != null) {
if (installArchive(archive, archiveFile, monitor)) { if (installArchive(archive, archiveFile, monitor)) {
monitor.setResult("Installed: %1$s", monitor.setResult("Installed: %1$s", name);
archive.getParentPackage().getShortDescription()); numInstalled++;
num_installed++;
} }
} }
monitor.incProgress(10);
} catch (Throwable t) { } catch (Throwable t) {
// Display anything unexpected in the monitor. // Display anything unexpected in the monitor.
monitor.setResult("Unexpected Error: %1$s", t.getMessage()); monitor.setResult("Unexpected Error: %1$s", t.getMessage());
} finally { } finally {
if (archiveFile != null) { // Delete the temp archive if it exists
if (!archiveFile.delete()) { deleteFileOrFolder(archiveFile);
archiveFile.deleteOnExit();
} // Always move the progress bar to the desired position.
} // This allows internal methods to not have to care in case
// they abort early
monitor.incProgress(nextProgress - monitor.getProgress());
} }
} }
if (num_installed == 0) { if (numInstalled == 0) {
monitor.setResult("Nothing was installed."); monitor.setDescription("Done. Nothing was installed.");
} else {
monitor.setDescription("Done. %1$d %2$s installed.",
numInstalled,
numInstalled == 1 ? "package" : "packages");
} }
} }
}); });
@@ -397,8 +417,9 @@ public class UpdaterWindowImpl {
File tmpFile = File.createTempFile("sdkupload", ".bin"); //$NON-NLS-1$ //$NON-NLS-2$ File tmpFile = File.createTempFile("sdkupload", ".bin"); //$NON-NLS-1$ //$NON-NLS-2$
tmpFileToDelete = tmpFile; tmpFileToDelete = tmpFile;
monitor.setDescription("Downloading %1$s", String name = archive.getParentPackage().getShortDescription();
archive.getParentPackage().getShortDescription()); String desc = String.format("Downloading %1$s", name);
monitor.setDescription(desc);
String link = archive.getUrl(); String link = archive.getUrl();
if (!link.startsWith("http://") //$NON-NLS-1$ if (!link.startsWith("http://") //$NON-NLS-1$
@@ -408,8 +429,7 @@ public class UpdaterWindowImpl {
Package pkg = archive.getParentPackage(); Package pkg = archive.getParentPackage();
RepoSource src = pkg.getParentSource(); RepoSource src = pkg.getParentSource();
if (src == null) { if (src == null) {
monitor.setResult("Internal error: no source for archive %1$s", monitor.setResult("Internal error: no source for archive %1$s", name);
archive.getShortDescription());
return null; return null;
} }
@@ -422,7 +442,7 @@ public class UpdaterWindowImpl {
link = base + link; link = base + link;
} }
if (fetchUrl(tmpFile, archive, link, monitor)) { if (fetchUrl(tmpFile, archive, link, desc, monitor)) {
// Fetching was successful, don't delete the temp file here! // Fetching was successful, don't delete the temp file here!
tmpFileToDelete = null; tmpFileToDelete = null;
return tmpFile; return tmpFile;
@@ -432,11 +452,7 @@ public class UpdaterWindowImpl {
monitor.setResult(e.getMessage()); monitor.setResult(e.getMessage());
} finally { } finally {
if (tmpFileToDelete != null) { deleteFileOrFolder(tmpFileToDelete);
if (!tmpFileToDelete.delete()) {
tmpFileToDelete.deleteOnExit();
}
}
} }
return null; return null;
} }
@@ -448,11 +464,17 @@ public class UpdaterWindowImpl {
* Success is defined as downloading as many bytes as was expected and having the same * Success is defined as downloading as many bytes as was expected and having the same
* SHA1 as expected. Returns true on success or false if any of those checks fail. * SHA1 as expected. Returns true on success or false if any of those checks fail.
* <p/> * <p/>
* Increments the monitor by {@link #NUM_FETCH_URL_MONITOR_INC} (which is 10). * Increments the monitor by {@link #NUM_MONITOR_INC}.
*/ */
private boolean fetchUrl(File tmpFile, Archive archive, String urlString, ITaskMonitor monitor) { private boolean fetchUrl(File tmpFile,
Archive archive,
String urlString,
String description,
ITaskMonitor monitor) {
URL url; URL url;
description += " (%1$d%%, %2$.0f KiB/s, %3$d %4$s left)";
FileOutputStream os = null; FileOutputStream os = null;
InputStream is = null; InputStream is = null;
try { try {
@@ -467,22 +489,49 @@ public class UpdaterWindowImpl {
long total = 0; long total = 0;
long size = archive.getSize(); long size = archive.getSize();
long inc = size / NUM_FETCH_URL_MONITOR_INC; long inc = size / NUM_MONITOR_INC;
long next_inc = inc; long next_inc = inc;
long startMs = System.currentTimeMillis();
long nextMs = startMs + 2000; // start update after 2 seconds
while ((n = is.read(buf)) >= 0) { while ((n = is.read(buf)) >= 0) {
if (n > 0) { if (n > 0) {
os.write(buf, 0, n); os.write(buf, 0, n);
digester.update(buf, 0, n); digester.update(buf, 0, n);
} }
long timeMs = System.currentTimeMillis();
total += n; total += n;
if (total >= next_inc) { if (total >= next_inc) {
monitor.incProgress(1); monitor.incProgress(1);
next_inc += inc; next_inc += inc;
} }
if (monitor.cancelRequested()) { if (timeMs > nextMs) {
long delta = timeMs - startMs;
if (total > 0 && delta > 0) {
// percent left to download
int percent = (int) (100 * total / size);
// speed in KiB/s
float speed = (float)total / (float)delta * (1000.f / 1024.f);
// time left to download the rest at the current KiB/s rate
int timeLeft = (speed > 1e-3) ?
(int)(((size - total) / 1024.0f) / speed) :
0;
String timeUnit = "seconds";
if (timeLeft > 120) {
timeUnit = "minutes";
timeLeft /= 60;
}
monitor.setDescription(description, percent, speed, timeLeft, timeUnit);
}
nextMs = timeMs + 1000; // update every second
}
if (monitor.isCancelRequested()) {
monitor.setResult("Download aborted by user at %1$d bytes.", total); monitor.setResult("Download aborted by user at %1$d bytes.", total);
return false; return false;
} }
@@ -540,14 +589,234 @@ public class UpdaterWindowImpl {
return false; return false;
} }
/**
* Install the given archive in the given folder.
*/
private boolean installArchive(Archive archive, File archiveFile, ITaskMonitor monitor) { private boolean installArchive(Archive archive, File archiveFile, ITaskMonitor monitor) {
monitor.setDescription("Installing %1$s", archive.getShortDescription()); String name = archive.getParentPackage().getShortDescription();
String desc = String.format("Installing %1$s", name);
monitor.setDescription(desc);
File destFolder = archive.getParentPackage().getInstallFolder(mUpdaterData.getOsSdkRoot()); File destFolder = archive.getParentPackage().getInstallFolder(mUpdaterData.getOsSdkRoot());
File unzipDestFolder = destFolder;
File renamedDestFolder = null;
try {
// If this folder already exists, unzip in a temporary folder and then move/unlink.
if (destFolder.exists()) {
// Find a new temp folder that doesn't exist yet
unzipDestFolder = findTempFolder(destFolder, "new"); //$NON-NLS-1$
if (unzipDestFolder == null) {
// this should not seriously happen.
monitor.setResult("Failed to find a suitable temp directory similar to %1$s.",
destFolder.getPath());
return false;
}
if (!unzipDestFolder.mkdirs()) {
monitor.setResult("Failed to create temp directory %1$s",
unzipDestFolder.getPath());
return false;
}
}
if (!unzipFolder(archiveFile, archive.getSize(), unzipDestFolder, desc, monitor)) {
return false;
}
if (unzipDestFolder != destFolder) {
// Swap the old folder by the new one.
// Both original folders will be deleted in the finally clause below.
renamedDestFolder = findTempFolder(destFolder, "old"); //$NON-NLS-1$
if (renamedDestFolder == null) {
// this should not seriously happen.
monitor.setResult("Failed to find a suitable temp directory similar to %1$s.",
destFolder.getPath());
return false;
}
if (!destFolder.renameTo(renamedDestFolder)) {
monitor.setResult("Failed to rename directory %1$s to %2$s",
destFolder.getPath(), renamedDestFolder.getPath());
return false;
}
if (!unzipDestFolder.renameTo(destFolder)) {
monitor.setResult("Failed to rename directory %1$s to %2$s",
unzipDestFolder.getPath(), destFolder.getPath());
return false;
}
}
return true;
} finally {
// Cleanup if the unzip folder is still set.
deleteFileOrFolder(renamedDestFolder);
if (unzipDestFolder != destFolder) {
deleteFileOrFolder(unzipDestFolder);
}
}
}
private boolean unzipFolder(File archiveFile,
long compressedSize,
File unzipDestFolder,
String description,
ITaskMonitor monitor) {
description += " (%1$d%%)";
FileInputStream fis = null;
ZipInputStream zis = null;
try {
fis = new FileInputStream(archiveFile);
zis = new ZipInputStream(fis);
// To advance the percent and the progress bar, we don't know the number of
// items left to unzip. However we know the size of the archive and the size of
// each uncompressed item. The zip file format overhead is negligible so that's
// a good approximation.
long incStep = compressedSize / NUM_MONITOR_INC;
long incTotal = 0;
long incCurr = 0;
int lastPercent = 0;
byte[] buf = new byte[65536];
ZipEntry entry;
while ((entry = zis.getNextEntry()) != null) {
String name = entry.getName();
// ZipFile entries should have forward slashes, but not all Zip
// implementations can be expected to do that.
name = name.replace('\\', '/');
File destFile = new File(unzipDestFolder, name);
if (name.endsWith("/")) { //$NON-NLS-1$
// Create directory if it doesn't exist yet. This allows us to create
// empty directories.
if (!destFile.isDirectory() && !destFile.mkdirs()) {
monitor.setResult("Failed to create temp directory %1$s",
destFile.getPath());
return false;
}
continue;
} else if (name.indexOf('/') != -1) {
// Otherwise it's a file in a sub-directory.
// Make sure the parent directory has been created.
File parentDir = destFile.getParentFile();
if (!parentDir.isDirectory()) {
if (!parentDir.mkdirs()) {
monitor.setResult("Failed to create temp directory %1$s",
parentDir.getPath());
return false;
}
}
}
FileOutputStream fos = null;
try {
fos = new FileOutputStream(destFile);
int n;
while ((n = zis.read(buf)) != -1) {
if (n > 0) {
fos.write(buf, 0, n);
}
}
} finally {
if (fos != null) {
fos.close();
}
}
// Increment progress bar to match. We update only between files.
for(incTotal += entry.getCompressedSize(); incCurr < incTotal; incCurr += incStep) {
monitor.incProgress(1);
}
int percent = (int) (100 * incTotal / compressedSize);
if (percent != lastPercent) {
monitor.setDescription(description, percent);
lastPercent = percent;
}
if (monitor.isCancelRequested()) {
return false;
}
}
return true;
} catch (IOException e) {
monitor.setResult("Unzip failed: %1$s", e.getMessage());
} finally {
if (zis != null) {
try {
zis.close();
} catch (IOException e) {
// pass
}
}
if (fis != null) {
try {
fis.close();
} catch (IOException e) {
// pass
}
}
}
return false; return false;
} }
/**
* Finds a temp folder which name is similar to the one of the ideal folder
* and with a ".tmpN" appended.
* <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
* returned folder does not exist yet.
* <p/>
* Returns null if no such folder can be found (e.g. if all candidates exist),
* which is rather unlikely.
*/
private File findTempFolder(File idealFolder, String suffix) {
String basePath = idealFolder.getPath();
for (int i = 1; i < 100; i++) {
File folder = new File(String.format("%1$s.%2$s%3$02d", basePath, suffix, i)); //$NON-NLS-1$
if (!folder.exists()) {
return folder;
}
}
return null;
}
/**
* Deletes a file or a directory.
* Directories are deleted recursively.
* The argument can be null.
*/
private void deleteFileOrFolder(File fileOrFolder) {
if (fileOrFolder != null) {
if (fileOrFolder.isDirectory()) {
// Must delete content recursively first
for (File item : fileOrFolder.listFiles()) {
deleteFileOrFolder(item);
}
}
if (!fileOrFolder.delete()) {
fileOrFolder.deleteOnExit();
}
}
}
// End of hiding from SWT Designer // End of hiding from SWT Designer
//$hide<<$ //$hide<<$
} }