@tokamakdev/platform-ios-simulator-arm64 0.1.1-beta.49 → 0.1.1-beta.50

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.
@@ -89,33 +89,32 @@ sign_bundle() {
89
89
  signing_args+=(--device-id "$(cat "$metadata/device-id")")
90
90
  fi
91
91
  "$apple_tool" "${signing_args[@]}"
92
- elif [[ "$platform" == macos && -n "${TOKAMAK_MACOS_TEAM_ID:-}" ]]; then
92
+ elif [[ "$platform" == macos ]]; then
93
93
  "$apple_tool" sign \
94
94
  --platform macos \
95
95
  --project "$project_dir" \
96
96
  --bundle "$bundle" \
97
97
  --bundle-id "$identifier"
98
98
  else
99
- # Ad-hoc signatures carry no provisioning profile.
100
- rm -f "$contents/embedded.provisionprofile"
101
99
  codesign --force --timestamp=none --sign - "$bundle"
102
100
  fi
103
101
  }
104
102
 
105
103
  bundle_key_file=$output_parent/.tokamak/$(basename "$output").input-key
106
104
  if [[ -f "$metadata/input-key" ]]; then
107
- plist_path=
108
105
  case "$platform" in
109
- ios|ios-simulator) plist_path=${TOKAMAK_IOS_PLIST:-} ;;
110
- macos) plist_path=${TOKAMAK_MACOS_PLIST:-} ;;
106
+ ios|ios-simulator) app_files=("${TOKAMAK_IOS_PLIST:-}" "${TOKAMAK_IOS_ENTITLEMENTS:-}") ;;
107
+ macos) app_files=("${TOKAMAK_MACOS_PLIST:-}" "${TOKAMAK_MACOS_ENTITLEMENTS:-}") ;;
111
108
  esac
