@otakit/capacitor-updater 2.3.2 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/README.md +46 -5
  2. package/android/build.gradle +16 -0
  3. package/android/src/main/java/com/otakit/updater/BundleCrypto.java +38 -17
  4. package/android/src/main/java/com/otakit/updater/BundleInfo.java +10 -0
  5. package/android/src/main/java/com/otakit/updater/BundleStore.java +134 -86
  6. package/android/src/main/java/com/otakit/updater/CheckFailure.java +53 -0
  7. package/android/src/main/java/com/otakit/updater/DeltaAssembler.java +46 -41
  8. package/android/src/main/java/com/otakit/updater/DeviceEventClient.java +125 -26
  9. package/android/src/main/java/com/otakit/updater/DocumentReadyBridge.java +71 -0
  10. package/android/src/main/java/com/otakit/updater/DownloadRetry.java +144 -0
  11. package/android/src/main/java/com/otakit/updater/EventOutbox.java +174 -0
  12. package/android/src/main/java/com/otakit/updater/FileDownloader.java +80 -0
  13. package/android/src/main/java/com/otakit/updater/ForegroundDeadline.java +84 -0
  14. package/android/src/main/java/com/otakit/updater/HashUtils.java +26 -0
  15. package/android/src/main/java/com/otakit/updater/ManifestClient.java +12 -9
  16. package/android/src/main/java/com/otakit/updater/ManifestKeyConfig.java +47 -0
  17. package/android/src/main/java/com/otakit/updater/ManifestVerifier.java +22 -4
  18. package/android/src/main/java/com/otakit/updater/SDKVersion.java +9 -0
  19. package/android/src/main/java/com/otakit/updater/UpdateOwner.java +27 -0
  20. package/android/src/main/java/com/otakit/updater/UpdaterCoordinator.java +154 -59
  21. package/android/src/main/java/com/otakit/updater/UpdaterPlugin.java +284 -209
  22. package/android/src/main/java/com/otakit/updater/WebViewActivation.java +26 -0
  23. package/dist/esm/definitions.d.ts +3 -3
  24. package/dist/esm/definitions.d.ts.map +1 -1
  25. package/ios/Sources/UpdaterPlugin/BundleCrypto.swift +35 -3
  26. package/ios/Sources/UpdaterPlugin/BundleInfo.swift +13 -0
  27. package/ios/Sources/UpdaterPlugin/BundleStore.swift +103 -77
  28. package/ios/Sources/UpdaterPlugin/CheckFailure.swift +41 -0
  29. package/ios/Sources/UpdaterPlugin/DeltaAssembler.swift +29 -17
  30. package/ios/Sources/UpdaterPlugin/DeviceEventClient.swift +12 -10
  31. package/ios/Sources/UpdaterPlugin/DocumentReadyBridge.swift +43 -0
  32. package/ios/Sources/UpdaterPlugin/DownloadRetry.swift +51 -0
  33. package/ios/Sources/UpdaterPlugin/Downloader.swift +97 -39
  34. package/ios/Sources/UpdaterPlugin/EventOutbox.swift +183 -0
  35. package/ios/Sources/UpdaterPlugin/ForegroundDeadline.swift +94 -0
  36. package/ios/Sources/UpdaterPlugin/HashUtils.swift +16 -0
  37. package/ios/Sources/UpdaterPlugin/ManifestClient.swift +2 -4
  38. package/ios/Sources/UpdaterPlugin/ManifestKeyConfig.swift +20 -0
  39. package/ios/Sources/UpdaterPlugin/ManifestVerifier.swift +7 -1
  40. package/ios/Sources/UpdaterPlugin/SDKVersion.swift +4 -0
  41. package/ios/Sources/UpdaterPlugin/UpdateOwner.swift +12 -0
  42. package/ios/Sources/UpdaterPlugin/UpdaterCoordinator.swift +146 -110
  43. package/ios/Sources/UpdaterPlugin/UpdaterPlugin.swift +206 -202
  44. package/ios/Tests/UpdaterPluginTests/BundleCryptoTests.swift +80 -0
  45. package/ios/Tests/UpdaterPluginTests/BundlePersistenceTests.swift +102 -0
  46. package/ios/Tests/UpdaterPluginTests/CheckFailureTests.swift +55 -0
  47. package/ios/Tests/UpdaterPluginTests/DeltaCacheIntegrityTests.swift +69 -0
  48. package/ios/Tests/UpdaterPluginTests/DocumentReadyBridgeTests.swift +102 -0
  49. package/ios/Tests/UpdaterPluginTests/DownloadIntegrityTests.swift +40 -0
  50. package/ios/Tests/UpdaterPluginTests/DownloadRetryTests.swift +189 -0
  51. package/ios/Tests/UpdaterPluginTests/EventDeliveryTests.swift +56 -0
  52. package/ios/Tests/UpdaterPluginTests/EventOutboxTests.swift +86 -0
  53. package/ios/Tests/UpdaterPluginTests/ForegroundDeadlineTests.swift +144 -0
  54. package/ios/Tests/UpdaterPluginTests/ManifestKeyConfigTests.swift +48 -0
  55. package/ios/Tests/UpdaterPluginTests/UpdateOwnerTests.swift +34 -0
  56. package/ios/Tests/UpdaterPluginTests/UpdaterCoordinatorTests.swift +394 -0
  57. package/package.json +7 -3
