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("ExpoUpdates") 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("reload") { (promise: Promise) in 67 guard let updatesService = updatesService, let config = updatesService.config, 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, 84 let config = updatesService.config, 85 let selectionPolicy = updatesService.selectionPolicy, 86 config.isEnabled else { 87 throw UpdatesDisabledException() 88 } 89 guard updatesService.isStarted else { 90 throw UpdatesNotInitializedException() 91 } 92 93 var extraHeaders: [String: Any] = [:] 94 updatesService.database.databaseQueue.sync { 95 extraHeaders = FileDownloader.extraHeadersForRemoteUpdateRequest( 96 withDatabase: updatesService.database, 97 config: config, 98 launchedUpdate: updatesService.launchedUpdate, 99 embeddedUpdate: updatesService.embeddedUpdate 100 ) 101 } 102 103 let fileDownloader = FileDownloader(config: config) 104 fileDownloader.downloadRemoteUpdate( 105 // swiftlint:disable:next force_unwrapping 106 fromURL: config.updateUrl!, 107 withDatabase: updatesService.database, 108 extraHeaders: extraHeaders 109 ) { updateResponse in 110 guard let update = updateResponse.manifestUpdateResponsePart?.updateManifest else { 111 promise.resolve([ 112 "isAvailable": false 113 ]) 114 return 115 } 116 117 let launchedUpdate = updatesService.launchedUpdate 118 if selectionPolicy.shouldLoadNewUpdate(update, withLaunchedUpdate: launchedUpdate, filters: updateResponse.responseHeaderData?.manifestFilters) { 119 promise.resolve([ 120 "isAvailable": true, 121 "manifest": update.manifest.rawManifestJSON() 122 ]) 123 } else { 124 promise.resolve([ 125 "isAvailable": false 126 ]) 127 } 128 } errorBlock: { error in 129 promise.reject("ERR_UPDATES_CHECK", error.localizedDescription) 130 } 131 } 132 133 AsyncFunction("getExtraClientParamsAsync") { (promise: Promise) in 134 guard let updatesService = updatesService, 135 let config = updatesService.config, 136 config.isEnabled else { 137 throw UpdatesDisabledException() 138 } 139 140 guard let scopeKey = config.scopeKey else { 141 throw Exception(name: "ERR_UPDATES_SCOPE_KEY", description: "Muse have scopeKey in config") 142 } 143 144 updatesService.database.databaseQueue.async { 145 do { 146 promise.resolve(try updatesService.database.extraParams(withScopeKey: scopeKey)) 147 } catch { 148 promise.reject(error) 149 } 150 } 151 } 152 153 AsyncFunction("setExtraParamAsync") { (key: String, value: String?, promise: Promise) in 154 guard let updatesService = updatesService, 155 let config = updatesService.config, 156 config.isEnabled else { 157 throw UpdatesDisabledException() 158 } 159 160 guard let scopeKey = config.scopeKey else { 161 throw Exception(name: "ERR_UPDATES_SCOPE_KEY", description: "Muse have scopeKey in config") 162 } 163 164 updatesService.database.databaseQueue.async { 165 do { 166 try updatesService.database.setExtraParam(key: key, value: value, withScopeKey: scopeKey) 167 promise.resolve(nil) 168 } catch { 169 promise.reject(error) 170 } 171 } 172 } 173 174 AsyncFunction("readLogEntriesAsync") { (maxAge: Int) -> [[String: Any]] in 175 // maxAge is in milliseconds, convert to seconds 176 do { 177 return try UpdatesLogReader().getLogEntries(newerThan: Date(timeIntervalSinceNow: TimeInterval(-1 * (maxAge / 1000)))) 178 } catch { 179 throw Exception(name: "ERR_UPDATES_READ_LOGS", description: error.localizedDescription) 180 } 181 } 182 183 AsyncFunction("clearLogEntriesAsync") { (promise: Promise) in 184 UpdatesLogReader().purgeLogEntries(olderThan: Date()) { error in 185 guard let error = error else { 186 promise.resolve(nil) 187 return 188 } 189 promise.reject("ERR_UPDATES_READ_LOGS", error.localizedDescription) 190 } 191 } 192 193 AsyncFunction("fetchUpdateAsync") { (promise: Promise) in 194 guard let updatesService = updatesService, 195 let config = updatesService.config, 196 let selectionPolicy = updatesService.selectionPolicy, 197 config.isEnabled else { 198 throw UpdatesDisabledException() 199 } 200 guard updatesService.isStarted else { 201 throw UpdatesNotInitializedException() 202 } 203 204 let remoteAppLoader = RemoteAppLoader( 205 config: config, 206 database: updatesService.database, 207 directory: updatesService.directory, 208 launchedUpdate: updatesService.launchedUpdate, 209 completionQueue: methodQueue 210 ) 211 remoteAppLoader.loadUpdate( 212 // swiftlint:disable:next force_unwrapping 213 fromURL: config.updateUrl! 214 ) { updateResponse in 215 if let updateDirective = updateResponse.directiveUpdateResponsePart?.updateDirective { 216 switch updateDirective { 217 case is NoUpdateAvailableUpdateDirective: 218 return false 219 case is RollBackToEmbeddedUpdateDirective: 220 return true 221 default: 222 NSException(name: .internalInconsistencyException, reason: "Unhandled update directive type").raise() 223 return false 224 } 225 } 226 227 guard let update = updateResponse.manifestUpdateResponsePart?.updateManifest else { 228 return false 229 } 230 231 return selectionPolicy.shouldLoadNewUpdate( 232 update, 233 withLaunchedUpdate: updatesService.launchedUpdate, 234 filters: updateResponse.responseHeaderData?.manifestFilters 235 ) 236 } asset: { _, _, _, _ in 237 // do nothing for now 238 } success: { updateResponse in 239 if updateResponse?.directiveUpdateResponsePart?.updateDirective is RollBackToEmbeddedUpdateDirective { 240 promise.resolve([ 241 "isRollBackToEmbedded": true 242 ]) 243 } else { 244 if let update = updateResponse?.manifestUpdateResponsePart?.updateManifest { 245 updatesService.resetSelectionPolicy() 246 promise.resolve([ 247 "isNew": true, 248 "manifest": update.manifest.rawManifestJSON() 249 ]) 250 } else { 251 promise.resolve([ 252 "isNew": false 253 ]) 254 } 255 } 256 } error: { error in 257 promise.reject("ERR_UPDATES_FETCH", "Failed to download new update: \(error.localizedDescription)") 258 } 259 } 260 } 261 } 262