112
109
  bundle_key=$(
113
110
  {
114
111
  cat "$metadata/input-key"
115
112
  shasum -a 256 "$0" "$apple_tool"
116
- if [[ -n "$plist_path" ]]; then
117
- shasum -a 256 "$plist_path"
118
- fi
113
+ for app_file in "${app_files[@]}"; do
114
+ if [[ -n "$app_file" ]]; then
115
+ shasum -a 256 "$app_file"
116
+ fi
117
+ done
119
118
  env | LC_ALL=C sort | sed -n '/^TOKAMAK_/p' |
120
119
  sed '/^TOKAMAK_IOS_TEAM_ID=/d; /^TOKAMAK_IOS_SIGNING_IDENTITY=/d; /^TOKAMAK_IOS_PROVISIONING_PROFILE=/d; /^TOKAMAK_MACOS_TEAM_ID=/d; /^TOKAMAK_IOS_BUILD_NUMBER=/d; /^TOKAMAK_MACOS_BUILD_NUMBER=/d'
121
120
  printf '%s\n' "${DEVELOPER_DIR:-}" "$PATH"
@@ -185,11 +184,11 @@ done
185
184
  registry=$temporary/TokamakPluginRegistry.swift
186
185
  {
187
186
  printf '%s\n\n' 'import Foundation'
188
- printf '%s\n' 'func tokamakPlugins() -> [any TokamakPlugin] {'
187
+ printf '%s\n' 'func tokamakPlugins(host: TokamakHost) -> [any TokamakPlugin] {'
189
188
  printf '%s\n' ' ['
190
189
  for plugin in "$input/plugins"/*; do
191
190
  [[ -d "$plugin" ]] || continue
192
- printf ' %s(),\n' "$(cat "$plugin/class")"
191
+ printf ' %s(host: host),\n' "$(cat "$plugin/class")"
193
192
  done
194
193
  printf '%s\n' ' ]'
195
194
  printf '%s\n' '}'
@@ -219,24 +218,11 @@ for plugin in "$input/plugins"/*; do
219
218
  done
220
219
  done
221
220
 
222
- # Simulator builds embed application-identifier and keychain-access-groups in
223
- # __TEXT,__entitlements, where the simulator reads them.
221
+ # Simulator builds embed their entitlements in __TEXT,__entitlements, where
222
+ # the simulator reads them.
224
223
  if [[ "$platform" == ios-simulator ]]; then
225
224
  simulator_entitlements=$temporary/simulator.entitlements
226
- cat > "$simulator_entitlements" <<EOF
227
- <?xml version="1.0" encoding="UTF-8"?>
228
- <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
229
- <plist version="1.0">
230
- <dict>
231
- <key>application-identifier</key>
232
- <string>$identifier</string>
233
- <key>keychain-access-groups</key>
234
- <array>
235
- <string>$identifier</string>
236
- </array>
237
- </dict>
238
- </plist>
239
- EOF
225
+ "$apple_tool" simulator-entitlements --identifier "$identifier" --output "$simulator_entitlements"
240
226
  swift_args+=(-Xlinker -sectcreate -Xlinker __TEXT -Xlinker __entitlements -Xlinker "$simulator_entitlements")
241
227
  fi
242
228
 
@@ -11,6 +11,12 @@ enum {
11
11
  TOKAMAK_DECISION_USE = 2,
12
12
  };
13
13
 
14
+ enum {
15
+ TOKAMAK_DISPATCH_HANDLED = 0,
16
+ TOKAMAK_DISPATCH_UNHANDLED = 1,
17
+ TOKAMAK_DISPATCH_FAILED = 2,
18
+ };
19
+
14
20
  typedef struct {
15
21
  uint8_t *data;
16
22
  size_t len;
@@ -33,6 +39,10 @@ uint16_t tokamak_runtime_restore_gateway(const void *runtime, char *error,
33
39
  bool tokamak_runtime_suspend(const void *runtime);
34
40
  bool tokamak_runtime_resume(const void *runtime);
35
41
  void tokamak_runtime_stop(void *runtime);
42
+ int32_t tokamak_runtime_dispatch(const void *runtime, const char *event,
43
+ const char *payload, uint64_t timeout_ms,
44
+ TokamakBytes *result, char *error,
45
+ size_t error_len);
36
46
 
37
47
  int32_t tokamak_runtime_server_authority(const void *runtime, const char *host,
38
48
  TokamakBytes *authority);
@@ -0,0 +1,372 @@
1
+ import Foundation
2
+ import TokamakRuntime
3
+
4
+ private let startupErrorFile = "startup-error.log"
5
+
6
+ /// The tokamak runtime and native plugins of this process. Both outlive any
7
+ /// WebView, so plugins receive app events before a page loads and a Worker
8
+ /// event can run while no page is loaded.
9
+ final class TokamakHost {
10
+ private(set) var plugins: [any TokamakPlugin] = []
11
+ private var runtime: Result<RuntimeHandle, Error>?
12
+ private var waiting: [(Result<RuntimeHandle, Error>) -> Void] = []
13
+
14
+ /// Creates the plugins and starts the runtime in the background. Call it
15
+ /// before launch finishes.
16
+ init() {
17
+ plugins = tokamakPlugins(host: self)
18
+ DispatchQueue.global(qos: .userInitiated).async {
19
+ let result = Result { try RuntimeHandle() }
20
+ DispatchQueue.main.async { self.started(result) }
21
+ }
22
+ }
23
+
24
+ /// Calls `completion` on the main thread once the runtime has started or
25
+ /// failed to start.
26
+ func whenStarted(_ completion: @escaping (Result<RuntimeHandle, Error>) -> Void) {
27
+ dispatchPrecondition(condition: .onQueue(.main))
28
+ if let runtime {
29
+ completion(runtime)
30
+ } else {
31
+ waiting.append(completion)
32
+ }
33
+ }
34
+
35
+ /// Runs the Worker's `event` handler with a JSON-serialisable `payload`,
36
+ /// including work it passes to `ctx.waitUntil`, and stops it after
37
+ /// `timeout`. Call it on the main thread; `completion` runs on the main
38
+ /// thread.
39
+ func dispatch(
40
+ event: String,
41
+ payload: Any,
42
+ timeout: TimeInterval,
43
+ completion: @escaping (Result<TokamakEventOutcome, Error>) -> Void
44
+ ) {
45
+ whenStarted { result in
46
+ switch result {
47
+ case .failure(let error):
48
+ completion(.failure(error))
49
+ case .success(let runtime):
50
+ DispatchQueue.global(qos: .userInitiated).async {
51
+ let outcome = Result {
52
+ try runtime.dispatch(event: event, payload: payload, timeout: timeout)
53
+ }
54
+ DispatchQueue.main.async { completion(outcome) }
55
+ }
56
+ }
57
+ }
58
+ }
59
+
60
+ func didRegisterForRemoteNotifications(deviceToken: Data) {
61
+ for plugin in plugins {
62
+ plugin.didRegisterForRemoteNotifications(deviceToken: deviceToken)
63
+ }
64
+ }
65
+
66
+ func didFailToRegisterForRemoteNotifications(error: Error) {
67
+ for plugin in plugins {
68
+ plugin.didFailToRegisterForRemoteNotifications(error: error)
69
+ }
70
+ }
71
+
72
+ /// Delivers a remote notification to every plugin, calling `completion` on
73
+ /// the main thread once each has finished with it.
74
+ func didReceiveRemoteNotification(
75
+ _ userInfo: [AnyHashable: Any],
76
+ completion: @escaping (TokamakBackgroundResult) -> Void
77
+ ) {
78
+ let group = DispatchGroup()
79
+ var results: [TokamakBackgroundResult] = []
80
+ for plugin in plugins {
81
+ group.enter()
82
+ plugin.didReceiveRemoteNotification(userInfo) { result in
83
+ DispatchQueue.main.async {
84
+ results.append(result)
85
+ group.leave()
86
+ }
87
+ }
88
+ }
89
+ group.notify(queue: .main) {
90
+ completion(TokamakBackgroundResult(combining: results))
91
+ }
92
+ }
93
+
94
+ private func started(_ result: Result<RuntimeHandle, Error>) {
95
+ if case .failure(let error) = result {
96
+ RuntimeHandle.recordStartupFailure(error)
97
+ }
98
+ runtime = result
99
+ let callbacks = waiting
100
+ waiting.removeAll()
101
+ for callback in callbacks {
102
+ callback(result)
103
+ }
104
+ }
105
+ }
106
+
107
+ /// What a Worker event handler did.
108
+ enum TokamakEventOutcome {
109
+ /// The handler's result, decoded from JSON; `NSNull` when it returned nothing.
110
+ case handled(Any)
111
+ /// The Worker has no handler for the event.
112
+ case unhandled
113
+ }
114
+
115
+ /// What a plugin did with a background event, reported to the system.
116
+ enum TokamakBackgroundResult {
117
+ case newData
118
+ case noData
119
+ case failed
120
+
121
+ /// New data when any plugin fetched some, otherwise a failure when any
122
+ /// plugin failed.
123
+ init(combining results: [Self]) {
124
+ if results.contains(.newData) {
125
+ self = .newData
126
+ } else if results.contains(.failed) {
127
+ self = .failed
128
+ } else {
129
+ self = .noData
130
+ }
131
+ }
132
+ }
133
+
134
+ enum RuntimeError: Error, CustomStringConvertible {
135
+ case configuration(String)
136
+ case runtime(String)
137
+
138
+ var description: String {
139
+ switch self {
140
+ case .configuration(let message), .runtime(let message):
141
+ return message
142
+ }
143
+ }
144
+ }
145
+
146
+ enum AuthenticationMaterial<Value> {
147
+ case defaultHandling
148
+ case cancel
149
+ case use(Value)
150
+ }
151
+
152
+ /// A started tokamak runtime, stopped when released.
153
+ final class RuntimeHandle {
154
+ let appHost: String
155
+ private var handle: UnsafeMutableRawPointer?
156
+
157
+ init() throws {
158
+ guard
159
+ let appHost = Bundle.main.object(forInfoDictionaryKey: "TokamakHost") as? String,
160
+ !appHost.isEmpty
161
+ else {
162
+ throw RuntimeError.configuration("TokamakHost is required")
163
+ }
164
+ self.appHost = appHost
165
+
166
+ let state = try Self.stateDirectory()
167
+ var error = [CChar](repeating: 0, count: 512)
168
+ if let endpoint = Bundle.main.object(forInfoDictionaryKey: "TokamakDevEndpoint") as? String,
169
+ let sessionToken = Bundle.main.object(forInfoDictionaryKey: "TokamakDevSessionToken")
170
+ as? String
171
+ {
172
+ handle = state.path.withCString { statePath in
173
+ appHost.withCString { appHost in
174
+ endpoint.withCString { endpoint in
175
+ sessionToken.withCString { sessionToken in
176
+ error.withUnsafeMutableBufferPointer { error in
177
+ tokamak_runtime_start_development(
178
+ statePath,
179
+ appHost,
180
+ endpoint,
181
+ sessionToken,
182
+ error.baseAddress,
183
+ error.count
184
+ )
185
+ }
186
+ }
187
+ }
188
+ }
189
+ }
190
+ } else {
191
+ let bundle = Bundle.main.resourceURL?.appendingPathComponent("app")
192
+ guard let bundle else {
193
+ throw RuntimeError.configuration("app bundle resources are unavailable")
194
+ }
195
+ handle = bundle.path.withCString { bundlePath in
196
+ state.path.withCString { statePath in
197
+ appHost.withCString { appHost in
198
+ error.withUnsafeMutableBufferPointer { error in
199
+ tokamak_runtime_start(
200
+ bundlePath,
201
+ statePath,
202
+ appHost,
203
+ error.baseAddress,
204
+ error.count
205
+ )
206
+ }
207
+ }
208
+ }
209
+ }
210
+ }
211
+ guard handle != nil else {
212
+ throw RuntimeError.runtime(String(cString: error))
213
+ }
214
+ try? FileManager.default.removeItem(
215
+ at: state.appendingPathComponent(startupErrorFile)
216
+ )
217
+ }
218
+
219
+ deinit {
220
+ if let handle {
221
+ tokamak_runtime_stop(handle)
222
+ }
223
+ }
224
+
225
+ var port: UInt16 {
226
+ tokamak_runtime_port(handle)
227
+ }
228
+
229
+ func restoreGateway() throws -> UInt16 {
230
+ var error = [CChar](repeating: 0, count: 512)
231
+ let port = error.withUnsafeMutableBufferPointer { error in
232
+ tokamak_runtime_restore_gateway(
233
+ handle,
234
+ error.baseAddress,
235
+ error.count
236
+ )
237
+ }
238
+ guard port != 0 else {
239
+ throw RuntimeError.runtime(String(cString: error))
240
+ }
241
+ return port
242
+ }
243
+
244
+ /// Runs the Worker's `event` handler, blocking until it settles or
245
+ /// `timeout` passes.
246
+ func dispatch(event: String, payload: Any, timeout: TimeInterval) throws -> TokamakEventOutcome {
247
+ guard JSONSerialization.isValidJSONObject([payload]) else {
248
+ throw RuntimeError.runtime("the \(event) payload is not JSON")
249
+ }
250
+ let payload = String(
251
+ decoding: try JSONSerialization.data(withJSONObject: payload, options: .fragmentsAllowed),
252
+ as: UTF8.self
253
+ )
254
+ var result = TokamakBytes(data: nil, len: 0)
255
+ var error = [CChar](repeating: 0, count: 1024)
256
+ let status = event.withCString { event in
257
+ payload.withCString { payload in
258
+ error.withUnsafeMutableBufferPointer { error in
259
+ tokamak_runtime_dispatch(
260
+ handle,
261
+ event,
262
+ payload,
263
+ UInt64(timeout * 1000),
264
+ &result,
265
+ error.baseAddress,
266
+ error.count
267
+ )
268
+ }
269
+ }
270
+ }
271
+ defer { tokamak_bytes_free(result) }
272
+ switch status {
273
+ case Int32(TOKAMAK_DISPATCH_HANDLED):
274
+ guard let bytes = result.data else {
275
+ throw RuntimeError.runtime("the Worker event result is missing")
276
+ }
277
+ return .handled(
278
+ try JSONSerialization.jsonObject(
279
+ with: Data(bytes: bytes, count: result.len),
280
+ options: .fragmentsAllowed
281
+ ))
282
+ case Int32(TOKAMAK_DISPATCH_UNHANDLED):
283
+ return .unhandled
284
+ default:
285
+ throw RuntimeError.runtime(String(cString: error))
286
+ }
287
+ }
288
+
289
+ func serverAuthority(
290
+ host: String
291
+ ) -> AuthenticationMaterial<Data> {
292
+ var bytes = TokamakBytes(data: nil, len: 0)
293
+ let decision = host.withCString {
294
+ tokamak_runtime_server_authority(handle, $0, &bytes)
295
+ }
296
+ switch decision {
297
+ case Int32(TOKAMAK_DECISION_DEFAULT):
298
+ return .defaultHandling
299
+ case Int32(TOKAMAK_DECISION_USE):
300
+ defer { tokamak_bytes_free(bytes) }
301
+ guard let data = bytes.data else { return .cancel }
302
+ return .use(Data(bytes: data, count: bytes.len))
303
+ default:
304
+ return .cancel
305
+ }
306
+ }
307
+
308
+ func clientIdentity(
309
+ host: String,
310
+ previousFailures: Int
311
+ ) -> AuthenticationMaterial<(Data, Data)> {
312
+ var identity = TokamakIdentity(
313
+ certificate: TokamakBytes(data: nil, len: 0),
314
+ private_key: TokamakBytes(data: nil, len: 0)
315
+ )
316
+ let decision = host.withCString {
317
+ tokamak_runtime_client_identity(
318
+ handle,
319
+ $0,
320
+ max(previousFailures, 0),
321
+ &identity
322
+ )
323
+ }
324
+ switch decision {
325
+ case Int32(TOKAMAK_DECISION_DEFAULT):
326
+ return .defaultHandling
327
+ case Int32(TOKAMAK_DECISION_USE):
328
+ defer { tokamak_identity_free(identity) }
329
+ guard
330
+ let certificate = identity.certificate.data,
331
+ let privateKey = identity.private_key.data
332
+ else {
333
+ return .cancel
334
+ }
335
+ return .use(
336
+ (
337
+ Data(bytes: certificate, count: identity.certificate.len),
338
+ Data(bytes: privateKey, count: identity.private_key.len)
339
+ ))
340
+ default:
341
+ return .cancel
342
+ }
343
+ }
344
+
345
+ static func recordStartupFailure(_ error: Error) {
346
+ print("tokamak runtime startup failed: \(error)")
347
+ guard let state = try? stateDirectory() else { return }
348
+ try? String(describing: error).write(
349
+ to: state.appendingPathComponent(startupErrorFile),
350
+ atomically: true,
351
+ encoding: .utf8
352
+ )
353
+ }
354
+
355
+ private static func stateDirectory() throws -> URL {
356
+ guard let identifier = Bundle.main.bundleIdentifier else {
357
+ throw RuntimeError.configuration("CFBundleIdentifier is required")
358
+ }
359
+ let root = try FileManager.default.url(
360
+ for: .cachesDirectory,
361
+ in: .userDomainMask,
362
+ appropriateFor: nil,
363
+ create: true
364
+ )
365
+ let state = root.appendingPathComponent(identifier, isDirectory: true)
366
+ try FileManager.default.createDirectory(
367
+ at: state,
368
+ withIntermediateDirectories: true
369
+ )
370
+ return state
371
+ }
372
+ }
@@ -12,9 +12,12 @@ struct TokamakPluginError: Error {
12
12
 
13
13
  typealias TokamakPluginReply = (Result<Any?, TokamakPluginError>) -> Void
14
14
 
15
+ /// A native plugin, created once per process when the app launches.
15
16
  protocol TokamakPlugin: AnyObject {
16
17
  var id: String { get }
17
18
 
19
+ init(host: TokamakHost)
20
+
18
21
  func call(
19
22
  method: String,
20
23
  arguments: Any,
@@ -26,6 +29,17 @@ protocol TokamakPlugin: AnyObject {
26
29
  arguments: Any,
27
30
  reply: @escaping TokamakPluginReply
28
31
  ) -> (() -> Void)
32
+
33
+ func didRegisterForRemoteNotifications(deviceToken: Data)
34
+
35
+ func didFailToRegisterForRemoteNotifications(error: Error)
36
+
37
+ /// Handles a remote notification delivered to the app, calling `completion`
38
+ /// once when finished with it.
39
+ func didReceiveRemoteNotification(
40
+ _ userInfo: [AnyHashable: Any],
41
+ completion: @escaping (TokamakBackgroundResult) -> Void
42
+ )
29
43
  }
30
44
 
31
45
  extension TokamakPlugin {
@@ -45,6 +59,17 @@ extension TokamakPlugin {
45
59
  reply(.failure(.notSupported("\(id).\(method) is not supported")))
46
60
  return {}
47
61
  }
62
+
63
+ func didRegisterForRemoteNotifications(deviceToken: Data) {}
64
+
65
+ func didFailToRegisterForRemoteNotifications(error: Error) {}
66
+
67
+ func didReceiveRemoteNotification(
68
+ _ userInfo: [AnyHashable: Any],
69
+ completion: @escaping (TokamakBackgroundResult) -> Void
70
+ ) {
71
+ completion(.noData)
72
+ }
48
73
  }
49
74
 
50
75
  final class TokamakPluginBridge: NSObject, WKScriptMessageHandler {
@@ -1,7 +1,6 @@
1
1
  import Foundation
2
2
  import Network
3
3
  import Security
4
- import TokamakRuntime
5
4
  import WebKit
6
5
 
7
6
  #if os(macOS)
@@ -10,209 +9,14 @@ import WebKit
10
9
  import UIKit
11
10
  #endif
12
11
 
13
- private let startupErrorFile = "startup-error.log"
14
12
  private let failurePage =
15
13
  "<h1>App failed to start</h1><p>Check startup-error.log for details.</p>"
16
14
 
17
- private enum ShellError: Error, CustomStringConvertible {
18
- case configuration(String)
19
- case runtime(String)
20
-
21
- var description: String {
22
- switch self {
23
- case .configuration(let message), .runtime(let message):
24
- return message
25
- }
26
- }
27
- }
28
-
29
- private enum AuthenticationMaterial<Value> {
30
- case defaultHandling
31
- case cancel
32
- case use(Value)
33
- }
34
-
35
- private final class RuntimeHost {
36
- let host: String
37
- private var handle: UnsafeMutableRawPointer?
38
-
39
- init() throws {
40
- guard
41
- let host = Bundle.main.object(forInfoDictionaryKey: "TokamakHost") as? String,
42
- !host.isEmpty
43
- else {
44
- throw ShellError.configuration("TokamakHost is required")
45
- }
46
- self.host = host
47
-
48
- let state = try Self.stateDirectory()
49
- var error = [CChar](repeating: 0, count: 512)
50
- if let endpoint = Bundle.main.object(forInfoDictionaryKey: "TokamakDevEndpoint") as? String,
51
- let sessionToken = Bundle.main.object(forInfoDictionaryKey: "TokamakDevSessionToken")
52
- as? String
53
- {
54
- handle = state.path.withCString { statePath in
55
- host.withCString { host in
56
- endpoint.withCString { endpoint in
57
- sessionToken.withCString { sessionToken in
58
- error.withUnsafeMutableBufferPointer { error in
59
- tokamak_runtime_start_development(
60
- statePath,
61
- host,
62
- endpoint,
63
- sessionToken,
64
- error.baseAddress,
65
- error.count
66
- )
67
- }
68
- }
69
- }
70
- }
71
- }
72
- } else {
73
- let bundle = Bundle.main.resourceURL?.appendingPathComponent("app")
74
- guard let bundle else {
75
- throw ShellError.configuration("app bundle resources are unavailable")
76
- }
77
- handle = bundle.path.withCString { bundlePath in
78
- state.path.withCString { statePath in
79
- host.withCString { host in
80
- error.withUnsafeMutableBufferPointer { error in
81
- tokamak_runtime_start(
82
- bundlePath,
83
- statePath,
84
- host,
85
- error.baseAddress,
86
- error.count
87
- )
88
- }
89
- }
90
- }
91
- }
92
- }
93
- guard handle != nil else {
94
- throw ShellError.runtime(String(cString: error))
95
- }
96
- try? FileManager.default.removeItem(
97
- at: state.appendingPathComponent(startupErrorFile)
98
- )
99
- }
100
-
101
- deinit {
102
- if let handle {
103
- tokamak_runtime_stop(handle)
104
- }
105
- }
106
-
107
- var port: UInt16 {
108
- tokamak_runtime_port(handle)
109
- }
110
-
111
- func restoreGateway() throws -> UInt16 {
112
- var error = [CChar](repeating: 0, count: 512)
113
- let port = error.withUnsafeMutableBufferPointer { error in
114
- tokamak_runtime_restore_gateway(
115
- handle,
116
- error.baseAddress,
117
- error.count
118
- )
119
- }
120
- guard port != 0 else {
121
- throw ShellError.runtime(String(cString: error))
122
- }
123
- return port
124
- }
125
-
126
- func serverAuthority(
127
- host: String
128
- ) -> AuthenticationMaterial<Data> {
129
- var bytes = TokamakBytes(data: nil, len: 0)
130
- let decision = host.withCString {
131
- tokamak_runtime_server_authority(handle, $0, &bytes)
132
- }
133
- switch decision {
134
- case Int32(TOKAMAK_DECISION_DEFAULT):
135
- return .defaultHandling
136
- case Int32(TOKAMAK_DECISION_USE):
137
- defer { tokamak_bytes_free(bytes) }
138
- guard let data = bytes.data else { return .cancel }
139
- return .use(Data(bytes: data, count: bytes.len))
140
- default:
141
- return .cancel
142
- }
143
- }
144
-
145
- func clientIdentity(
146
- host: String,
147
- previousFailures: Int
148
- ) -> AuthenticationMaterial<(Data, Data)> {
149
- var identity = TokamakIdentity(
150
- certificate: TokamakBytes(data: nil, len: 0),
151
- private_key: TokamakBytes(data: nil, len: 0)
152
- )
153
- let decision = host.withCString {
154
- tokamak_runtime_client_identity(
155
- handle,
156
- $0,
157
- max(previousFailures, 0),
158
- &identity
159
- )
160
- }
161
- switch decision {
162
- case Int32(TOKAMAK_DECISION_DEFAULT):
163
- return .defaultHandling
164
- case Int32(TOKAMAK_DECISION_USE):
165
- defer { tokamak_identity_free(identity) }
166
- guard
167
- let certificate = identity.certificate.data,
168
- let privateKey = identity.private_key.data
169
- else {
170
- return .cancel
171
- }
172
- return .use(
173
- (
174
- Data(bytes: certificate, count: identity.certificate.len),
175
- Data(bytes: privateKey, count: identity.private_key.len)
176
- ))
177
- default:
178
- return .cancel
179
- }
180
- }
181
-
182
- static func recordStartupFailure(_ error: Error) {
183
- print("tokamak runtime startup failed: \(error)")
184
- guard let state = try? stateDirectory() else { return }
185
- try? String(describing: error).write(
186
- to: state.appendingPathComponent(startupErrorFile),
187
- atomically: true,
188
- encoding: .utf8
189
- )
190
- }
191
-
192
- private static func stateDirectory() throws -> URL {
193
- guard let identifier = Bundle.main.bundleIdentifier else {
194
- throw ShellError.configuration("CFBundleIdentifier is required")
195
- }
196
- let root = try FileManager.default.url(
197
- for: .cachesDirectory,
198
- in: .userDomainMask,
199
- appropriateFor: nil,
200
- create: true
201
- )
202
- let state = root.appendingPathComponent(identifier, isDirectory: true)
203
- try FileManager.default.createDirectory(
204
- at: state,
205
- withIntermediateDirectories: true
206
- )
207
- return state
208
- }
209
- }
210
-
211
15
  private final class NavigationDelegate: NSObject, WKNavigationDelegate, WKUIDelegate {
212
- private let runtime: RuntimeHost
16
+ private let runtime: RuntimeHandle
213
17
  private let pluginBridge: TokamakPluginBridge
214
18
 
215
- init(runtime: RuntimeHost, pluginBridge: TokamakPluginBridge) {
19
+ init(runtime: RuntimeHandle, pluginBridge: TokamakPluginBridge) {
216
20
  self.runtime = runtime
217
21
  self.pluginBridge = pluginBridge
218
22
  }
@@ -239,7 +43,7 @@ private final class NavigationDelegate: NSObject, WKNavigationDelegate, WKUIDele
239
43
  return
240
44
  }
241
45
 
242
- if url.isAppOrigin(runtime.host) {
46
+ if url.isAppOrigin(runtime.appHost) {
243
47
  if navigationAction.targetFrame == nil {
244
48
  webView.load(navigationAction.request)
245
49
  decisionHandler(.cancel)
@@ -266,7 +70,7 @@ private final class NavigationDelegate: NSObject, WKNavigationDelegate, WKUIDele
266
70
  return
267
71
  }
268
72
 
269
- if url.isAppOrigin(runtime.host) {
73
+ if url.isAppOrigin(runtime.appHost) {
270
74
  decisionHandler(.allow)
271
75
  } else {
272
76
  openExternal(url)
@@ -282,7 +86,7 @@ private final class NavigationDelegate: NSObject, WKNavigationDelegate, WKUIDele
282
86
  ) -> WKWebView? {
283
87
  _ = (configuration, windowFeatures)
284
88
  guard let url = navigationAction.request.url else { return nil }
285
- if url.isAppOrigin(runtime.host) {
89
+ if url.isAppOrigin(runtime.appHost) {
286
90
  webView.load(navigationAction.request)
287
91
  } else {
288
92
  openExternal(url)
@@ -299,7 +103,7 @@ private final class NavigationDelegate: NSObject, WKNavigationDelegate, WKUIDele
299
103
  type: WKMediaCaptureType,
300
104
  decisionHandler: @escaping (WKPermissionDecision) -> Void
301
105
  ) {
302
- decisionHandler(origin.isAppOrigin(runtime.host) ? .grant : .prompt)
106
+ decisionHandler(origin.isAppOrigin(runtime.appHost) ? .grant : .prompt)
303
107
  }
304
108
 
305
109
  func webView(
@@ -457,28 +261,29 @@ private final class NavigationDelegate: NSObject, WKNavigationDelegate, WKUIDele
457
261
  }
458
262
 
459
263
  private final class TokamakController {
460
- private var runtime: RuntimeHost?
264
+ private let host: TokamakHost
265
+ private var runtime: RuntimeHandle?
461
266
  private var navigation: NavigationDelegate?
462
267
  private var pluginBridge: TokamakPluginBridge?
463
268
  private weak var activeWebView: WKWebView?
464
269
  private var dataStore: WKWebsiteDataStore?
465
270
  private var proxyPort: UInt16?
466
271
 
272
+ init(host: TokamakHost) {
273
+ self.host = host
274
+ }
275
+
467
276
  func start(
468
277
  frame: CGRect,
469
278
  completion: @escaping (WKWebView) -> Void
470
279
  ) {
471
- DispatchQueue.global(qos: .userInitiated).async {
472
- let result = Result { try RuntimeHost() }
473
- DispatchQueue.main.async {
474
- switch result {
475
- case .success(let runtime):
476
- self.runtime = runtime
477
- completion(self.webView(frame: frame, runtime: runtime))
478
- case .failure(let error):
479
- RuntimeHost.recordStartupFailure(error)
480
- completion(self.failureWebView(frame: frame))
481
- }
280
+ host.whenStarted { result in
281
+ switch result {
282
+ case .success(let runtime):
283
+ self.runtime = runtime
284
+ completion(self.webView(frame: frame, runtime: runtime))
285
+ case .failure:
286
+ completion(self.failureWebView(frame: frame))
482
287
  }
483
288
  }
484
289
  }
@@ -488,7 +293,7 @@ private final class TokamakController {
488
293
  do {
489
294
  let port = try runtime.restoreGateway()
490
295
  guard port != proxyPort else { return }
491
- setProxy(port: port, host: runtime.host, dataStore: dataStore)
296
+ setProxy(port: port, host: runtime.appHost, dataStore: dataStore)
492
297
  proxyPort = port
493
298
  activeWebView?.reload()
494
299
  } catch {
@@ -496,11 +301,11 @@ private final class TokamakController {
496
301
  }
497
302
  }
498
303
 
499
- private func webView(frame: CGRect, runtime: RuntimeHost) -> WKWebView {
304
+ private func webView(frame: CGRect, runtime: RuntimeHandle) -> WKWebView {
500
305
  let configuration = WKWebViewConfiguration()
501
306
  let dataStore = WKWebsiteDataStore.default()
502
307
  let port = runtime.port
503
- setProxy(port: port, host: runtime.host, dataStore: dataStore)
308
+ setProxy(port: port, host: runtime.appHost, dataStore: dataStore)
504
309
  self.dataStore = dataStore
505
310
  self.proxyPort = port
506
311
  configuration.websiteDataStore = dataStore
@@ -511,8 +316,8 @@ private final class TokamakController {
511
316
  configuration.mediaTypesRequiringUserActionForPlayback = []
512
317
 
513
318
  let pluginBridge = TokamakPluginBridge(
514
- host: runtime.host,
515
- plugins: tokamakPlugins()
319
+ host: runtime.appHost,
320
+ plugins: host.plugins
516
321
  )
517
322
  pluginBridge.install(in: configuration.userContentController)
518
323
  let navigation = NavigationDelegate(
@@ -530,7 +335,7 @@ private final class TokamakController {
530
335
  self.pluginBridge = pluginBridge
531
336
  webView.navigationDelegate = navigation
532
337
  webView.uiDelegate = navigation
533
- webView.load(URLRequest(url: URL(string: "https://\(runtime.host)/")!))
338
+ webView.load(URLRequest(url: URL(string: "https://\(runtime.appHost)/")!))
534
339
  return webView
535
340
  }
536
341
 
@@ -559,7 +364,8 @@ private final class TokamakController {
559
364
 
560
365
  #if os(macOS)
561
366
  private final class TokamakMacApplicationDelegate: NSObject, NSApplicationDelegate {
562
- private let controller = TokamakController()
367
+ private lazy var host = TokamakHost()
368
+ private lazy var controller = TokamakController(host: host)
563
369
  private var window: NSWindow?
564
370
 
565
371
  func applicationDidFinishLaunching(_ notification: Notification) {
@@ -595,6 +401,27 @@ private final class TokamakController {
595
401
  ) -> Bool {
596
402
  true
597
403
  }
404
+
405
+ func application(
406
+ _ application: NSApplication,
407
+ didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
408
+ ) {
409
+ host.didRegisterForRemoteNotifications(deviceToken: deviceToken)
410
+ }
411
+
412
+ func application(
413
+ _ application: NSApplication,
414
+ didFailToRegisterForRemoteNotificationsWithError error: Error
415
+ ) {
416
+ host.didFailToRegisterForRemoteNotifications(error: error)
417
+ }
418
+
419
+ func application(
420
+ _ application: NSApplication,
421
+ didReceiveRemoteNotification userInfo: [String: Any]
422
+ ) {
423
+ host.didReceiveRemoteNotification(userInfo) { _ in }
424
+ }
598
425
  }
599
426
 
600
427
  @main
@@ -609,7 +436,8 @@ private final class TokamakController {
609
436
  }
610
437
  #else
611
438
  private final class TokamakIOSApplicationDelegate: UIResponder, UIApplicationDelegate {
612
- private let controller = TokamakController()
439
+ private lazy var host = TokamakHost()
440
+ private lazy var controller = TokamakController(host: host)
613
441
  var window: UIWindow?
614
442
 
615
443
  func application(
@@ -636,6 +464,33 @@ private final class TokamakController {
636
464
  controller.restoreGateway()
637
465
  }
638
466
 
467
+ func application(
468
+ _ application: UIApplication,
469
+ didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data
470
+ ) {
471
+ host.didRegisterForRemoteNotifications(deviceToken: deviceToken)
472
+ }
473
+
474
+ func application(
475
+ _ application: UIApplication,
476
+ didFailToRegisterForRemoteNotificationsWithError error: Error
477
+ ) {
478
+ host.didFailToRegisterForRemoteNotifications(error: error)
479
+ }
480
+
481
+ func application(
482
+ _ application: UIApplication,
483
+ didReceiveRemoteNotification userInfo: [AnyHashable: Any],
484
+ fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void
485
+ ) {
486
+ host.didReceiveRemoteNotification(userInfo) { result in
487
+ switch result {
488
+ case .newData: completionHandler(.newData)
489
+ case .noData: completionHandler(.noData)
490
+ case .failed: completionHandler(.failed)
491
+ }
492
+ }
493
+ }
639
494
  }
640
495
 
641
496
  @main
@@ -23,6 +23,10 @@
23
23
  "kind": "string",
24
24
  "description": "CFBundleVersion, one to three period-separated integers; defaults to the app version"
25
25
  },
26
+ "entitlements": {
27
+ "kind": "path",
28
+ "description": "Entitlements plist layered over the provisioning profile's entitlements at signing"
29
+ },
26
30
  "plist": {
27
31
  "kind": "path",
28
32
  "description": "Info.plist whose values take precedence over generated and plugin values"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokamakdev/platform-ios-simulator-arm64",
3
- "version": "0.1.1-beta.49",
3
+ "version": "0.1.1-beta.50",
4
4
  "description": "The tokamak platform pack for ios-simulator-arm64",
5
5
  "repository": {
6
6
  "type": "git",