@@ -10,6 +10,16 @@ import java.util.concurrent.locks.ReentrantLock;
10
10
 
11
11
  final class UpdaterCoordinator {
12
12
 
13
+ static final class Trial {
14
+
15
+ final String bundleId;
16
+ final String activationId = java.util.UUID.randomUUID().toString();
17
+
18
+ Trial(String bundleId) {
19
+ this.bundleId = bundleId;
20
+ }
21
+ }
22
+
13
23
  interface BundlePredicate {
14
24
  boolean test(BundleInfo bundle);
15
25
  }
@@ -42,6 +52,8 @@ final class UpdaterCoordinator {
42
52
  final String channel;
43
53
  final String releaseId;
44
54
  final String detail;
55
+ final String attemptId;
56
+ final String phase;
45
57
 
46
58
  DeviceEventPayload(
47
59
  String action,
@@ -50,6 +62,19 @@ final class UpdaterCoordinator {
50
62
  String channel,
51
63
  String releaseId,
52
64
  String detail
65
+ ) {
66
+ this(action, bundleVersion, runtimeVersion, channel, releaseId, detail, null, null);
67
+ }
68
+
69
+ DeviceEventPayload(
70
+ String action,
71
+ String bundleVersion,
72
+ String runtimeVersion,
73
+ String channel,
74
+ String releaseId,
75
+ String detail,
76
+ String attemptId,
77
+ String phase
53
78
  ) {
54
79
  this.action = action;
55
80
  this.bundleVersion = bundleVersion;
@@ -57,6 +82,8 @@ final class UpdaterCoordinator {
57
82
  this.channel = channel;
58
83
  this.releaseId = releaseId;
59
84
  this.detail = detail;
85
+ this.attemptId = attemptId;
86
+ this.phase = phase;
60
87
  }
61
88
  }
62
89
 
@@ -86,18 +113,18 @@ final class UpdaterCoordinator {
86
113
  static final class StartupPreparation {
87
114
 
88
115
  final String activationPath;
89
- final String trialBundleId;
116
+ final Trial trial;
90
117
  final List<String> cleanupBundleIds;
91
118
  final DeviceEventPayload eventPayload;
92
119
 
93
120
  StartupPreparation(
94
121
  String activationPath,
95
- String trialBundleId,
122
+ Trial trial,
96
123
  List<String> cleanupBundleIds,
97
124
  DeviceEventPayload eventPayload
98
125
  ) {
99
126
  this.activationPath = activationPath;
100
- this.trialBundleId = trialBundleId;
127
+ this.trial = trial;
101
128
  this.cleanupBundleIds = cleanupBundleIds;
102
129
  this.eventPayload = eventPayload;
103
130
  }
@@ -106,12 +133,12 @@ final class UpdaterCoordinator {
106
133
  static final class ApplyPreparation {
107
134
 
108
135
  final String activationPath;
109
- final String trialBundleId;
136
+ final Trial trial;
110
137
  final List<String> cleanupBundleIds;
111
138
 
112
- ApplyPreparation(String activationPath, String trialBundleId, List<String> cleanupBundleIds) {
139
+ ApplyPreparation(String activationPath, Trial trial, List<String> cleanupBundleIds) {
113
140
  this.activationPath = activationPath;
114
- this.trialBundleId = trialBundleId;
141
+ this.trial = trial;
115
142
  this.cleanupBundleIds = cleanupBundleIds;
116
143
  }
117
144
 
@@ -176,6 +203,7 @@ final class UpdaterCoordinator {
176
203
  private final BundleStore store;
177
204
  private final AtomicBoolean operationInProgress = new AtomicBoolean(false);
178
205
  private final ReentrantLock stateLock = new ReentrantLock();
206
+ private Trial activeTrial;
179
207
 
180
208
  UpdaterCoordinator(BundleStore store) {
181
209
  this.store = store;
@@ -214,6 +242,7 @@ final class UpdaterCoordinator {
214
242
 
215
243
  List<String> pruneIncompatibleBundles(BundlePredicate isCompatibleRuntime) {
216
244
  return withStateLock(() -> {
245
+ store.protectedBundleIds();
217
246
  Set<String> cleanupBundleIds = new LinkedHashSet<>();
218
247
 
219
248
  for (BundleInfo bundle : store.listDownloadedBundleInfos()) {
@@ -234,6 +263,8 @@ final class UpdaterCoordinator {
234
263
 
235
264
  StartupPreparation normalizeStartupState(BundlePredicate isBundleUsable) {
236
265
  return withStateLock(() -> {
266
+ store.protectedBundleIds();
267
+ activeTrial = null;
237
268
  Set<String> cleanupBundleIds = new LinkedHashSet<>();
238
269
  clearStaleBundlePointersLocked();
239
270
  normalizeStagedPointerLocked(isBundleUsable, cleanupBundleIds);
@@ -251,20 +282,28 @@ final class UpdaterCoordinator {
251
282
  }
252
283
 
253
284
  BundleInfo normalizedCurrent = store.getCurrentBundle();
254
- if (!normalizedCurrent.isBuiltin() && !isBundleUsable.test(normalizedCurrent)) {
285
+ if (
286
+ !normalizedCurrent.isBuiltin() &&
287
+ (normalizedCurrent.status == BundleStatus.ERROR || !isBundleUsable.test(normalizedCurrent))
288
+ ) {
255
289
  cleanupBundleIds.add(normalizedCurrent.id);
256
- normalizedCurrent = restoreFallbackOrBuiltinLocked(isBundleUsable, cleanupBundleIds);
290
+ normalizedCurrent = restoreFallbackOrBuiltinLocked(
291
+ isBundleUsable,
292
+ cleanupBundleIds,
293
+ normalizedCurrent.id,
294
+ null
295
+ );
257
296
  }
258
297
 
259
- String trialBundleId = null;
298
+ Trial trial = null;
260
299
  if (!normalizedCurrent.isBuiltin() && normalizedCurrent.status == BundleStatus.PENDING) {
261
300
  normalizedCurrent = updateStatusLocked(normalizedCurrent, BundleStatus.TRIAL);
262
- trialBundleId = normalizedCurrent.id;
301
+ trial = beginTrialLocked(normalizedCurrent.id);
263
302
  }
264
303
 
265
304
  return new StartupPreparation(
266
305
  normalizedCurrent.isBuiltin() ? null : normalizedCurrent.path,
267
- trialBundleId,
306
+ trial,
268
307
  new ArrayList<>(cleanupBundleIds),
269
308
  eventPayload
270
309
  );
@@ -318,6 +357,9 @@ final class UpdaterCoordinator {
318
357
 
319
358
  List<String> stageDownloadedBundle(BundleInfo bundle) throws Exception {
320
359
  return withStateLock(() -> {
360
+ if (bundle.isBuiltin() || store.protectedBundleIds().contains(bundle.id)) {
361
+ throw new IllegalStateException("Cannot overwrite a referenced bundle installation");
362
+ }
321
363
  Set<String> cleanupBundleIds = new LinkedHashSet<>();
322
364
  String previousStagedId = store.getStagedBundleId();
323
365
  store.saveBundle(bundle);
@@ -343,6 +385,11 @@ final class UpdaterCoordinator {
343
385
  ) {
344
386
  return withStateLock(() -> {
345
387
  Set<String> cleanupBundleIds = new LinkedHashSet<>();
388
+ BundleInfo previousCurrent = store.getCurrentBundle();
389
+ // A staged update must not replace the only running trial before it is acknowledged.
390
+ if (previousCurrent.status == BundleStatus.TRIAL) {
391
+ return new ApplyPreparation(null, null, new ArrayList<>());
392
+ }
346
393
  BundleInfo staged = stagedBundleLocked(
347
394
  true,
348
395
  isCompatibleRuntime,
@@ -353,38 +400,42 @@ final class UpdaterCoordinator {
353
400
  return new ApplyPreparation(null, null, new ArrayList<>(cleanupBundleIds));
354
401
  }
355
402
 
356
- BundleInfo previousCurrent = store.getCurrentBundle();
357
- if (previousCurrent.isBuiltin()) {
358
- store.setFallbackBundleId(null);
359
- } else {
360
- store.setFallbackBundleId(previousCurrent.id);
361
- }
362
-
363
- store.setCurrentBundleId(staged.id);
364
- store.setStagedBundleId(null);
365
-
366
- String trialBundleId = null;
403
+ String fallbackId = previousCurrent.isBuiltin()
404
+ ? null
405
+ : (previousCurrent.status == BundleStatus.SUCCESS
406
+ ? previousCurrent.id
407
+ : store.getFallbackBundleId());
367
408
  if (staged.status == BundleStatus.PENDING) {
368
409
  staged = updateStatusLocked(staged, BundleStatus.TRIAL);
369
- trialBundleId = staged.id;
370
- } else if (staged.status == BundleStatus.TRIAL) {
371
- trialBundleId = staged.id;
372
410
  }
411
+ store.setCoreState(staged.id, fallbackId, null, store.getLastFailedBundle());
412
+ Trial trial = staged.status == BundleStatus.TRIAL ? beginTrialLocked(staged.id) : null;
373
413
 
374
- return new ApplyPreparation(staged.path, trialBundleId, new ArrayList<>(cleanupBundleIds));
414
+ return new ApplyPreparation(staged.path, trial, new ArrayList<>(cleanupBundleIds));
375
415
  });
376
416
  }
377
417
 
378
- NotifyReadyPreparation prepareNotifyAppReady() {
418
+ NotifyReadyPreparation prepareNotifyAppReady(String activationId) {
379
419
  return withStateLock(() -> {
380
420
  BundleInfo current = store.getCurrentBundle();
381
- if (current.isBuiltin() || current.status != BundleStatus.TRIAL) {
421
+ if (
422
+ current.isBuiltin() ||
423
+ activeTrial == null ||
424
+ !activeTrial.activationId.equals(activationId) ||
425
+ !activeTrial.bundleId.equals(current.id) ||
426
+ (current.status != BundleStatus.TRIAL &&
427
+ !(current.status == BundleStatus.SUCCESS &&
428
+ !current.id.equals(store.getFallbackBundleId())))
429
+ ) {
382
430
  return new NotifyReadyPreparation(null, new ArrayList<>());
383
431
  }
384
432
 
385
433
  String oldFallbackId = store.getFallbackBundleId();
386
- updateStatusLocked(current, BundleStatus.SUCCESS);
434
+ if (current.status == BundleStatus.TRIAL) {
435
+ updateStatusLocked(current, BundleStatus.SUCCESS);
436
+ }
387
437
  store.setFallbackBundleId(current.id);
438
+ activeTrial = null;
388
439
 
389
440
  Set<String> cleanupBundleIds = new LinkedHashSet<>();
390
441
  if (oldFallbackId != null && !oldFallbackId.equals(current.id)) {
@@ -398,15 +449,31 @@ final class UpdaterCoordinator {
398
449
  current.runtimeVersion,
399
450
  current.channel,
400
451
  current.releaseId,
401
- null
452
+ null,
453
+ current.attemptId(),
454
+ "readiness"
402
455
  ),
403
456
  new ArrayList<>(cleanupBundleIds)
404
457
  );
405
458
  });
406
459
  }
407
460
 
408
- RollbackPreparation prepareRollback(String reason, BundlePredicate isBundleUsable) {
461
+ RollbackPreparation prepareRollback(
462
+ Trial expectedTrial,
463
+ String reason,
464
+ BundlePredicate isBundleUsable
465
+ ) {
409
466
  return withStateLock(() -> {
467
+ BundleInfo current = store.getCurrentBundle();
468
+ if (
469
+ activeTrial == null ||
470
+ expectedTrial == null ||
471
+ !activeTrial.activationId.equals(expectedTrial.activationId) ||
472
+ !current.id.equals(expectedTrial.bundleId) ||
473
+ current.status != BundleStatus.TRIAL
474
+ ) {
475
+ return new RollbackPreparation(false, null, null, new ArrayList<>());
476
+ }
410
477
  Set<String> cleanupBundleIds = new LinkedHashSet<>();
411
478
  LockedRollbackResult rollback = rollbackLocked(reason, isBundleUsable, cleanupBundleIds);
412
479
  return new RollbackPreparation(
@@ -418,11 +485,9 @@ final class UpdaterCoordinator {
418
485
  });
419
486
  }
420
487
 
421
- boolean isCurrentTrialBundle(String bundleId) {
422
- return withStateLock(() -> {
423
- BundleInfo current = store.getCurrentBundle();
424
- return current.id.equals(bundleId) && current.status == BundleStatus.TRIAL;
425
- });
488
+ private Trial beginTrialLocked(String bundleId) {
489
+ activeTrial = new Trial(bundleId);
490
+ return activeTrial;
426
491
  }
427
492
 
428
493
  void cleanupBundles(List<String> bundleIds) {
@@ -433,9 +498,22 @@ final class UpdaterCoordinator {
433
498
  }
434
499
  }
435
500
 
436
- for (String bundleId : uniqueIds) {
437
- deleteRecursivelyQuietly(store.bundleDirectory(bundleId));
438
- }
501
+ withStateLock(() -> {
502
+ Set<String> protectedIds;
503
+ try {
504
+ protectedIds = store.protectedBundleIds();
505
+ } catch (Exception error) {
506
+ // An unreadable state is not evidence that these files are unreferenced.
507
+ return null;
508
+ }
509
+ for (String bundleId : uniqueIds) {
510
+ // Cleanup can run after a newer transition has made this bundle live again.
511
+ if (!protectedIds.contains(bundleId)) {
512
+ deleteRecursivelyQuietly(store.bundleDirectory(bundleId));
513
+ }
514
+ }
515
+ return null;
516
+ });
439
517
  }
440
518
 
441
519
  private <T> T withStateLock(LockedSupplier<T> work) {
@@ -482,7 +560,7 @@ final class UpdaterCoordinator {
482
560
  return;
483
561
  }
484
562
 
485
- if (!isBundleUsable.test(fallback)) {
563
+ if (fallback.status != BundleStatus.SUCCESS || !isBundleUsable.test(fallback)) {
486
564
  store.setFallbackBundleId(null);
487
565
  cleanupBundleIds.add(fallback.id);
488
566
  }
@@ -576,11 +654,14 @@ final class UpdaterCoordinator {
576
654
  return new LockedRollbackResult(false, null, null);
577
655
  }
578
656
 
579
- BundleInfo failed = updateStatusLocked(current, BundleStatus.ERROR);
580
- store.setLastFailedBundle(failed);
581
- store.setStagedBundleId(null);
582
-
583
- BundleInfo fallback = restoreFallbackOrBuiltinLocked(isBundleUsable, cleanupBundleIds);
657
+ BundleInfo failed = current.withStatus(BundleStatus.ERROR);
658
+ BundleInfo fallback = restoreFallbackOrBuiltinLocked(
659
+ isBundleUsable,
660
+ cleanupBundleIds,
661
+ current.id,
662
+ failed
663
+ );
664
+ activeTrial = null;
584
665
  cleanupBundleIds.add(current.id);
585
666
 
586
667
  return new LockedRollbackResult(
@@ -592,40 +673,54 @@ final class UpdaterCoordinator {
592
673
  current.runtimeVersion,
593
674
  current.channel,
594
675
  current.releaseId,
595
- reason
676
+ reason,
677
+ current.attemptId(),
678
+ "rollback"
596
679
  )
597
680
  );
598
681
  }
599
682
 
600
683
  private BundleInfo restoreFallbackOrBuiltinLocked(
601
684
  BundlePredicate isBundleUsable,
602
- Set<String> cleanupBundleIds
685
+ Set<String> cleanupBundleIds,
686
+ String excludedBundleId,
687
+ BundleInfo failedBundle
603
688
  ) {
689
+ BundleInfo restored = store.builtinBundle();
604
690
  String fallbackId = store.getFallbackBundleId();
605
691
  if (fallbackId != null) {
606
692
  BundleInfo fallback = store.getBundle(fallbackId);
607
- if (fallback != null && !fallback.isBuiltin() && isBundleUsable.test(fallback)) {
608
- store.setCurrentBundleId(fallback.id);
609
- return fallback;
693
+ if (
694
+ fallback != null &&
695
+ !fallback.isBuiltin() &&
696
+ !fallback.id.equals(excludedBundleId) &&
697
+ fallback.status == BundleStatus.SUCCESS &&
698
+ isBundleUsable.test(fallback)
699
+ ) {
700
+ restored = fallback;
610
701
  }
611
702
  }
612
703
 
613
- if (fallbackId != null) {
614
- store.setFallbackBundleId(null);
615
- if (!"builtin".equals(fallbackId)) {
616
- cleanupBundleIds.add(fallbackId);
617
- }
704
+ if (fallbackId != null && !"builtin".equals(fallbackId) && !fallbackId.equals(restored.id)) {
705
+ cleanupBundleIds.add(fallbackId);
618
706
  }
619
-
620
- store.setCurrentBundleId(null);
621
- return store.builtinBundle();
707
+ String restoredId = restored.isBuiltin() ? null : restored.id;
708
+ store.setCoreState(
709
+ restoredId,
710
+ restoredId,
711
+ failedBundle == null ? store.getStagedBundleId() : null,
712
+ failedBundle == null ? store.getLastFailedBundle() : failedBundle
713
+ );
714
+ return restored;
622
715
  }
623
716
 
624
717
  private BundleInfo updateStatusLocked(BundleInfo bundle, BundleStatus status) {
625
718
  BundleInfo updated = bundle.withStatus(status);
626
719
  try {
627
720
  store.saveBundle(updated);
628
- } catch (Exception ignored) {}
721
+ } catch (Exception error) {
722
+ throw new IllegalStateException("Could not persist bundle status", error);
723
+ }
629
724
  return updated;
630
725
  }
631
726