@tokamakdev/platform-ios-simulator-x64 0.1.1-beta.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ios-simulator-x64/build/entrypoint +245 -0
- package/ios-simulator-x64/build/ios-deployment-target +1 -0
- package/ios-simulator-x64/build/macos-deployment-target +1 -0
- package/ios-simulator-x64/frameworks/TokamakRuntime.framework/Headers/TokamakRuntime.h +67 -0
- package/ios-simulator-x64/frameworks/TokamakRuntime.framework/Modules/module.modulemap +4 -0
- package/ios-simulator-x64/frameworks/TokamakRuntime.framework/TokamakRuntime +0 -0
- package/ios-simulator-x64/native-shell/TokamakApp.swift +569 -0
- package/ios-simulator-x64/native-shell/TokamakOrigin.swift +20 -0
- package/ios-simulator-x64/native-shell/TokamakPlugins.swift +309 -0
- package/ios-simulator-x64/native-shell/TokamakShell.swift +515 -0
- package/ios-simulator-x64/platform-pack.json +36 -0
- package/ios-simulator-x64/tools/tokamak-apple-signing +0 -0
- package/package.json +23 -0
|
@@ -0,0 +1,569 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import TokamakRuntime
|
|
3
|
+
|
|
4
|
+
#if os(iOS)
|
|
5
|
+
import UIKit
|
|
6
|
+
#endif
|
|
7
|
+
|
|
8
|
+
private let startupErrorFile = "startup-error.log"
|
|
9
|
+
/// How long the Worker has for `resume` and `suspend`.
|
|
10
|
+
private let lifecycleDeadline: TimeInterval = 10
|
|
11
|
+
#if os(iOS)
|
|
12
|
+
/// The system gives an app about 30 seconds after it leaves the foreground.
|
|
13
|
+
private let suspendDeadline: TimeInterval = 25
|
|
14
|
+
#endif
|
|
15
|
+
|
|
16
|
+
/// The tokamak runtime and native plugins of this process. Both outlive any
|
|
17
|
+
/// WebView, so plugins receive app events before a page loads and a Worker
|
|
18
|
+
/// event can run while no page is loaded.
|
|
19
|
+
final class TokamakApp {
|
|
20
|
+
/// Every plugin, by ID.
|
|
21
|
+
private(set) var plugins: [String: any TokamakPlugin] = [:]
|
|
22
|
+
private var runtime: Result<RuntimeHandle, Error>?
|
|
23
|
+
private var waiting: [(Result<RuntimeHandle, Error>) -> Void] = []
|
|
24
|
+
/// The Worker's calls and subscriptions on the plugins.
|
|
25
|
+
private lazy var workerRequests = TokamakPluginRequests<UInt64>(plugins: plugins) {
|
|
26
|
+
[unowned self] id, result in
|
|
27
|
+
whenStarted { started in
|
|
28
|
+
if case .success(let runtime) = started {
|
|
29
|
+
runtime.reply(id, result: result)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/// Delivers `resume` and `suspend` one at a time, in order.
|
|
34
|
+
private let lifecycle = DispatchQueue(label: "tokamak.lifecycle")
|
|
35
|
+
#if os(iOS)
|
|
36
|
+
/// The scenes in the foreground.
|
|
37
|
+
private var foregroundScenes = 0
|
|
38
|
+
#endif
|
|
39
|
+
|
|
40
|
+
/// Creates the plugins. Call it before launch finishes.
|
|
41
|
+
init() {
|
|
42
|
+
for plugin in tokamakPlugins(app: self) {
|
|
43
|
+
precondition(plugins.updateValue(plugin, forKey: plugin.id) == nil, "Duplicate plugin ID")
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/// Starts the runtime in the background, which delivers `start` with
|
|
48
|
+
/// `foreground`. Call it once, on the main thread.
|
|
49
|
+
func start(foreground: Bool) {
|
|
50
|
+
dispatchPrecondition(condition: .onQueue(.main))
|
|
51
|
+
let plugins = workerPlugins
|
|
52
|
+
DispatchQueue.global(qos: .userInitiated).async {
|
|
53
|
+
let result = Result { try RuntimeHandle(foreground: foreground, plugins: plugins) }
|
|
54
|
+
DispatchQueue.main.async { self.started(result) }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/// The plugins as the runtime calls them, on its own threads. The app
|
|
59
|
+
/// outlives the runtime, which holds it unretained.
|
|
60
|
+
private var workerPlugins: TokamakPluginHandler {
|
|
61
|
+
TokamakPluginHandler(
|
|
62
|
+
context: Unmanaged.passUnretained(self).toOpaque(),
|
|
63
|
+
call: { context, id, plugin, method, arguments in
|
|
64
|
+
TokamakApp.receive(context, id, subscribe: false, plugin, method, arguments)
|
|
65
|
+
},
|
|
66
|
+
subscribe: { context, id, plugin, method, arguments in
|
|
67
|
+
TokamakApp.receive(context, id, subscribe: true, plugin, method, arguments)
|
|
68
|
+
},
|
|
69
|
+
unsubscribe: { context, id in
|
|
70
|
+
let app = Unmanaged<TokamakApp>.fromOpaque(context!).takeUnretainedValue()
|
|
71
|
+
DispatchQueue.main.async { app.workerRequests.cancel(id) }
|
|
72
|
+
}
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/// Runs a call or subscription the Worker made through `context`'s app.
|
|
77
|
+
private static func receive(
|
|
78
|
+
_ context: UnsafeMutableRawPointer?,
|
|
79
|
+
_ id: UInt64,
|
|
80
|
+
subscribe: Bool,
|
|
81
|
+
_ plugin: UnsafePointer<CChar>?,
|
|
82
|
+
_ method: UnsafePointer<CChar>?,
|
|
83
|
+
_ arguments: UnsafePointer<CChar>?
|
|
84
|
+
) {
|
|
85
|
+
let app = Unmanaged<TokamakApp>.fromOpaque(context!).takeUnretainedValue()
|
|
86
|
+
let (plugin, method) = (String(cString: plugin!), String(cString: method!))
|
|
87
|
+
let arguments =
|
|
88
|
+
(try? JSONSerialization.jsonObject(
|
|
89
|
+
with: Data(String(cString: arguments!).utf8), options: .fragmentsAllowed)) ?? NSNull()
|
|
90
|
+
DispatchQueue.main.async {
|
|
91
|
+
app.workerRequests.run(
|
|
92
|
+
id, subscribe: subscribe, plugin: plugin, method: method, arguments: arguments)
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/// Calls `completion` on the main thread once the runtime has started or
|
|
97
|
+
/// failed to start.
|
|
98
|
+
func whenStarted(_ completion: @escaping (Result<RuntimeHandle, Error>) -> Void) {
|
|
99
|
+
dispatchPrecondition(condition: .onQueue(.main))
|
|
100
|
+
if let runtime {
|
|
101
|
+
completion(runtime)
|
|
102
|
+
} else {
|
|
103
|
+
waiting.append(completion)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Delivers the event `name` with `event`, JSON-serialisable, to the
|
|
108
|
+
/// Worker's listeners on `queue`, and calls `completion` on the main thread
|
|
109
|
+
/// with their reply, parsed from JSON, or nil. Fails unless they return
|
|
110
|
+
/// within `timeout`, which includes runtime startup. Call it on the main
|
|
111
|
+
/// thread.
|
|
112
|
+
func emit(
|
|
113
|
+
_ name: String,
|
|
114
|
+
event: Any,
|
|
115
|
+
timeout: TimeInterval,
|
|
116
|
+
on queue: DispatchQueue = .global(qos: .userInitiated),
|
|
117
|
+
completion: @escaping (Result<Any?, Error>) -> Void
|
|
118
|
+
) {
|
|
119
|
+
let deadline = Date() + timeout
|
|
120
|
+
withRuntime(on: queue, completion: completion) { runtime in
|
|
121
|
+
try runtime.emit(name, event: event, timeout: deadline.timeIntervalSinceNow)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Fetches what the app serves at `path` and calls `completion` on the main
|
|
126
|
+
/// thread with the body. Fails unless it arrives within `timeout`, which
|
|
127
|
+
/// includes runtime startup. Call it on the main thread.
|
|
128
|
+
func fetch(
|
|
129
|
+
_ path: String,
|
|
130
|
+
timeout: TimeInterval,
|
|
131
|
+
completion: @escaping (Result<Data, Error>) -> Void
|
|
132
|
+
) {
|
|
133
|
+
let deadline = Date() + timeout
|
|
134
|
+
withRuntime(on: .global(qos: .userInitiated), completion: completion) { runtime in
|
|
135
|
+
try runtime.fetch(path, timeout: deadline.timeIntervalSinceNow)
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/// Runs `work` with the started runtime on `queue`, and calls `completion`
|
|
140
|
+
/// on the main thread with its result.
|
|
141
|
+
private func withRuntime<Value>(
|
|
142
|
+
on queue: DispatchQueue,
|
|
143
|
+
completion: @escaping (Result<Value, Error>) -> Void,
|
|
144
|
+
work: @escaping (RuntimeHandle) throws -> Value
|
|
145
|
+
) {
|
|
146
|
+
whenStarted { result in
|
|
147
|
+
switch result {
|
|
148
|
+
case .failure(let error):
|
|
149
|
+
completion(.failure(error))
|
|
150
|
+
case .success(let runtime):
|
|
151
|
+
queue.async {
|
|
152
|
+
let outcome = Result { try work(runtime) }
|
|
153
|
+
DispatchQueue.main.async { completion(outcome) }
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/// Records the app moving into or out of the foreground once the runtime
|
|
160
|
+
/// has started, and delivers `resume` or `suspend` when that changed its
|
|
161
|
+
/// stage. Call it on the main thread.
|
|
162
|
+
func moved(toForeground foreground: Bool) {
|
|
163
|
+
let name = foreground ? "resume" : "suspend"
|
|
164
|
+
#if os(iOS)
|
|
165
|
+
// The system suspends the app soon after it leaves the foreground
|
|
166
|
+
// unless a background task holds it there.
|
|
167
|
+
var task = UIBackgroundTaskIdentifier.invalid
|
|
168
|
+
let finish = {
|
|
169
|
+
guard task != .invalid else { return }
|
|
170
|
+
UIApplication.shared.endBackgroundTask(task)
|
|
171
|
+
task = .invalid
|
|
172
|
+
}
|
|
173
|
+
if !foreground {
|
|
174
|
+
task = UIApplication.shared.beginBackgroundTask(
|
|
175
|
+
withName: "tokamak suspend", expirationHandler: finish)
|
|
176
|
+
}
|
|
177
|
+
let timeout = foreground ? lifecycleDeadline : suspendDeadline
|
|
178
|
+
#else
|
|
179
|
+
let finish = {}
|
|
180
|
+
let timeout = lifecycleDeadline
|
|
181
|
+
#endif
|
|
182
|
+
whenStarted { started in
|
|
183
|
+
guard case .success(let runtime) = started, runtime.setForeground(foreground) else {
|
|
184
|
+
finish()
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
self.emit(name, event: [String: Any](), timeout: timeout, on: self.lifecycle) { result in
|
|
188
|
+
if case .failure(let error) = result {
|
|
189
|
+
print("tokamak \(name) failed: \(error)")
|
|
190
|
+
}
|
|
191
|
+
finish()
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
#if os(iOS)
|
|
197
|
+
/// Delivers `resume` as the first scene enters the foreground.
|
|
198
|
+
func sceneWillEnterForeground() {
|
|
199
|
+
foregroundScenes += 1
|
|
200
|
+
if foregroundScenes == 1 {
|
|
201
|
+
moved(toForeground: true)
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/// Delivers `suspend` as the last scene leaves the foreground.
|
|
206
|
+
func sceneDidEnterBackground() {
|
|
207
|
+
foregroundScenes -= 1
|
|
208
|
+
if foregroundScenes == 0 {
|
|
209
|
+
moved(toForeground: false)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/// Whether a scene is in the foreground.
|
|
214
|
+
var hasForegroundScene: Bool {
|
|
215
|
+
foregroundScenes > 0
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/// Whether the app is in the foreground, as the runtime records it, or
|
|
219
|
+
/// until the runtime has started, whether a scene is in the foreground.
|
|
220
|
+
/// Read it on the main thread.
|
|
221
|
+
var isInForeground: Bool {
|
|
222
|
+
guard case .success(let runtime) = runtime else { return hasForegroundScene }
|
|
223
|
+
return runtime.isForeground
|
|
224
|
+
}
|
|
225
|
+
#endif
|
|
226
|
+
|
|
227
|
+
func didRegisterForRemoteNotifications(deviceToken: Data) {
|
|
228
|
+
for plugin in plugins.values {
|
|
229
|
+
plugin.didRegisterForRemoteNotifications(deviceToken: deviceToken)
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
func didFailToRegisterForRemoteNotifications(error: Error) {
|
|
234
|
+
for plugin in plugins.values {
|
|
235
|
+
plugin.didFailToRegisterForRemoteNotifications(error: error)
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/// Delivers a remote notification to every plugin, calling `completion` on
|
|
240
|
+
/// the main thread once each has finished with it.
|
|
241
|
+
func didReceiveRemoteNotification(
|
|
242
|
+
_ userInfo: [AnyHashable: Any],
|
|
243
|
+
completion: @escaping (TokamakBackgroundResult) -> Void
|
|
244
|
+
) {
|
|
245
|
+
let group = DispatchGroup()
|
|
246
|
+
var results: [TokamakBackgroundResult] = []
|
|
247
|
+
for plugin in plugins.values {
|
|
248
|
+
group.enter()
|
|
249
|
+
plugin.didReceiveRemoteNotification(userInfo) { result in
|
|
250
|
+
DispatchQueue.main.async {
|
|
251
|
+
results.append(result)
|
|
252
|
+
group.leave()
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
group.notify(queue: .main) {
|
|
257
|
+
completion(TokamakBackgroundResult(combining: results))
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private func started(_ result: Result<RuntimeHandle, Error>) {
|
|
262
|
+
if case .failure(let error) = result {
|
|
263
|
+
RuntimeHandle.recordStartupFailure(error)
|
|
264
|
+
}
|
|
265
|
+
runtime = result
|
|
266
|
+
let callbacks = waiting
|
|
267
|
+
waiting.removeAll()
|
|
268
|
+
for callback in callbacks {
|
|
269
|
+
callback(result)
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/// What a plugin did with a background event, reported to the system.
|
|
275
|
+
enum TokamakBackgroundResult {
|
|
276
|
+
case newData
|
|
277
|
+
case noData
|
|
278
|
+
case failed
|
|
279
|
+
|
|
280
|
+
/// New data when any plugin fetched some, otherwise a failure when any
|
|
281
|
+
/// plugin failed.
|
|
282
|
+
init(combining results: [Self]) {
|
|
283
|
+
if results.contains(.newData) {
|
|
284
|
+
self = .newData
|
|
285
|
+
} else if results.contains(.failed) {
|
|
286
|
+
self = .failed
|
|
287
|
+
} else {
|
|
288
|
+
self = .noData
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
enum RuntimeError: Error, CustomStringConvertible {
|
|
294
|
+
case configuration(String)
|
|
295
|
+
case runtime(String)
|
|
296
|
+
|
|
297
|
+
var description: String {
|
|
298
|
+
switch self {
|
|
299
|
+
case .configuration(let message), .runtime(let message):
|
|
300
|
+
return message
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
enum AuthenticationMaterial<Value> {
|
|
306
|
+
case defaultHandling
|
|
307
|
+
case cancel
|
|
308
|
+
case use(Value)
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/// A started tokamak runtime, stopped when released.
|
|
312
|
+
final class RuntimeHandle {
|
|
313
|
+
let appHost: String
|
|
314
|
+
private var handle: UnsafeMutableRawPointer?
|
|
315
|
+
|
|
316
|
+
/// Starts the runtime, which delivers `start` with `foreground`, and whose
|
|
317
|
+
/// Worker calls `plugins`.
|
|
318
|
+
init(foreground: Bool, plugins: TokamakPluginHandler) throws {
|
|
319
|
+
guard
|
|
320
|
+
let appHost = Bundle.main.object(forInfoDictionaryKey: "TokamakHost") as? String,
|
|
321
|
+
!appHost.isEmpty
|
|
322
|
+
else {
|
|
323
|
+
throw RuntimeError.configuration("TokamakHost is required")
|
|
324
|
+
}
|
|
325
|
+
self.appHost = appHost
|
|
326
|
+
|
|
327
|
+
let state = try Self.stateDirectory()
|
|
328
|
+
var error = [CChar](repeating: 0, count: 512)
|
|
329
|
+
if let endpoint = Bundle.main.object(forInfoDictionaryKey: "TokamakDevEndpoint") as? String,
|
|
330
|
+
let sessionToken = Bundle.main.object(forInfoDictionaryKey: "TokamakDevSessionToken")
|
|
331
|
+
as? String
|
|
332
|
+
{
|
|
333
|
+
handle = state.path.withCString { statePath in
|
|
334
|
+
appHost.withCString { appHost in
|
|
335
|
+
endpoint.withCString { endpoint in
|
|
336
|
+
sessionToken.withCString { sessionToken in
|
|
337
|
+
error.withUnsafeMutableBufferPointer { error in
|
|
338
|
+
tokamak_runtime_start_development(
|
|
339
|
+
statePath,
|
|
340
|
+
appHost,
|
|
341
|
+
endpoint,
|
|
342
|
+
sessionToken,
|
|
343
|
+
foreground,
|
|
344
|
+
plugins,
|
|
345
|
+
error.baseAddress,
|
|
346
|
+
error.count
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
} else {
|
|
354
|
+
let bundle = Bundle.main.resourceURL?.appendingPathComponent("app")
|
|
355
|
+
guard let bundle else {
|
|
356
|
+
throw RuntimeError.configuration("app bundle resources are unavailable")
|
|
357
|
+
}
|
|
358
|
+
let storage = try Self.storageDirectory()
|
|
359
|
+
handle = bundle.path.withCString { bundlePath in
|
|
360
|
+
state.path.withCString { statePath in
|
|
361
|
+
storage.path.withCString { storagePath in
|
|
362
|
+
appHost.withCString { appHost in
|
|
363
|
+
error.withUnsafeMutableBufferPointer { error in
|
|
364
|
+
tokamak_runtime_start(
|
|
365
|
+
bundlePath,
|
|
366
|
+
statePath,
|
|
367
|
+
storagePath,
|
|
368
|
+
appHost,
|
|
369
|
+
foreground,
|
|
370
|
+
plugins,
|
|
371
|
+
error.baseAddress,
|
|
372
|
+
error.count
|
|
373
|
+
)
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
guard handle != nil else {
|
|
381
|
+
throw RuntimeError.runtime(String(cString: error))
|
|
382
|
+
}
|
|
383
|
+
try? FileManager.default.removeItem(at: state.appendingPathComponent(startupErrorFile))
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
deinit {
|
|
387
|
+
if let handle {
|
|
388
|
+
tokamak_runtime_stop(handle)
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
var port: UInt16 {
|
|
393
|
+
tokamak_runtime_port(handle)
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
func restoreGateway() throws -> UInt16 {
|
|
397
|
+
var error = [CChar](repeating: 0, count: 512)
|
|
398
|
+
let port = error.withUnsafeMutableBufferPointer { error in
|
|
399
|
+
tokamak_runtime_restore_gateway(handle, error.baseAddress, error.count)
|
|
400
|
+
}
|
|
401
|
+
guard port != 0 else {
|
|
402
|
+
throw RuntimeError.runtime(String(cString: error))
|
|
403
|
+
}
|
|
404
|
+
return port
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/// Delivers the event `name` with `event`, JSON-serialisable, to the
|
|
408
|
+
/// Worker's listeners, blocking until they return or `timeout` passes, and
|
|
409
|
+
/// returns their reply, parsed from JSON, or nil.
|
|
410
|
+
func emit(_ name: String, event: Any, timeout: TimeInterval) throws -> Any? {
|
|
411
|
+
guard JSONSerialization.isValidJSONObject([event]) else {
|
|
412
|
+
throw RuntimeError.runtime("the \(name) event is not JSON")
|
|
413
|
+
}
|
|
414
|
+
let event = String(
|
|
415
|
+
decoding: try JSONSerialization.data(withJSONObject: event, options: .fragmentsAllowed),
|
|
416
|
+
as: UTF8.self
|
|
417
|
+
)
|
|
418
|
+
var reply = TokamakBytes()
|
|
419
|
+
var error = [CChar](repeating: 0, count: 1024)
|
|
420
|
+
let succeeded = name.withCString { name in
|
|
421
|
+
event.withCString { event in
|
|
422
|
+
error.withUnsafeMutableBufferPointer { error in
|
|
423
|
+
tokamak_runtime_emit(
|
|
424
|
+
handle,
|
|
425
|
+
name,
|
|
426
|
+
event,
|
|
427
|
+
UInt64(max(timeout, 0) * 1000),
|
|
428
|
+
&reply,
|
|
429
|
+
error.baseAddress,
|
|
430
|
+
error.count
|
|
431
|
+
)
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
defer { tokamak_bytes_free(reply) }
|
|
436
|
+
guard succeeded, let json = reply.contents else {
|
|
437
|
+
throw RuntimeError.runtime(String(cString: error))
|
|
438
|
+
}
|
|
439
|
+
let value = try JSONSerialization.jsonObject(with: json, options: .fragmentsAllowed)
|
|
440
|
+
return value is NSNull ? nil : value
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
/// What the app serves at `path`, blocking until it arrives or `timeout`
|
|
444
|
+
/// passes.
|
|
445
|
+
func fetch(_ path: String, timeout: TimeInterval) throws -> Data {
|
|
446
|
+
var body = TokamakBytes()
|
|
447
|
+
var error = [CChar](repeating: 0, count: 1024)
|
|
448
|
+
let succeeded = path.withCString { path in
|
|
449
|
+
error.withUnsafeMutableBufferPointer { error in
|
|
450
|
+
tokamak_runtime_fetch(
|
|
451
|
+
handle,
|
|
452
|
+
path,
|
|
453
|
+
UInt64(max(timeout, 0) * 1000),
|
|
454
|
+
&body,
|
|
455
|
+
error.baseAddress,
|
|
456
|
+
error.count
|
|
457
|
+
)
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
defer { tokamak_bytes_free(body) }
|
|
461
|
+
guard succeeded else {
|
|
462
|
+
throw RuntimeError.runtime(String(cString: error))
|
|
463
|
+
}
|
|
464
|
+
return body.contents ?? Data()
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/// Records whether the app is in the foreground, which the Worker's
|
|
468
|
+
/// `getLifecycleStage()` reports from then on, and returns whether that
|
|
469
|
+
/// changed.
|
|
470
|
+
func setForeground(_ foreground: Bool) -> Bool {
|
|
471
|
+
tokamak_runtime_set_foreground(handle, foreground)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/// Whether the app is in the foreground, as last recorded.
|
|
475
|
+
var isForeground: Bool {
|
|
476
|
+
tokamak_runtime_is_foreground(handle)
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/// Passes a plugin's `result`, JSON-serialisable, to the Worker's call or
|
|
480
|
+
/// subscription `id`.
|
|
481
|
+
func reply(_ id: UInt64, result: [String: Any]) {
|
|
482
|
+
guard let json = try? JSONSerialization.data(withJSONObject: result) else { return }
|
|
483
|
+
String(decoding: json, as: UTF8.self).withCString { tokamak_runtime_reply(handle, id, $0) }
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
func serverAuthority(host: String) -> AuthenticationMaterial<Data> {
|
|
487
|
+
var bytes = TokamakBytes()
|
|
488
|
+
let decision = host.withCString {
|
|
489
|
+
tokamak_runtime_server_authority(handle, $0, &bytes)
|
|
490
|
+
}
|
|
491
|
+
switch decision {
|
|
492
|
+
case Int32(TOKAMAK_DECISION_DEFAULT):
|
|
493
|
+
return .defaultHandling
|
|
494
|
+
case Int32(TOKAMAK_DECISION_USE):
|
|
495
|
+
defer { tokamak_bytes_free(bytes) }
|
|
496
|
+
guard let authority = bytes.contents else { return .cancel }
|
|
497
|
+
return .use(authority)
|
|
498
|
+
default:
|
|
499
|
+
return .cancel
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
func clientIdentity(host: String, previousFailures: Int) -> AuthenticationMaterial<(Data, Data)> {
|
|
504
|
+
var identity = TokamakIdentity()
|
|
505
|
+
let decision = host.withCString {
|
|
506
|
+
tokamak_runtime_client_identity(handle, $0, max(previousFailures, 0), &identity)
|
|
507
|
+
}
|
|
508
|
+
switch decision {
|
|
509
|
+
case Int32(TOKAMAK_DECISION_DEFAULT):
|
|
510
|
+
return .defaultHandling
|
|
511
|
+
case Int32(TOKAMAK_DECISION_USE):
|
|
512
|
+
defer { tokamak_identity_free(identity) }
|
|
513
|
+
guard
|
|
514
|
+
let certificate = identity.certificate.contents,
|
|
515
|
+
let privateKey = identity.private_key.contents
|
|
516
|
+
else {
|
|
517
|
+
return .cancel
|
|
518
|
+
}
|
|
519
|
+
return .use((certificate, privateKey))
|
|
520
|
+
default:
|
|
521
|
+
return .cancel
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
static func recordStartupFailure(_ error: Error) {
|
|
526
|
+
print("tokamak runtime startup failed: \(error)")
|
|
527
|
+
guard let state = try? stateDirectory() else { return }
|
|
528
|
+
try? String(describing: error).write(
|
|
529
|
+
to: state.appendingPathComponent(startupErrorFile),
|
|
530
|
+
atomically: true,
|
|
531
|
+
encoding: .utf8
|
|
532
|
+
)
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
/// Stores behind storage bindings, which device backups include.
|
|
536
|
+
private static func storageDirectory() throws -> URL {
|
|
537
|
+
let storage = try appDirectory(.applicationSupportDirectory)
|
|
538
|
+
.appendingPathComponent("tokamak/storage", isDirectory: true)
|
|
539
|
+
try FileManager.default.createDirectory(at: storage, withIntermediateDirectories: true)
|
|
540
|
+
return storage
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
private static func stateDirectory() throws -> URL {
|
|
544
|
+
let state = try appDirectory(.cachesDirectory)
|
|
545
|
+
try FileManager.default.createDirectory(at: state, withIntermediateDirectories: true)
|
|
546
|
+
return state
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/// This app's folder in the user's `directory`.
|
|
550
|
+
private static func appDirectory(_ directory: FileManager.SearchPathDirectory) throws -> URL {
|
|
551
|
+
guard let identifier = Bundle.main.bundleIdentifier else {
|
|
552
|
+
throw RuntimeError.configuration("CFBundleIdentifier is required")
|
|
553
|
+
}
|
|
554
|
+
return try FileManager.default.url(
|
|
555
|
+
for: directory,
|
|
556
|
+
in: .userDomainMask,
|
|
557
|
+
appropriateFor: nil,
|
|
558
|
+
create: true
|
|
559
|
+
)
|
|
560
|
+
.appendingPathComponent(identifier, isDirectory: true)
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
extension TokamakBytes {
|
|
565
|
+
/// A copy of the buffer, or nil when it is null.
|
|
566
|
+
fileprivate var contents: Data? {
|
|
567
|
+
data.map { Data(bytes: $0, count: len) }
|
|
568
|
+
}
|
|
569
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import WebKit
|
|
3
|
+
|
|
4
|
+
extension URL {
|
|
5
|
+
/// Whether this URL is on the app origin, `https://<host>`.
|
|
6
|
+
func isAppOrigin(_ host: String) -> Bool {
|
|
7
|
+
scheme?.caseInsensitiveCompare("https") == .orderedSame
|
|
8
|
+
&& self.host?.caseInsensitiveCompare(host) == .orderedSame
|
|
9
|
+
&& (port == nil || port == 443)
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
extension WKSecurityOrigin {
|
|
14
|
+
/// Whether this is the app origin, `https://<host>`.
|
|
15
|
+
func isAppOrigin(_ host: String) -> Bool {
|
|
16
|
+
self.protocol.caseInsensitiveCompare("https") == .orderedSame
|
|
17
|
+
&& self.host.caseInsensitiveCompare(host) == .orderedSame
|
|
18
|
+
&& (port == 0 || port == 443)
|
|
19
|
+
}
|
|
20
|
+
}
|