blob: d17ebaef0b6c977dce1540e04fec8670db320f9a [file] [log] [blame]
Richard Uhler28e73232019-01-21 16:48:55 +00001/*
2 * Copyright (C) 2018 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.server.rollback;
18
Narayan Kamathc034fe92019-01-23 10:48:17 +000019import android.annotation.NonNull;
Richard Uhlera7e9b2d2019-01-22 17:20:58 +000020import android.content.pm.VersionedPackage;
Richard Uhler28e73232019-01-21 16:48:55 +000021import android.content.rollback.PackageRollbackInfo;
Narayan Kamathc034fe92019-01-23 10:48:17 +000022import android.content.rollback.PackageRollbackInfo.RestoreInfo;
Richard Uhler28e73232019-01-21 16:48:55 +000023import android.content.rollback.RollbackInfo;
Narayan Kamathc034fe92019-01-23 10:48:17 +000024import android.util.IntArray;
Richard Uhler28e73232019-01-21 16:48:55 +000025import android.util.Log;
26
27import libcore.io.IoUtils;
28
29import org.json.JSONArray;
30import org.json.JSONException;
31import org.json.JSONObject;
32
33import java.io.File;
34import java.io.IOException;
35import java.io.PrintWriter;
Richard Uhler1f571c62019-01-31 15:16:46 +000036import java.nio.file.Files;
Richard Uhler28e73232019-01-21 16:48:55 +000037import java.time.Instant;
38import java.time.format.DateTimeParseException;
39import java.util.ArrayList;
40import java.util.List;
41
42/**
43 * Helper class for loading and saving rollback data to persistent storage.
44 */
45class RollbackStore {
46 private static final String TAG = "RollbackManager";
47
48 // Assuming the rollback data directory is /data/rollback, we use the
49 // following directory structure to store persisted data for available and
50 // recently executed rollbacks:
51 // /data/rollback/
52 // available/
53 // XXX/
54 // rollback.json
55 // com.package.A/
56 // base.apk
57 // com.package.B/
58 // base.apk
59 // YYY/
60 // rollback.json
61 // com.package.C/
62 // base.apk
63 // recently_executed.json
64 //
Richard Uhlerb9d54472019-01-22 12:50:08 +000065 // * XXX, YYY are the rollbackIds for the corresponding rollbacks.
Richard Uhler28e73232019-01-21 16:48:55 +000066 // * rollback.json contains all relevant metadata for the rollback. This
67 // file is not written until the rollback is made available.
68 //
69 // TODO: Use AtomicFile for all the .json files?
70 private final File mRollbackDataDir;
71 private final File mAvailableRollbacksDir;
72 private final File mRecentlyExecutedRollbacksFile;
73
74 RollbackStore(File rollbackDataDir) {
75 mRollbackDataDir = rollbackDataDir;
76 mAvailableRollbacksDir = new File(mRollbackDataDir, "available");
77 mRecentlyExecutedRollbacksFile = new File(mRollbackDataDir, "recently_executed.json");
78 }
79
80 /**
81 * Reads the list of available rollbacks from persistent storage.
82 */
83 List<RollbackData> loadAvailableRollbacks() {
84 List<RollbackData> availableRollbacks = new ArrayList<>();
85 mAvailableRollbacksDir.mkdirs();
86 for (File rollbackDir : mAvailableRollbacksDir.listFiles()) {
87 if (rollbackDir.isDirectory()) {
88 try {
89 RollbackData data = loadRollbackData(rollbackDir);
90 availableRollbacks.add(data);
91 } catch (IOException e) {
92 // Note: Deleting the rollbackDir here will cause pending
93 // rollbacks to be deleted. This should only ever happen
94 // if reloadPersistedData is called while there are
95 // pending rollbacks. The reloadPersistedData method is
96 // currently only for testing, so that should be okay.
97 Log.e(TAG, "Unable to read rollback data at " + rollbackDir, e);
98 removeFile(rollbackDir);
99 }
100 }
101 }
102 return availableRollbacks;
103 }
104
105 /**
Narayan Kamathc034fe92019-01-23 10:48:17 +0000106 * Converts an {@code JSONArray} of integers to an {@code IntArray}.
107 */
108 private static @NonNull IntArray convertToIntArray(@NonNull JSONArray jsonArray)
109 throws JSONException {
110 if (jsonArray.length() == 0) {
111 return new IntArray();
112 }
113
114 final int[] ret = new int[jsonArray.length()];
115 for (int i = 0; i < ret.length; ++i) {
116 ret[i] = jsonArray.getInt(i);
117 }
118
119 return IntArray.wrap(ret);
120 }
121
122 /**
123 * Converts an {@code IntArray} into an {@code JSONArray} of integers.
124 */
125 private static @NonNull JSONArray convertToJsonArray(@NonNull IntArray intArray) {
126 JSONArray jsonArray = new JSONArray();
127 for (int i = 0; i < intArray.size(); ++i) {
128 jsonArray.put(intArray.get(i));
129 }
130
131 return jsonArray;
132 }
133
134 private static @NonNull JSONArray convertToJsonArray(@NonNull List<RestoreInfo> list)
135 throws JSONException {
136 JSONArray jsonArray = new JSONArray();
137 for (RestoreInfo ri : list) {
138 JSONObject jo = new JSONObject();
139 jo.put("userId", ri.userId);
140 jo.put("appId", ri.appId);
141 jo.put("seInfo", ri.seInfo);
142 jsonArray.put(jo);
143 }
144
145 return jsonArray;
146 }
147
148 private static @NonNull ArrayList<RestoreInfo> convertToRestoreInfoArray(
149 @NonNull JSONArray array) throws JSONException {
150 ArrayList<RestoreInfo> restoreInfos = new ArrayList<>();
151
152 for (int i = 0; i < array.length(); ++i) {
153 JSONObject jo = array.getJSONObject(i);
154 restoreInfos.add(new RestoreInfo(
155 jo.getInt("userId"),
156 jo.getInt("appId"),
157 jo.getString("seInfo")));
158 }
159
160 return restoreInfos;
161 }
162
163 /**
Richard Uhler28e73232019-01-21 16:48:55 +0000164 * Reads the list of recently executed rollbacks from persistent storage.
165 */
166 List<RollbackInfo> loadRecentlyExecutedRollbacks() {
167 List<RollbackInfo> recentlyExecutedRollbacks = new ArrayList<>();
168 if (mRecentlyExecutedRollbacksFile.exists()) {
169 try {
170 // TODO: How to cope with changes to the format of this file from
171 // when RollbackStore is updated in the future?
172 String jsonString = IoUtils.readFileAsString(
173 mRecentlyExecutedRollbacksFile.getAbsolutePath());
174 JSONObject object = new JSONObject(jsonString);
175 JSONArray array = object.getJSONArray("recentlyExecuted");
176 for (int i = 0; i < array.length(); ++i) {
177 JSONObject element = array.getJSONObject(i);
Richard Uhlerb9d54472019-01-22 12:50:08 +0000178 int rollbackId = element.getInt("rollbackId");
Richard Uhler0a79b322019-01-23 13:51:07 +0000179 List<PackageRollbackInfo> packages = packageRollbackInfosFromJson(
180 element.getJSONArray("packages"));
Richard Uhlerccf035d2019-02-04 14:04:52 +0000181 boolean isStaged = element.getBoolean("isStaged");
Richard Uhlerbf5b5c42019-01-28 15:26:37 +0000182 List<VersionedPackage> causePackages = versionedPackagesFromJson(
183 element.getJSONArray("causePackages"));
Richard Uhlerccf035d2019-02-04 14:04:52 +0000184 int committedSessionId = element.getInt("committedSessionId");
185 RollbackInfo rollback = new RollbackInfo(rollbackId, packages, isStaged,
186 causePackages, committedSessionId);
Richard Uhler28e73232019-01-21 16:48:55 +0000187 recentlyExecutedRollbacks.add(rollback);
188 }
189 } catch (IOException | JSONException e) {
190 // TODO: What to do here? Surely we shouldn't just forget about
191 // everything after the point of exception?
192 Log.e(TAG, "Failed to read recently executed rollbacks", e);
193 }
194 }
195
196 return recentlyExecutedRollbacks;
197 }
198
199 /**
200 * Creates a new RollbackData instance with backupDir assigned.
201 */
Richard Uhlerb9d54472019-01-22 12:50:08 +0000202 RollbackData createAvailableRollback(int rollbackId) throws IOException {
203 File backupDir = new File(mAvailableRollbacksDir, Integer.toString(rollbackId));
Richard Uhler60ac7062019-02-05 13:25:39 +0000204 return new RollbackData(rollbackId, backupDir, -1, true);
Narayan Kamathfcd4a042019-02-01 14:16:37 +0000205 }
206
207 RollbackData createPendingStagedRollback(int rollbackId, int stagedSessionId)
208 throws IOException {
209 File backupDir = new File(mAvailableRollbacksDir, Integer.toString(rollbackId));
Richard Uhler60ac7062019-02-05 13:25:39 +0000210 return new RollbackData(rollbackId, backupDir, stagedSessionId, false);
Richard Uhler28e73232019-01-21 16:48:55 +0000211 }
212
213 /**
Richard Uhler1f571c62019-01-31 15:16:46 +0000214 * Creates a backup copy of the apk or apex for a package.
Richard Uhler28e73232019-01-21 16:48:55 +0000215 */
Richard Uhler1f571c62019-01-31 15:16:46 +0000216 static void backupPackageCode(RollbackData data, String packageName, String codePath)
217 throws IOException {
218 File sourceFile = new File(codePath);
219 File targetDir = new File(data.backupDir, packageName);
220 targetDir.mkdirs();
221 File targetFile = new File(targetDir, sourceFile.getName());
222
223 // TODO: Copy by hard link instead to save on cpu and storage space?
224 Files.copy(sourceFile.toPath(), targetFile.toPath());
225 }
226
227 /**
228 * Returns the apk or apex file backed up for the given package.
229 * Returns null if none found.
230 */
231 static File getPackageCode(RollbackData data, String packageName) {
232 File targetDir = new File(data.backupDir, packageName);
233 File[] files = targetDir.listFiles();
234 if (files == null || files.length != 1) {
235 return null;
236 }
237 return files[0];
Richard Uhler28e73232019-01-21 16:48:55 +0000238 }
239
240 /**
241 * Writes the metadata for an available rollback to persistent storage.
242 */
243 void saveAvailableRollback(RollbackData data) throws IOException {
244 try {
245 JSONObject dataJson = new JSONObject();
Richard Uhlerb9d54472019-01-22 12:50:08 +0000246 dataJson.put("rollbackId", data.rollbackId);
Richard Uhler0a79b322019-01-23 13:51:07 +0000247 dataJson.put("packages", toJson(data.packages));
Richard Uhler28e73232019-01-21 16:48:55 +0000248 dataJson.put("timestamp", data.timestamp.toString());
Narayan Kamathfcd4a042019-02-01 14:16:37 +0000249 dataJson.put("stagedSessionId", data.stagedSessionId);
Richard Uhler60ac7062019-02-05 13:25:39 +0000250 dataJson.put("isAvailable", data.isAvailable);
Richard Uhlerba13ab22019-02-05 15:27:12 +0000251 dataJson.put("apkSessionId", data.apkSessionId);
Richard Uhler28e73232019-01-21 16:48:55 +0000252
253 PrintWriter pw = new PrintWriter(new File(data.backupDir, "rollback.json"));
254 pw.println(dataJson.toString());
255 pw.close();
256 } catch (JSONException e) {
257 throw new IOException(e);
258 }
259 }
260
261 /**
262 * Removes all persistant storage associated with the given available
263 * rollback.
264 */
265 void deleteAvailableRollback(RollbackData data) {
Narayan Kamath869f7062019-01-10 12:24:15 +0000266 // TODO(narayan): Make sure we delete the userdata snapshot along with the backup of the
267 // actual app.
Richard Uhler28e73232019-01-21 16:48:55 +0000268 removeFile(data.backupDir);
269 }
270
271 /**
272 * Writes the list of recently executed rollbacks to storage.
273 */
274 void saveRecentlyExecutedRollbacks(List<RollbackInfo> recentlyExecutedRollbacks) {
275 try {
276 JSONObject json = new JSONObject();
277 JSONArray array = new JSONArray();
278 json.put("recentlyExecuted", array);
279
280 for (int i = 0; i < recentlyExecutedRollbacks.size(); ++i) {
281 RollbackInfo rollback = recentlyExecutedRollbacks.get(i);
282 JSONObject element = new JSONObject();
Richard Uhlerb9d54472019-01-22 12:50:08 +0000283 element.put("rollbackId", rollback.getRollbackId());
Richard Uhler0a79b322019-01-23 13:51:07 +0000284 element.put("packages", toJson(rollback.getPackages()));
Richard Uhlerccf035d2019-02-04 14:04:52 +0000285 element.put("isStaged", rollback.isStaged());
Richard Uhlerbf5b5c42019-01-28 15:26:37 +0000286 element.put("causePackages", versionedPackagesToJson(rollback.getCausePackages()));
Richard Uhlerccf035d2019-02-04 14:04:52 +0000287 element.put("committedSessionId", rollback.getCommittedSessionId());
Richard Uhler28e73232019-01-21 16:48:55 +0000288 array.put(element);
289 }
290
291 PrintWriter pw = new PrintWriter(mRecentlyExecutedRollbacksFile);
292 pw.println(json.toString());
293 pw.close();
294 } catch (IOException | JSONException e) {
295 // TODO: What to do here?
296 Log.e(TAG, "Failed to save recently executed rollbacks", e);
297 }
298 }
299
300 /**
301 * Reads the metadata for a rollback from the given directory.
302 * @throws IOException in case of error reading the data.
303 */
304 private RollbackData loadRollbackData(File backupDir) throws IOException {
305 try {
Richard Uhler28e73232019-01-21 16:48:55 +0000306 File rollbackJsonFile = new File(backupDir, "rollback.json");
307 JSONObject dataJson = new JSONObject(
308 IoUtils.readFileAsString(rollbackJsonFile.getAbsolutePath()));
Richard Uhlerb9d54472019-01-22 12:50:08 +0000309
310 int rollbackId = dataJson.getInt("rollbackId");
Narayan Kamathfcd4a042019-02-01 14:16:37 +0000311 int stagedSessionId = dataJson.getInt("stagedSessionId");
Richard Uhler60ac7062019-02-05 13:25:39 +0000312 boolean isAvailable = dataJson.getBoolean("isAvailable");
Narayan Kamathfcd4a042019-02-01 14:16:37 +0000313 RollbackData data = new RollbackData(rollbackId, backupDir,
Richard Uhler60ac7062019-02-05 13:25:39 +0000314 stagedSessionId, isAvailable);
Richard Uhler0a79b322019-01-23 13:51:07 +0000315 data.packages.addAll(packageRollbackInfosFromJson(dataJson.getJSONArray("packages")));
Richard Uhler28e73232019-01-21 16:48:55 +0000316 data.timestamp = Instant.parse(dataJson.getString("timestamp"));
Richard Uhlerba13ab22019-02-05 15:27:12 +0000317 data.apkSessionId = dataJson.getInt("apkSessionId");
Richard Uhler28e73232019-01-21 16:48:55 +0000318 return data;
319 } catch (JSONException | DateTimeParseException e) {
320 throw new IOException(e);
321 }
322 }
323
Richard Uhlerbf5b5c42019-01-28 15:26:37 +0000324 private JSONObject toJson(VersionedPackage pkg) throws JSONException {
325 JSONObject json = new JSONObject();
326 json.put("packageName", pkg.getPackageName());
327 json.put("longVersionCode", pkg.getLongVersionCode());
328 return json;
329 }
330
331 private VersionedPackage versionedPackageFromJson(JSONObject json) throws JSONException {
332 String packageName = json.getString("packageName");
333 long longVersionCode = json.getLong("longVersionCode");
334 return new VersionedPackage(packageName, longVersionCode);
335 }
336
Richard Uhler0a79b322019-01-23 13:51:07 +0000337 private JSONObject toJson(PackageRollbackInfo info) throws JSONException {
338 JSONObject json = new JSONObject();
Richard Uhlerbf5b5c42019-01-28 15:26:37 +0000339 json.put("versionRolledBackFrom", toJson(info.getVersionRolledBackFrom()));
340 json.put("versionRolledBackTo", toJson(info.getVersionRolledBackTo()));
Narayan Kamathc034fe92019-01-23 10:48:17 +0000341
342 IntArray pendingBackups = info.getPendingBackups();
343 List<RestoreInfo> pendingRestores = info.getPendingRestores();
344 json.put("pendingBackups", convertToJsonArray(pendingBackups));
345 json.put("pendingRestores", convertToJsonArray(pendingRestores));
346
Narayan Kamathfcd4a042019-02-01 14:16:37 +0000347 json.put("isApex", info.isApex());
348
Richard Uhler0a79b322019-01-23 13:51:07 +0000349 return json;
350 }
351
352 private PackageRollbackInfo packageRollbackInfoFromJson(JSONObject json) throws JSONException {
Richard Uhlerbf5b5c42019-01-28 15:26:37 +0000353 VersionedPackage versionRolledBackFrom = versionedPackageFromJson(
354 json.getJSONObject("versionRolledBackFrom"));
355 VersionedPackage versionRolledBackTo = versionedPackageFromJson(
356 json.getJSONObject("versionRolledBackTo"));
Narayan Kamathc034fe92019-01-23 10:48:17 +0000357
358 final IntArray pendingBackups = convertToIntArray(
359 json.getJSONArray("pendingBackups"));
360 final ArrayList<RestoreInfo> pendingRestores = convertToRestoreInfoArray(
361 json.getJSONArray("pendingRestores"));
362
Narayan Kamathfcd4a042019-02-01 14:16:37 +0000363 final boolean isApex = json.getBoolean("isApex");
364
Narayan Kamathc034fe92019-01-23 10:48:17 +0000365 return new PackageRollbackInfo(versionRolledBackFrom, versionRolledBackTo,
Narayan Kamathfcd4a042019-02-01 14:16:37 +0000366 pendingBackups, pendingRestores, isApex);
Richard Uhlerbf5b5c42019-01-28 15:26:37 +0000367 }
368
369 private JSONArray versionedPackagesToJson(List<VersionedPackage> packages)
370 throws JSONException {
371 JSONArray json = new JSONArray();
372 for (VersionedPackage pkg : packages) {
373 json.put(toJson(pkg));
374 }
375 return json;
376 }
377
378 private List<VersionedPackage> versionedPackagesFromJson(JSONArray json) throws JSONException {
379 List<VersionedPackage> packages = new ArrayList<>();
380 for (int i = 0; i < json.length(); ++i) {
381 packages.add(versionedPackageFromJson(json.getJSONObject(i)));
382 }
383 return packages;
Richard Uhler0a79b322019-01-23 13:51:07 +0000384 }
385
386 private JSONArray toJson(List<PackageRollbackInfo> infos) throws JSONException {
387 JSONArray json = new JSONArray();
388 for (PackageRollbackInfo info : infos) {
389 json.put(toJson(info));
390 }
391 return json;
392 }
393
394 private List<PackageRollbackInfo> packageRollbackInfosFromJson(JSONArray json)
395 throws JSONException {
396 List<PackageRollbackInfo> infos = new ArrayList<>();
397 for (int i = 0; i < json.length(); ++i) {
398 infos.add(packageRollbackInfoFromJson(json.getJSONObject(i)));
399 }
400 return infos;
401 }
402
Richard Uhler28e73232019-01-21 16:48:55 +0000403 /**
404 * Deletes a file completely.
405 * If the file is a directory, its contents are deleted as well.
406 * Has no effect if the directory does not exist.
407 */
408 private void removeFile(File file) {
409 if (file.isDirectory()) {
410 for (File child : file.listFiles()) {
411 removeFile(child);
412 }
413 }
414 if (file.exists()) {
415 file.delete();
416 }
417 }
418}