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("reloadAsync") { (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.extraHeaders( 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("readLogEntriesAsync") { (maxAge: Int) -> [[String: Any]] in 134 // maxAge is in milliseconds, convert to seconds 135 do { 136 return try UpdatesLogReader().getLogEntries(newerThan: Date(timeIntervalSinceNow: TimeInterval(-1 * (maxAge / 1000)))) 137 } catch { 138 throw Exception(name: "ERR_UPDATES_READ_LOGS", description: error.localizedDescription) 139 } 140 } 141 142 AsyncFunction("clearLogEntriesAsync") { (promise: Promise) in 143 UpdatesLogReader().purgeLogEntries(olderThan: Date()) { error in 144 guard let error = error else { 145 promise.resolve(nil) 146 return 147 } 148 promise.reject("ERR_UPDATES_READ_LOGS", error.localizedDescription) 149 } 150 } 151 152 AsyncFunction("fetchUpdateAsync") { (promise: Promise) in 153 guard let updatesService = updatesService, 154 let config = updatesService.config, 155 let selectionPolicy = updatesService.selectionPolicy, 156 config.isEnabled else { 157 throw UpdatesDisabledException() 158 } 159 guard updatesService.isStarted else { 160 throw UpdatesNotInitializedException() 161 } 162 163 let remoteAppLoader = RemoteAppLoader( 164 config: config, 165 database: updatesService.database, 166 directory: updatesService.directory, 167 launchedUpdate: updatesService.launchedUpdate, 168 completionQueue: methodQueue 169 ) 170 remoteAppLoader.loadUpdate( 171 // swiftlint:disable:next force_unwrapping 172 fromURL: config.updateUrl! 173 ) { updateResponse in 174 if let updateDirective = updateResponse.directiveUpdateResponsePart?.updateDirective { 175 switch updateDirective { 176 case is NoUpdateAvailableUpdateDirective: 177 return false 178 case is RollBackToEmbeddedUpdateDirective: 179 return true 180 default: 181 NSException(name: .internalInconsistencyException, reason: "Unhandled update directive type").raise() 182 return false 183 } 184 } 185 186 guard let update = updateResponse.manifestUpdateResponsePart?.updateManifest else { 187 return false 188 } 189 190 return selectionPolicy.shouldLoadNewUpdate( 191 update, 192 withLaunchedUpdate: updatesService.launchedUpdate, 193 filters: updateResponse.responseHeaderData?.manifestFilters 194 ) 195 } asset: { _, _, _, _ in 196 // do nothing for now 197 } success: { updateResponse in 198 if updateResponse?.directiveUpdateResponsePart?.updateDirective is RollBackToEmbeddedUpdateDirective { 199 promise.resolve([ 200 "isRollBackToEmbedded": true 201 ]) 202 } else { 203 if let update = updateResponse?.manifestUpdateResponsePart?.updateManifest { 204 updatesService.resetSelectionPolicy() 205 promise.resolve([ 206 "isNew": true, 207 "manifest": update.manifest.rawManifestJSON() 208 ]) 209 } else { 210 promise.resolve([ 211 "isNew": false 212 ]) 213 } 214 } 215 } error: { error in 216 promise.reject("ERR_UPDATES_FETCH", "Failed to download new update: \(error.localizedDescription)") 217 } 218 } 219 } 220 } 221