1 // Copyright 2019 650 Industries. All rights reserved. 2 3 // swiftlint:disable closure_body_length 4 // swiftlint:disable function_body_length 5 // swiftlint:disable type_body_length 6 7 import ExpoModulesCore 8 9 /** 10 * Exported module which provides to the JS runtime information about the currently running update 11 * and updates state, along with methods to check for and download new updates, reload with the 12 * newest downloaded update applied, and read/clear native log entries. 13 * 14 * Communicates with the updates hub (AppController in most apps, EXAppLoaderExpoUpdates in 15 * Expo Go and legacy standalone apps) via EXUpdatesService, an internal module which is overridden 16 * by EXUpdatesBinding, a scoped module, in Expo Go. 17 */ 18 public final class UpdatesModule: Module { 19 private let updatesService: EXUpdatesModuleInterface? 20 private let methodQueue = DispatchQueue(label: "expo.modules.EXUpdatesQueue") 21 22 public required init(appContext: AppContext) { 23 updatesService = appContext.legacyModule(implementing: EXUpdatesModuleInterface.self) 24 super.init(appContext: appContext) 25 } 26 27 // swiftlint:disable cyclomatic_complexity 28 public func definition() -> ModuleDefinition { 29 Name("ExpoUpdates") 30 31 Constants { 32 let releaseChannel = updatesService?.config?.releaseChannel 33 let channel = updatesService?.config?.requestHeaders["expo-channel-name"] ?? "" 34 let runtimeVersion = updatesService?.config?.runtimeVersion ?? "" 35 let checkAutomatically = updatesService?.config?.checkOnLaunch.asString ?? CheckAutomaticallyConfig.Always.asString 36 let isMissingRuntimeVersion = updatesService?.config?.isMissingRuntimeVersion() 37 38 guard let updatesService = updatesService, 39 updatesService.isStarted, 40 let launchedUpdate = updatesService.launchedUpdate else { 41 return [ 42 "isEnabled": false, 43 "isEmbeddedLaunch": false, 44 "isMissingRuntimeVersion": isMissingRuntimeVersion, 45 "releaseChannel": releaseChannel, 46 "runtimeVersion": runtimeVersion, 47 "checkAutomatically": checkAutomatically, 48 "channel": channel 49 ] 50 } 51 52 let commitTime = UInt64(floor(launchedUpdate.commitTime.timeIntervalSince1970 * 1000)) 53 return [ 54 "isEnabled": true, 55 "isEmbeddedLaunch": updatesService.isEmbeddedLaunch, 56 "isUsingEmbeddedAssets": updatesService.isUsingEmbeddedAssets, 57 "updateId": launchedUpdate.updateId.uuidString, 58 "manifest": launchedUpdate.manifest.rawManifestJSON(), 59 "localAssets": updatesService.assetFilesMap ?? [:], 60 "isEmergencyLaunch": updatesService.isEmergencyLaunch, 61 "isMissingRuntimeVersion": isMissingRuntimeVersion, 62 "releaseChannel": releaseChannel, 63 "runtimeVersion": runtimeVersion, 64 "checkAutomatically": checkAutomatically, 65 "channel": channel, 66 "commitTime": commitTime, 67 "nativeDebug": UpdatesUtils.isNativeDebuggingEnabled() 68 ] 69 } 70 71 AsyncFunction("reload") { (promise: Promise) in 72 guard let updatesService = updatesService, let config = updatesService.config, config.isEnabled else { 73 throw UpdatesDisabledException() 74 } 75 guard updatesService.canRelaunch else { 76 throw UpdatesNotInitializedException() 77 } 78 updatesService.requestRelaunch { success in 79 if success { 80 promise.resolve(nil) 81 } else { 82 promise.reject(UpdatesReloadException()) 83 } 84 } 85 } 86 87 AsyncFunction("checkForUpdateAsync") { (promise: Promise) in 88 guard let updatesService = updatesService, 89 let config = updatesService.config, 90 let selectionPolicy = updatesService.selectionPolicy, 91 config.isEnabled else { 92 throw UpdatesDisabledException() 93 } 94 guard updatesService.isStarted else { 95 throw UpdatesNotInitializedException() 96 } 97 98 var extraHeaders: [String: Any] = [:] 99 updatesService.database.databaseQueue.sync { 100 extraHeaders = FileDownloader.extraHeadersForRemoteUpdateRequest( 101 withDatabase: updatesService.database, 102 config: config, 103 launchedUpdate: updatesService.launchedUpdate, 104 embeddedUpdate: updatesService.embeddedUpdate 105 ) 106 } 107 108 let fileDownloader = FileDownloader(config: config) 109 fileDownloader.downloadRemoteUpdate( 110 // swiftlint:disable:next force_unwrapping 111 fromURL: config.updateUrl!, 112 withDatabase: updatesService.database, 113 extraHeaders: extraHeaders 114 ) { updateResponse in 115 guard let update = updateResponse.manifestUpdateResponsePart?.updateManifest else { 116 promise.resolve([ 117 "isAvailable": false 118 ]) 119 return 120 } 121 122 let launchedUpdate = updatesService.launchedUpdate 123 if selectionPolicy.shouldLoadNewUpdate(update, withLaunchedUpdate: launchedUpdate, filters: updateResponse.responseHeaderData?.manifestFilters) { 124 promise.resolve([ 125 "isAvailable": true, 126 "manifest": update.manifest.rawManifestJSON() 127 ]) 128 } else { 129 promise.resolve([ 130 "isAvailable": false 131 ]) 132 } 133 } errorBlock: { error in 134 promise.reject("ERR_UPDATES_CHECK", error.localizedDescription) 135 } 136 } 137 138 AsyncFunction("getExtraClientParamsAsync") { (promise: Promise) in 139 guard let updatesService = updatesService, 140 let config = updatesService.config, 141 config.isEnabled else { 142 throw UpdatesDisabledException() 143 } 144 145 guard let scopeKey = config.scopeKey else { 146 throw Exception(name: "ERR_UPDATES_SCOPE_KEY", description: "Muse have scopeKey in config") 147 } 148 149 updatesService.database.databaseQueue.async { 150 do { 151 promise.resolve(try updatesService.database.extraParams(withScopeKey: scopeKey)) 152 } catch { 153 promise.reject(error) 154 } 155 } 156 } 157 158 AsyncFunction("setExtraParamAsync") { (key: String, value: String?, promise: Promise) in 159 guard let updatesService = updatesService, 160 let config = updatesService.config, 161 config.isEnabled else { 162 throw UpdatesDisabledException() 163 } 164 165 guard let scopeKey = config.scopeKey else { 166 throw Exception(name: "ERR_UPDATES_SCOPE_KEY", description: "Muse have scopeKey in config") 167 } 168 169 updatesService.database.databaseQueue.async { 170 do { 171 try updatesService.database.setExtraParam(key: key, value: value, withScopeKey: scopeKey) 172 promise.resolve(nil) 173 } catch { 174 promise.reject(error) 175 } 176 } 177 } 178 179 AsyncFunction("readLogEntriesAsync") { (maxAge: Int) -> [[String: Any]] in 180 // maxAge is in milliseconds, convert to seconds 181 do { 182 return try UpdatesLogReader().getLogEntries(newerThan: Date(timeIntervalSinceNow: TimeInterval(-1 * (maxAge / 1000)))) 183 } catch { 184 throw Exception(name: "ERR_UPDATES_READ_LOGS", description: error.localizedDescription) 185 } 186 } 187 188 AsyncFunction("clearLogEntriesAsync") { (promise: Promise) in 189 UpdatesLogReader().purgeLogEntries(olderThan: Date()) { error in 190 guard let error = error else { 191 promise.resolve(nil) 192 return 193 } 194 promise.reject("ERR_UPDATES_READ_LOGS", error.localizedDescription) 195 } 196 } 197 198 AsyncFunction("fetchUpdateAsync") { (promise: Promise) in 199 guard let updatesService = updatesService, 200 let config = updatesService.config, 201 let selectionPolicy = updatesService.selectionPolicy, 202 config.isEnabled else { 203 throw UpdatesDisabledException() 204 } 205 guard updatesService.isStarted else { 206 throw UpdatesNotInitializedException() 207 } 208 209 let remoteAppLoader = RemoteAppLoader( 210 config: config, 211 database: updatesService.database, 212 directory: updatesService.directory, 213 launchedUpdate: updatesService.launchedUpdate, 214 completionQueue: methodQueue 215 ) 216 remoteAppLoader.loadUpdate( 217 // swiftlint:disable:next force_unwrapping 218 fromURL: config.updateUrl! 219 ) { updateResponse in 220 if let updateDirective = updateResponse.directiveUpdateResponsePart?.updateDirective { 221 switch updateDirective { 222 case is NoUpdateAvailableUpdateDirective: 223 return false 224 case is RollBackToEmbeddedUpdateDirective: 225 return true 226 default: 227 NSException(name: .internalInconsistencyException, reason: "Unhandled update directive type").raise() 228 return false 229 } 230 } 231 232 guard let update = updateResponse.manifestUpdateResponsePart?.updateManifest else { 233 return false 234 } 235 236 return selectionPolicy.shouldLoadNewUpdate( 237 update, 238 withLaunchedUpdate: updatesService.launchedUpdate, 239 filters: updateResponse.responseHeaderData?.manifestFilters 240 ) 241 } asset: { _, _, _, _ in 242 // do nothing for now 243 } success: { updateResponse in 244 if updateResponse?.directiveUpdateResponsePart?.updateDirective is RollBackToEmbeddedUpdateDirective { 245 promise.resolve([ 246 "isRollBackToEmbedded": true 247 ]) 248 } else { 249 if let update = updateResponse?.manifestUpdateResponsePart?.updateManifest { 250 updatesService.resetSelectionPolicy() 251 promise.resolve([ 252 "isNew": true, 253 "manifest": update.manifest.rawManifestJSON() 254 ]) 255 } else { 256 promise.resolve([ 257 "isNew": false 258 ]) 259 } 260 } 261 } error: { error in 262 promise.reject("ERR_UPDATES_FETCH", "Failed to download new update: \(error.localizedDescription)") 263 } 264 } 265 } 266 } 267