Skip to content

Commit 6071286

Browse files
authored
Download dynamic patch to separate file, then rename it to install. (flutter#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.
1 parent 4c9136b commit 6071286

File tree

3 files changed

+106
-31
lines changed

3 files changed

+106
-31
lines changed

shell/platform/android/io/flutter/app/FlutterActivityDelegate.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -345,7 +345,7 @@ private void runBundle(String appBundlePath) {
345345
FlutterRunArguments args = new FlutterRunArguments();
346346
ArrayList<String> bundlePaths = new ArrayList<>();
347347
if (FlutterMain.getResourceUpdater() != null) {
348-
File patchFile = FlutterMain.getResourceUpdater().getPatch();
348+
File patchFile = FlutterMain.getResourceUpdater().getInstalledPatch();
349349
bundlePaths.add(patchFile.getPath());
350350
}
351351
bundlePaths.add(appBundlePath);

shell/platform/android/io/flutter/view/ResourceExtractor.java

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -50,37 +50,68 @@ private class ExtractTask extends AsyncTask<Void, Void, Void> {
5050
protected Void doInBackground(Void... unused) {
5151
final File dataDir = new File(PathUtils.getDataDirectory(mContext));
5252

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

58-
final String timestamp = checkTimestamp(dataDir, updateManifest);
59-
if (timestamp == null) {
60-
return null;
61-
}
60+
try {
61+
if (resourceUpdater != null) {
62+
File updateFile = resourceUpdater.getDownloadedPatch();
63+
File activeFile = resourceUpdater.getInstalledPatch();
64+
65+
if (updateFile.exists()) {
66+
// Graduate patch file as active for asset manager.
67+
if (activeFile.exists() && !activeFile.delete()) {
68+
Log.w(TAG, "Could not delete file " + activeFile);
69+
return null;
70+
}
71+
if (!updateFile.renameTo(activeFile)) {
72+
Log.w(TAG, "Could not create file " + activeFile);
73+
return null;
74+
}
75+
}
76+
}
6277

63-
deleteFiles();
78+
JSONObject updateManifest = readUpdateManifest();
79+
if (!validateUpdateManifest(updateManifest)) {
80+
updateManifest = null;
81+
}
6482

65-
if (updateManifest != null) {
66-
if (!extractUpdate(dataDir)) {
83+
final String timestamp = checkTimestamp(dataDir, updateManifest);
84+
if (timestamp == null) {
6785
return null;
6886
}
69-
}
7087

71-
if (!extractAPK(dataDir)) {
72-
return null;
73-
}
88+
deleteFiles();
7489

75-
if (timestamp != null) {
76-
try {
77-
new File(dataDir, timestamp).createNewFile();
78-
} catch (IOException e) {
79-
Log.w(TAG, "Failed to write resource timestamp");
90+
if (updateManifest != null) {
91+
if (!extractUpdate(dataDir)) {
92+
return null;
93+
}
8094
}
81-
}
8295

83-
return null;
96+
if (!extractAPK(dataDir)) {
97+
return null;
98+
}
99+
100+
if (timestamp != null) {
101+
try {
102+
new File(dataDir, timestamp).createNewFile();
103+
} catch (IOException e) {
104+
Log.w(TAG, "Failed to write resource timestamp");
105+
}
106+
}
107+
108+
return null;
109+
110+
} finally {
111+
if (resourceUpdater != null) {
112+
resourceUpdater.getInstallationLock().unlock();
113+
}
114+
}
84115
}
85116
}
86117

@@ -200,7 +231,7 @@ private boolean extractUpdate(File dataDir) {
200231
return true;
201232
}
202233

203-
File updateFile = resourceUpdater.getPatch();
234+
File updateFile = resourceUpdater.getInstalledPatch();
204235
if (!updateFile.exists()) {
205236
return true;
206237
}
@@ -288,7 +319,7 @@ private String checkTimestamp(File dataDir, JSONObject updateManifest) {
288319
} else {
289320
ResourceUpdater resourceUpdater = FlutterMain.getResourceUpdater();
290321
assert resourceUpdater != null;
291-
File patchFile = resourceUpdater.getPatch();
322+
File patchFile = resourceUpdater.getInstalledPatch();
292323
assert patchFile.exists();
293324
if (patchNumber != null) {
294325
expectedTimestamp += "-" + patchNumber + "-" + patchFile.lastModified();
@@ -361,7 +392,7 @@ private JSONObject readUpdateManifest() {
361392
return null;
362393
}
363394

364-
File updateFile = resourceUpdater.getPatch();
395+
File updateFile = resourceUpdater.getInstalledPatch();
365396
if (!updateFile.exists()) {
366397
return null;
367398
}

shell/platform/android/io/flutter/view/ResourceUpdater.java

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,16 @@
1515
import java.io.InputStream;
1616
import java.io.IOException;
1717
import java.io.OutputStream;
18+
import java.lang.Math;
1819
import java.net.HttpURLConnection;
1920
import java.net.URI;
2021
import java.net.URISyntaxException;
2122
import java.net.URL;
2223
import java.util.Date;
2324
import java.util.concurrent.CancellationException;
2425
import java.util.concurrent.ExecutionException;
26+
import java.util.concurrent.locks.Lock;
27+
import java.util.concurrent.locks.ReentrantLock;
2528

2629
public final class ResourceUpdater {
2730
private static final String TAG = "ResourceUpdater";
@@ -58,20 +61,44 @@ enum InstallMode {
5861
IMMEDIATE
5962
}
6063

64+
/// Lock that prevents replacement of the install file by the downloader
65+
/// while this file is being extracted, since these can happen in parallel.
66+
Lock getInstallationLock() {
67+
return installationLock;
68+
}
69+
70+
// Patch file that's fully installed and is ready to serve assets.
71+
// This file represents the final stage in the installation process.
72+
public File getInstalledPatch() {
73+
return new File(context.getFilesDir().toString() + "/patch.zip");
74+
}
75+
76+
// Patch file that's finished downloading and is ready to be installed.
77+
// This is a separate file in order to prevent serving assets from patch
78+
// that failed installing for any reason, such as mismatched APK version.
79+
File getDownloadedPatch() {
80+
return new File(getInstalledPatch().getPath() + ".install");
81+
}
82+
6183
private class DownloadTask extends AsyncTask<String, String, Void> {
6284
@Override
6385
protected Void doInBackground(String... unused) {
6486
try {
6587
URL unresolvedURL = new URL(buildUpdateDownloadURL());
66-
File localFile = getPatch();
88+
89+
// Download to transient file to avoid extracting incomplete download.
90+
File localFile = new File(getInstalledPatch().getPath() + ".download");
6791

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

7195
HttpURLConnection connection =
7296
(HttpURLConnection)unresolvedURL.openConnection();
7397

74-
long lastDownloadTime = localFile.lastModified();
98+
long lastDownloadTime = Math.max(
99+
getDownloadedPatch().lastModified(),
100+
getInstalledPatch().lastModified());
101+
75102
if (lastDownloadTime != 0) {
76103
Log.i(TAG, "Active update timestamp " + lastDownloadTime);
77104
connection.setIfModifiedSince(lastDownloadTime);
@@ -107,9 +134,29 @@ protected Void doInBackground(String... unused) {
107134

108135
long totalMillis = new Date().getTime() - startMillis;
109136
Log.i(TAG, "Update downloaded in " + totalMillis / 100 / 10. + "s");
137+
}
138+
}
139+
140+
// Wait renaming the file if extraction is in progress.
141+
installationLock.lock();
142+
143+
try {
144+
File updateFile = getDownloadedPatch();
110145

146+
// Graduate downloaded file as ready for installation.
147+
if (updateFile.exists() && !updateFile.delete()) {
148+
Log.w(TAG, "Could not delete file " + updateFile);
149+
return null;
150+
}
151+
if (!localFile.renameTo(updateFile)) {
152+
Log.w(TAG, "Could not create file " + updateFile);
111153
return null;
112154
}
155+
156+
return null;
157+
158+
} finally {
159+
installationLock.unlock();
113160
}
114161

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

122169
private final Context context;
123170
private DownloadTask downloadTask;
171+
private final Lock installationLock = new ReentrantLock();
124172

125173
public ResourceUpdater(Context context) {
126174
this.context = context;
@@ -137,10 +185,6 @@ private String getAPKVersion() {
137185
}
138186
}
139187

140-
public File getPatch() {
141-
return new File(context.getFilesDir().toString() + "/patch.zip");
142-
}
143-
144188
private String buildUpdateDownloadURL() {
145189
Bundle metaData;
146190
try {

0 commit comments

Comments
 (0)