Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/@expo/cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
- [Internal] Resolve the dev server port once instead of re-deriving it, and read the URL environment variables outside the `UrlCreator` ([#48236](https://github.com/expo/expo/pull/48236) by [@ramonclaudio](https://github.com/ramonclaudio))
- Update `expo start --private-key-path` help text ([#47795](https://github.com/expo/expo/pull/47795) by [@kitten](https://github.com/kitten))
- Re-enable sextant QR code for Zed ([#48382](https://github.com/expo/expo/pull/48382) by [@mchisolm0](https://github.com/mchisolm0))
- [Internal] Fix the flaky `waitForActionAsync` timeout test by driving it with fake timers instead of real time. ([#48811](https://github.com/expo/expo/pull/48811) by [@expo-bot](https://github.com/expo-bot))
- Bump to `multitars@1.0.2` to address symlink and unicode bugs ([#48833](https://github.com/expo/expo/pull/48833) by [@kitten](https://github.com/kitten))
- [Internal] Move static HTML asset injection into `getStaticContent()`. ([#47006](https://github.com/expo/expo/pull/47006) by [@hassankhan](https://github.com/hassankhan))
- Prewarm Metro transform workers while waiting for the first development bundle request ([#48836](https://github.com/expo/expo/pull/48836) by [@kitten](https://github.com/kitten))
Expand Down
17 changes: 14 additions & 3 deletions packages/@expo/cli/src/utils/__tests__/delay-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,26 @@ describe(waitForActionAsync, () => {
expect(fn).toHaveBeenCalledTimes(1);
});
it(`times out waiting for a given action to return a truthy value`, async () => {
// Fake timers keep the number of polls deterministic. On real timers the
// action runs twice only while two 80ms intervals still fit in the 100ms
// max wait time. A busy machine can delay the first interval past 100ms,
// and then the action runs once.
jest.useFakeTimers();
const fn = jest.fn(() => '');
const result = await waitForActionAsync({

const promise = waitForActionAsync({
action: fn,
interval: 80,
maxWaitTime: 100,
});
expect(result).toEqual('');
// First interval: 80ms elapsed, still inside the max wait time.
await jest.advanceTimersByTimeAsync(80);
// Second interval: 160ms elapsed, so the loop stops.
await jest.advanceTimersByTimeAsync(80);

await expect(promise).resolves.toEqual('');
expect(fn).toHaveBeenCalledTimes(2);
}, 500);
});
});

describe(resolveWithTimeout, () => {
Expand Down
6 changes: 6 additions & 0 deletions packages/expo-updates/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

### 🐛 Bug fixes

- [iOS] Fix the embedded manifest recording the wrong `packagerHash` for assets that ship scale variants iOS does not allow (such as `@1.5x` and `@4x`): the hashes were read by the filtered scale index instead of the asset's own, so those images resolved to an empty URI and rendered blank in release builds. ([#48811](https://github.com/expo/expo/pull/48811) by [@expo-bot](https://github.com/expo-bot))
- [iOS] Fix `expo-dev-client` being detected as installed when it is absent, which enabled `USE_DEV_CLIENT` and printed a `MODULE_NOT_FOUND` trace during `pod install`. ([#49233](https://github.com/expo/expo/pull/49233) by [@dennytosp](https://github.com/dennytosp))
- [iOS] Set `always_out_of_date` on the `Generate updates resources for expo-updates` script_phase to silence the Xcode "run script phase will run on every build" dependency-analysis warning. ([#47622](https://github.com/expo/expo/pull/47622) by [@ramonclaudio](https://github.com/ramonclaudio))
- [iOS] Fix two launch crashes reachable when a native module resolves the updates controller before `start()` runs: reading `launchedUpdateId` / `launchAssetPath` / `launchAssetUrl()` trapped on an implicitly-unwrapped optional, and the unsynchronized `stateChangeListeners` dictionary could fault while `UpdatesStateMachine` iterated it. ([#48898](https://github.com/expo/expo/pull/48898) by [@spsaucier](https://github.com/spsaucier))
Expand All @@ -26,6 +27,11 @@
- [Android] Register the embedded update in a single transaction. An interrupted registration previously left an update row with no launch asset, which is treated as launchable and then fails every cold start with "Launch asset not found for update"; it now leaves no row, so the next launch registers it cleanly. ([#49130](https://github.com/expo/expo/pull/49130) by [@gwdp](https://github.com/gwdp))
- [Android] Apply `reactNativeArchitectures` as a CMake ABI filter so single-ABI builds no longer compile the native code for unused ABIs. ([#49299](https://github.com/expo/expo/pull/49299) by [@alanjhughes](https://github.com/alanjhughes))
- [Android] Keep the Room-generated `UpdatesDatabase_Impl` constructor, so minified release builds no longer crash with `NoSuchMethodException` on first database access. ([#47729](https://github.com/expo/expo/pull/47729) by [@gabrieldonadel](https://github.com/gabrieldonadel))
- [iOS] Propagate database failures from `addNewAssets` instead of swallowing them, which let an update be marked ready without its launch asset and then fail every launch. ([#49456](https://github.com/expo/expo/pull/49456) by [@alanjhughes](https://github.com/alanjhughes))
- [iOS] Repair ready updates that are missing their launch asset by demoting them to pending during launcher selection, so a corrupted row is skipped and retried instead of failing every launch. ([#49457](https://github.com/expo/expo/pull/49457) by [@alanjhughes](https://github.com/alanjhughes))
- [iOS] Register a downloaded update's assets, links, and ready status in a single transaction, so an interrupted or partially failed registration leaves no partial state behind. ([#49458](https://github.com/expo/expo/pull/49458) by [@alanjhughes](https://github.com/alanjhughes))
- [iOS] Report a specific launch asset not found error when a launchable update has no linked launch asset, instead of failing silently or, for an update with no assets at all, hanging on the splash screen. ([#49459](https://github.com/expo/expo/pull/49459) by [@alanjhughes](https://github.com/alanjhughes))
- [iOS] Preserve the cached update's launch failure as the emergency launch reason when the remote check finds no new update, instead of replacing it with the generic AppLoaderTask error. ([#49460](https://github.com/expo/expo/pull/49460) by [@alanjhughes](https://github.com/alanjhughes))

### 💡 Others

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,11 @@ public class AppLauncherWithDatabase: NSObject, AppLauncher {
)

completionQueue.async {
self.completion!(self.launchAssetError, self.launchAssetUrl != nil)
var error = self.launchAssetError
if error == nil && self.launchAssetUrl == nil {
error = UpdatesError.appLauncherLaunchAssetNotFound(updateId: launchedUpdate.updateId)
}
self.completion!(error, self.launchAssetUrl != nil)
self.completion = nil
}
return
Expand All @@ -211,6 +215,16 @@ public class AppLauncherWithDatabase: NSObject, AppLauncher {
self.assetFilesMap = UpdatesUtils.embeddedAssetsMap(withConfig: config, database: database, logger: logger)

let assets = launchedUpdate.assets()!
if assets.isEmpty {
// a ready update with no assets cannot launch, and an empty loop below would never
// invoke the completion
completionQueue.async {
self.completion!(UpdatesError.appLauncherLaunchAssetNotFound(updateId: launchedUpdate.updateId), false)
self.completion = nil
}
return
}

let totalAssetCount = assets.count
for asset in assets {
let assetLocalUrl = directory.appendingPathComponent(asset.filename)
Expand All @@ -230,7 +244,12 @@ public class AppLauncherWithDatabase: NSObject, AppLauncher {

if self.completedAssets == totalAssetCount {
self.completionQueue.async {
self.completion!(self.launchAssetError, self.launchAssetUrl != nil)
var error = self.launchAssetError
if error == nil && self.launchAssetUrl == nil {
// completing without an error and without a launch asset would fail the launch silently
error = UpdatesError.appLauncherLaunchAssetNotFound(updateId: launchedUpdate.updateId)
}
self.completion!(error, self.launchAssetUrl != nil)
self.completion = nil
}
}
Expand Down
46 changes: 24 additions & 22 deletions packages/expo-updates/ios/EXUpdates/AppLoader/AppLoader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -358,18 +358,29 @@ open class AppLoader: NSObject {
database.databaseQueue.async {
self.arrayLock.lock()

guard let updateResponse = self.updateResponseContainingManifest,
let updateManifest = updateResponse.manifestUpdateResponsePart?.updateManifest else {
self.arrayLock.unlock()
self.finish(withError: UpdatesError.appLoaderFinishedWithoutManifest)
return
}

var assetsToLink: [UpdateAsset] = []
for existingAsset in self.existingAssets {
var existingAssetFound: Bool = false
let existingAssetFound: Bool
do {
existingAssetFound = try self.database.addExistingAsset(
existingAsset,
toUpdateWithId: self.updateResponseContainingManifest!.manifestUpdateResponsePart!.updateManifest.updateId
)
existingAssetFound = try self.database.asset(withKey: existingAsset.key) != nil
} catch {
self.logger.warn(message: "Error searching for existing asset in DB: \(error.localizedDescription)")
// treating a failed check as "not found" would re-insert an existing asset, and the
// key-conflicting replace cascade-deletes every update that uses the old row
self.arrayLock.unlock()
self.finish(withError: UpdatesError.appLoaderUnknownError(cause: error))
return
}

if !existingAssetFound {
if existingAssetFound {
assetsToLink.append(existingAsset)
} else {
// the database and filesystem have gotten out of sync
// do our best to create a new entry for this file even though it already existed on disk
// TODO: we should probably get rid of this assumption that if an asset exists on disk with the same filename, it's the same asset
Expand All @@ -392,27 +403,18 @@ open class AppLoader: NSObject {
}

do {
try self.database.addNewAssets(
self.finishedAssets,
toUpdateWithId: self.updateResponseContainingManifest!.manifestUpdateResponsePart!.updateManifest.updateId
try self.database.finishUpdateRegistration(
updateManifest,
newAssets: self.finishedAssets,
existingAssets: assetsToLink,
markFinished: self.erroredAssets.isEmpty
)
} catch {
self.arrayLock.unlock()
self.finish(withError: UpdatesError.appLoaderUnknownError(cause: error))
return
}

if self.erroredAssets.isEmpty {
do {
let updateManifest = self.updateResponseContainingManifest!.manifestUpdateResponsePart!.updateManifest
try self.database.markUpdateFinished(updateManifest)
} catch {
self.arrayLock.unlock()
self.finish(withError: UpdatesError.appLoaderUnknownError(cause: error))
return
}
}

var successBlock: AppLoaderSuccessBlock?
var errorBlock: AppLoaderErrorBlock?

Expand All @@ -432,7 +434,7 @@ open class AppLoader: NSObject {
if let errorBlock = errorBlock {
errorBlock(UpdatesError.appLoaderFailedToLoadAllAssets)
} else if let successBlock = successBlock {
successBlock(self.updateResponseContainingManifest!)
successBlock(updateResponse)
}
}
}
Expand Down
16 changes: 10 additions & 6 deletions packages/expo-updates/ios/EXUpdates/AppLoader/AppLoaderTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ public enum BackgroundUpdateStatus: Int {
*/
@objc(EXUpdatesAppLoaderTask)
@objcMembers
public final class AppLoaderTask: NSObject {
public class AppLoaderTask: NSObject {
public weak var delegate: AppLoaderTaskDelegate?
public weak var swiftDelegate: AppLoaderTaskSwiftDelegate?

Expand All @@ -126,6 +126,7 @@ public final class AppLoaderTask: NSObject {
private var isTimerFinished: Bool
private var hasLaunched: Bool
private var isUpToDate: Bool
private var fallbackLaunchError: UpdatesError = .appLoaderTaskUnexpectedErrorDuringLaunch
private let loaderTaskQueue: DispatchQueue

public required init(
Expand Down Expand Up @@ -166,10 +167,13 @@ public final class AppLoaderTask: NSObject {
loadEmbeddedUpdate {
self.launch { error, success in
if !success {
// keep the cause so a launch that finishes without a specific error reports
// this failure instead of the generic error
let cause = UpdatesError.appLoaderTaskFailedToLaunch(cause: error)
self.fallbackLaunchError = cause
if !shouldCheckForUpdate {
self.finish(withError: error)
}
let cause = UpdatesError.appLoaderTaskFailedToLaunch(cause: error)
self.logger.error(
cause: cause,
code: .updateFailedToLoad
Expand Down Expand Up @@ -223,7 +227,7 @@ public final class AppLoaderTask: NSObject {
} else {
delegate.appLoaderTask(
self,
didFinishWithError: error ?? UpdatesError.appLoaderTaskUnexpectedErrorDuringLaunch
didFinishWithError: error ?? self.fallbackLaunchError
)
}
}
Expand Down Expand Up @@ -268,7 +272,7 @@ public final class AppLoaderTask: NSObject {
}
}

private func loadEmbeddedUpdate(withCompletion completion: @escaping () -> Void) {
func loadEmbeddedUpdate(withCompletion completion: @escaping () -> Void) {
AppLauncherWithDatabase.launchableUpdate(
withConfig: config,
database: database,
Expand Down Expand Up @@ -325,13 +329,13 @@ public final class AppLoaderTask: NSObject {
}
}

private func launch(withCompletion completion: @escaping (_ error: UpdatesError?, _ success: Bool) -> Void) {
func launch(withCompletion completion: @escaping (_ error: UpdatesError?, _ success: Bool) -> Void) {
let launcher = AppLauncherWithDatabase(config: config, database: database, directory: directory, completionQueue: loaderTaskQueue, logger: self.logger)
candidateLauncher = launcher
launcher.launchUpdate(withSelectionPolicy: selectionPolicy, completion: completion)
}

private func loadRemoteUpdate(withCompletion completion: @escaping (_ remoteError: UpdatesError?, _ updateResponse: UpdateResponse?) -> Void) {
func loadRemoteUpdate(withCompletion completion: @escaping (_ remoteError: UpdatesError?, _ updateResponse: UpdateResponse?) -> Void) {
remoteAppLoader = RemoteAppLoader(
config: config,
logger: logger,
Expand Down
Loading
Loading