1 // Copyright 2019 650 Industries. All rights reserved. 2 3 // swiftlint:disable closure_body_length 4 // swiftlint:disable function_body_length 5 6 import ExpoModulesCore 7 8 /** 9 * Exported module which provides to the JS runtime information about the currently running update 10 * and updates state, along with methods to check for and download new updates, reload with the 11 * newest downloaded update applied, and read/clear native log entries. 12 * 13 * Communicates with the updates hub (AppController in most apps, EXAppLoaderExpoUpdates in 14 * Expo Go and legacy standalone apps) via EXUpdatesService, an internal module which is overridden 15 * by EXUpdatesBinding, a scoped module, in Expo Go. 16 */ 17 public final class UpdatesModule: Module { 18 private let updatesService: EXUpdatesModuleInterface? 19 private let methodQueue = DispatchQueue(label: "expo.modules.EXUpdatesQueue") 20 21 public required init(appContext: AppContext) { 22 updatesService = appContext.legacyModule(implementing: EXUpdatesModuleInterface.self) 23 super.init(appContext: appContext) 24 } 25 26 public func definition() -> ModuleDefinition { 27 Name("EXUpdates") 28 29 Constants { 30 let releaseChannel = updatesService?.config.releaseChannel 31 let channel = updatesService?.config.requestHeaders["expo-channel-name"] ?? "" 32 let runtimeVersion = updatesService?.config.runtimeVersion ?? "" 33 let isMissingRuntimeVersion = updatesService?.config.isMissingRuntimeVersion() 34 35 guard let updatesService = updatesService, 36 updatesService.isStarted, 37 let launchedUpdate = updatesService.launchedUpdate else { 38 return [ 39 "isEnabled": false, 40 "isEmbeddedLaunch": false, 41 "isMissingRuntimeVersion": isMissingRuntimeVersion, 42 "releaseChannel": releaseChannel, 43 "runtimeVersion": runtimeVersion, 44 "channel": channel 45 ] 46 } 47 48 let commitTime = UInt64(floor(launchedUpdate.commitTime.timeIntervalSince1970 * 1000)) 49 return [ 50 "isEnabled": true, 51 "isEmbeddedLaunch": updatesService.isEmbeddedLaunch, 52 "isUsingEmbeddedAssets": updatesService.isUsingEmbeddedAssets, 53 "updateId": launchedUpdate.updateId.uuidString, 54 "manifest": launchedUpdate.manifest.rawManifestJSON(), 55 "localAssets": updatesService.assetFilesMap ?? [:], 56 "isEmergencyLaunch": updatesService.isEmergencyLaunch, 57 "isMissingRuntimeVersion": isMissingRuntimeVersion, 58 "releaseChannel": releaseChannel, 59 "runtimeVersion": runtimeVersion, 60 "channel": channel, 61 "commitTime": commitTime, 62 "nativeDebug": UpdatesUtils.isNativeDebuggingEnabled() 63 ] 64 } 65 66 AsyncFunction("reloadAsync") { (promise: Promise) in 67 guard let updatesService = updatesService, updatesService.config.isEnabled else { 68 throw UpdatesDisabledException() 69 } 70 guard updatesService.canRelaunch else { 71 throw UpdatesNotInitializedException() 72 } 73 updatesService.requestRelaunch { success in 74 if success { 75 promise.resolve(nil) 76 } else { 77 promise.reject(UpdatesReloadException()) 78 } 79 } 80 } 81 82 AsyncFunction("checkForUpdateAsync") { (promise: Promise) in 83 guard let updatesService = updatesService, updatesService.config.isEnabled else { 84 throw UpdatesDisabledException() 85 } 86 guard updatesService.isStarted else { 87 throw UpdatesNotInitializedException() 88 } 89 90 var extraHeaders: [String: Any] = [:] 91 updatesService.database.databaseQueue.sync { 92 extraHeaders = FileDownloader.extraHeaders( 93 withDatabase: updatesService.database, 94 config: updatesService.config, 95 launchedUpdate: updatesService.launchedUpdate, 96 embeddedUpdate: updatesService.embeddedUpdate 97 ) 98 } 99 100 let fileDownloader = FileDownloader(config: updatesService.config) 101 fileDownloader.downloadManifest( 102 // swiftlint:disable:next force_unwrapping 103 fromURL: updatesService.config.updateUrl!, 104 withDatabase: updatesService.database, 105 extraHeaders: extraHeaders 106 ) { update in 107 let launchedUpdate = updatesService.launchedUpdate 108 let selectionPolicy = updatesService.selectionPolicy 109 if selectionPolicy.shouldLoadNewUpdate(update, withLaunchedUpdate: launchedUpdate, filters: update.manifestFilters) { 110 promise.resolve([ 111 "isAvailable": true, 112 "manifest": update.manifest.rawManifestJSON() 113 ]) 114 } 115 } errorBlock: { error in 116 promise.reject("ERR_UPDATES_CHECK", error.localizedDescription) 117 } 118 } 119 120 AsyncFunction("readLogEntriesAsync") { (maxAge: Int) -> [[String: Any]] in 121 // maxAge is in milliseconds, convert to seconds 122 do { 123 return try UpdatesLogReader().getLogEntries(newerThan: Date(timeIntervalSinceNow: TimeInterval(-1 * (maxAge / 1000)))) 124 } catch { 125 throw Exception(name: "ERR_UPDATES_READ_LOGS", description: error.localizedDescription) 126 } 127 } 128 129 AsyncFunction("clearLogEntriesAsync") { (promise: Promise) in 130 UpdatesLogReader().purgeLogEntries(olderThan: Date()) { error in 131 guard let error = error else { 132 promise.resolve(nil) 133 return 134 } 135 promise.reject("ERR_UPDATES_READ_LOGS", error.localizedDescription) 136 } 137 } 138 139 AsyncFunction("fetchUpdateAsync") { (promise: Promise) in 140 guard let updatesService = updatesService, updatesService.config.isEnabled else { 141 throw UpdatesDisabledException() 142 } 143 guard updatesService.isStarted else { 144 throw UpdatesNotInitializedException() 145 } 146 147 let remoteAppLoader = RemoteAppLoader( 148 config: updatesService.config, 149 database: updatesService.database, 150 directory: updatesService.directory, 151 launchedUpdate: updatesService.launchedUpdate, 152 completionQueue: methodQueue 153 ) 154 remoteAppLoader.loadUpdate( 155 // swiftlint:disable:next force_unwrapping 156 fromURL: updatesService.config.updateUrl! 157 ) { update in 158 return updatesService.selectionPolicy.shouldLoadNewUpdate( 159 update, 160 withLaunchedUpdate: updatesService.launchedUpdate, 161 filters: update.manifestFilters 162 ) 163 } asset: { _, _, _, _ in 164 // do nothing for now 165 } success: { update in 166 if let update = update { 167 updatesService.resetSelectionPolicy() 168 promise.resolve([ 169 "isNew": true, 170 "manifest": update.manifest.rawManifestJSON() 171 ]) 172 } else { 173 promise.resolve([ 174 "isNew": false 175 ]) 176 } 177 } error: { error in 178 promise.reject("ERR_UPDATES_FETCH", "Failed to download new update: \(error.localizedDescription)") 179 } 180 } 181 } 182 } 183