Skip to content

Commit

Permalink
Download dynamic patch to separate file, then rename it to install. (f…
Browse files Browse the repository at this point in the history
…lutter#7428)

This fixes potential race condition when patch gets downloaded on top
of zip file that's currently in active use by resource extractor and/or
asset manager. This change is necessary since download can happen in
the background while normal application operations are in progress.
  • Loading branch information
sbaranov authored Jan 10, 2019
1 parent 4c9136b commit 6071286
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -345,7 +345,7 @@ private void runBundle(String appBundlePath) {
FlutterRunArguments args = new FlutterRunArguments();
ArrayList<String> bundlePaths = new ArrayList<>();
if (FlutterMain.getResourceUpdater() != null) {
File patchFile = FlutterMain.getResourceUpdater().getPatch();
File patchFile = FlutterMain.getResourceUpdater().getInstalledPatch();
bundlePaths.add(patchFile.getPath());
}
bundlePaths.add(appBundlePath);
Expand Down
79 changes: 55 additions & 24 deletions shell/platform/android/io/flutter/view/ResourceExtractor.java
Original file line number Diff line number Diff line change
Expand Up @@ -50,37 +50,68 @@ private class ExtractTask extends AsyncTask<Void, Void, Void> {
protected Void doInBackground(Void... unused) {
final File dataDir = new File(PathUtils.getDataDirectory(mContext));

JSONObject updateManifest = readUpdateManifest();
if (!validateUpdateManifest(updateManifest)) {
updateManifest = null;
ResourceUpdater resourceUpdater = FlutterMain.getResourceUpdater();
if (resourceUpdater != null) {
// Protect patch file from being overwritten by downloader while
// it's being extracted since downloading happens asynchronously.
resourceUpdater.getInstallationLock().lock();
}

final String timestamp = checkTimestamp(dataDir, updateManifest);
if (timestamp == null) {
return null;
}
try {
if (resourceUpdater != null) {
File updateFile = resourceUpdater.getDownloadedPatch();
File activeFile = resourceUpdater.getInstalledPatch();

if (updateFile.exists()) {
// Graduate patch file as active for asset manager.
if (activeFile.exists() && !activeFile.delete()) {
Log.w(TAG, "Could not delete file " + activeFile);
return null;
}
if (!updateFile.renameTo(activeFile)) {
Log.w(TAG, "Could not create file " + activeFile);
return null;
}
}
}

deleteFiles();
JSONObject updateManifest = readUpdateManifest();
if (!validateUpdateManifest(updateManifest)) {
updateManifest = null;
}

if (updateManifest != null) {
if (!extractUpdate(dataDir)) {
final String timestamp = checkTimestamp(dataDir, updateManifest);
if (timestamp == null) {
return null;
}
}

if (!extractAPK(dataDir)) {
return null;
}
deleteFiles();

if (timestamp != null) {
try {
new File(dataDir, timestamp).createNewFile();
} catch (IOException e) {
Log.w(TAG, "Failed to write resource timestamp");
if (updateManifest != null) {
if (!extractUpdate(dataDir)) {
return null;
}
}
}

return null;
if (!extractAPK(dataDir)) {
return null;
}

if (timestamp != null) {
try {
new File(dataDir, timestamp).createNewFile();
} catch (IOException e) {
Log.w(TAG, "Failed to write resource timestamp");
}
}

return null;

} finally {
if (resourceUpdater != null) {
resourceUpdater.getInstallationLock().unlock();
}
}
}
}

Expand Down Expand Up @@ -200,7 +231,7 @@ private boolean extractUpdate(File dataDir) {
return true;
}

File updateFile = resourceUpdater.getPatch();
File updateFile = resourceUpdater.getInstalledPatch();
if (!updateFile.exists()) {
return true;
}
Expand Down Expand Up @@ -288,7 +319,7 @@ private String checkTimestamp(File dataDir, JSONObject updateManifest) {
} else {
ResourceUpdater resourceUpdater = FlutterMain.getResourceUpdater();
assert resourceUpdater != null;
File patchFile = resourceUpdater.getPatch();
File patchFile = resourceUpdater.getInstalledPatch();
assert patchFile.exists();
if (patchNumber != null) {
expectedTimestamp += "-" + patchNumber + "-" + patchFile.lastModified();
Expand Down Expand Up @@ -361,7 +392,7 @@ private JSONObject readUpdateManifest() {
return null;
}

File updateFile = resourceUpdater.getPatch();
File updateFile = resourceUpdater.getInstalledPatch();
if (!updateFile.exists()) {
return null;
}
Expand Down
56 changes: 50 additions & 6 deletions shell/platform/android/io/flutter/view/ResourceUpdater.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,16 @@
import java.io.InputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.Math;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.Date;
import java.util.concurrent.CancellationException;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public final class ResourceUpdater {
private static final String TAG = "ResourceUpdater";
Expand Down Expand Up @@ -58,20 +61,44 @@ enum InstallMode {
IMMEDIATE
}

/// Lock that prevents replacement of the install file by the downloader
/// while this file is being extracted, since these can happen in parallel.
Lock getInstallationLock() {
return installationLock;
}

// Patch file that's fully installed and is ready to serve assets.
// This file represents the final stage in the installation process.
public File getInstalledPatch() {
return new File(context.getFilesDir().toString() + "/patch.zip");
}

// Patch file that's finished downloading and is ready to be installed.
// This is a separate file in order to prevent serving assets from patch
// that failed installing for any reason, such as mismatched APK version.
File getDownloadedPatch() {
return new File(getInstalledPatch().getPath() + ".install");
}

private class DownloadTask extends AsyncTask<String, String, Void> {
@Override
protected Void doInBackground(String... unused) {
try {
URL unresolvedURL = new URL(buildUpdateDownloadURL());
File localFile = getPatch();

// Download to transient file to avoid extracting incomplete download.
File localFile = new File(getInstalledPatch().getPath() + ".download");

long startMillis = new Date().getTime();
Log.i(TAG, "Checking for updates at " + unresolvedURL);

HttpURLConnection connection =
(HttpURLConnection)unresolvedURL.openConnection();

long lastDownloadTime = localFile.lastModified();
long lastDownloadTime = Math.max(
getDownloadedPatch().lastModified(),
getInstalledPatch().lastModified());

if (lastDownloadTime != 0) {
Log.i(TAG, "Active update timestamp " + lastDownloadTime);
connection.setIfModifiedSince(lastDownloadTime);
Expand Down Expand Up @@ -107,9 +134,29 @@ protected Void doInBackground(String... unused) {

long totalMillis = new Date().getTime() - startMillis;
Log.i(TAG, "Update downloaded in " + totalMillis / 100 / 10. + "s");
}
}

// Wait renaming the file if extraction is in progress.
installationLock.lock();

try {
File updateFile = getDownloadedPatch();

// Graduate downloaded file as ready for installation.
if (updateFile.exists() && !updateFile.delete()) {
Log.w(TAG, "Could not delete file " + updateFile);
return null;
}
if (!localFile.renameTo(updateFile)) {
Log.w(TAG, "Could not create file " + updateFile);
return null;
}

return null;

} finally {
installationLock.unlock();
}

} catch (IOException e) {
Expand All @@ -121,6 +168,7 @@ protected Void doInBackground(String... unused) {

private final Context context;
private DownloadTask downloadTask;
private final Lock installationLock = new ReentrantLock();

public ResourceUpdater(Context context) {
this.context = context;
Expand All @@ -137,10 +185,6 @@ private String getAPKVersion() {
}
}

public File getPatch() {
return new File(context.getFilesDir().toString() + "/patch.zip");
}

private String buildUpdateDownloadURL() {
Bundle metaData;
try {
Expand Down

0 comments on commit 6071286

Please sign in to comment.