@mentra/glasses-media 3.4.0-dev.950 → 3.4.0-dev.951
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/index.d.ts +1 -1
- package/index.js +1 -1
- package/ios/CoreKit/Sources/GlassesMediaCore/HotspotInternetPolicy.swift +98 -1
- package/ios/CoreKit/Sources/GlassesMediaCore/MacHotspotAssociation.swift +58 -13
- package/ios/GlassesHotspotNetwork.swift +105 -47
- package/ios/GlassesMediaRelayModule.swift +2 -2
- package/package.json +1 -1
package/index.d.ts
CHANGED
package/index.js
CHANGED
|
@@ -6,5 +6,5 @@ const native = () => require("expo-modules-core").requireNativeModule("MentraGla
|
|
|
6
6
|
export const glassesHotspot = Object.freeze({
|
|
7
7
|
join: (id, ssid, password, gateway, purpose = "outgoing-client") =>
|
|
8
8
|
native().joinLocalHotspot(id, ssid, password, gateway ?? null, purpose),
|
|
9
|
-
leave: (id) => native().leaveLocalHotspot(id),
|
|
9
|
+
leave: (id, waitForInternet = false) => native().leaveLocalHotspot(id, waitForInternet),
|
|
10
10
|
})
|
|
@@ -8,9 +8,106 @@ public enum HotspotInternetPolicy {
|
|
|
8
8
|
if cellular { return "cellular" }
|
|
9
9
|
if ethernet { return "ethernet" }
|
|
10
10
|
// Callers supply a Wi-Fi SSID only after releasing the glasses hotspot.
|
|
11
|
-
if let restoredWifiSSID, !restoredWifiSSID.isEmpty, restoredWifiSSID != glassesSSID {
|
|
11
|
+
if let restoredWifiSSID, let glassesSSID, !glassesSSID.isEmpty, !restoredWifiSSID.isEmpty, restoredWifiSSID != glassesSSID {
|
|
12
12
|
return "wifi"
|
|
13
13
|
}
|
|
14
14
|
return nil
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
/// One queue-confined route observation. Late SSID callbacks cannot settle a
|
|
19
|
+
/// newer path or session, and timeout/cancellation settle this observation once.
|
|
20
|
+
public final class HotspotInternetObservation {
|
|
21
|
+
private let generation: Int
|
|
22
|
+
private let glassesSSID: String?
|
|
23
|
+
private var pathRevision = 0
|
|
24
|
+
private var finished = false
|
|
25
|
+
private var readingSSID = false
|
|
26
|
+
private var settledReply: (() -> Void)?
|
|
27
|
+
|
|
28
|
+
public init(generation: Int, glassesSSID: String?) {
|
|
29
|
+
self.generation = generation
|
|
30
|
+
self.glassesSSID = glassesSSID
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public func beginPath(currentGeneration: Int) -> Int? {
|
|
34
|
+
guard !finished, currentGeneration == generation else { return nil }
|
|
35
|
+
pathRevision += 1
|
|
36
|
+
return pathRevision
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public func restoredWifiRoute(ssid: String?, pathRevision: Int, currentGeneration: Int) -> String? {
|
|
40
|
+
guard !finished, currentGeneration == generation, pathRevision == self.pathRevision else { return nil }
|
|
41
|
+
return HotspotInternetPolicy.route(satisfied: true, cellular: false, ethernet: false,
|
|
42
|
+
restoredWifiSSID: ssid, glassesSSID: glassesSSID)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public func beginSSIDRead() -> Bool {
|
|
46
|
+
guard !finished, !readingSSID else { return false }
|
|
47
|
+
readingSSID = true
|
|
48
|
+
return true
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
public func completeSSIDRead() {
|
|
52
|
+
readingSSID = false
|
|
53
|
+
let reply = settledReply
|
|
54
|
+
settledReply = nil
|
|
55
|
+
reply?()
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public func afterSSIDRead(_ reply: @escaping () -> Void) {
|
|
59
|
+
if readingSSID { settledReply = reply } else { reply() }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public func settle() -> Bool {
|
|
63
|
+
guard !finished else { return false }
|
|
64
|
+
finished = true
|
|
65
|
+
return true
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Queue-confined cancellation owner. An old observation may finish joining its
|
|
70
|
+
/// native read after its replacement starts, but cannot clear that replacement.
|
|
71
|
+
public final class HotspotInternetObservationOwner {
|
|
72
|
+
private var current: (observation: HotspotInternetObservation, cancel: () -> Void)?
|
|
73
|
+
|
|
74
|
+
public init() {}
|
|
75
|
+
|
|
76
|
+
public func install(_ observation: HotspotInternetObservation, cancel: @escaping () -> Void) {
|
|
77
|
+
current?.cancel()
|
|
78
|
+
current = (observation, cancel)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
public func clear(_ observation: HotspotInternetObservation) {
|
|
82
|
+
if current?.observation === observation { current = nil }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public func cancel() {
|
|
86
|
+
current?.cancel()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/// Queue-confined completion of one leave. Concurrent callers share cleanup;
|
|
91
|
+
/// the original ownership token is released before any caller can start again.
|
|
92
|
+
public final class HotspotLeaveCompletion {
|
|
93
|
+
private var replies: [(Result<Void, Error>) -> Void] = []
|
|
94
|
+
private var pending = true
|
|
95
|
+
private let release: () -> Void
|
|
96
|
+
|
|
97
|
+
public init(release: @escaping () -> Void) {
|
|
98
|
+
self.release = release
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
public func append(_ reply: @escaping (Result<Void, Error>) -> Void) {
|
|
102
|
+
replies.append(reply)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
public func complete(_ result: Result<Void, Error>) {
|
|
106
|
+
guard pending else { return }
|
|
107
|
+
pending = false
|
|
108
|
+
if case .success = result { release() }
|
|
109
|
+
let current = replies
|
|
110
|
+
replies.removeAll()
|
|
111
|
+
current.forEach { $0(result) }
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -5,13 +5,23 @@ import ObjectiveC
|
|
|
5
5
|
/// Blocking public CoreWLAN calls are isolated here; the existing hotspot owner
|
|
6
6
|
/// retains custody until they return, even after cancellation or a timeout.
|
|
7
7
|
final class MacHotspotAssociation {
|
|
8
|
-
private let
|
|
8
|
+
private let condition = NSCondition()
|
|
9
9
|
private var cancelled = false
|
|
10
10
|
private var interface: MacWifiInterface?
|
|
11
11
|
private var joinedSSID: String?
|
|
12
|
+
private let deadline: TimeInterval
|
|
13
|
+
private let now: () -> TimeInterval
|
|
14
|
+
private let pause: ((TimeInterval) -> Void)?
|
|
12
15
|
private let makeInterface: () throws -> MacWifiInterface
|
|
13
16
|
|
|
14
|
-
init(
|
|
17
|
+
init(deadline: TimeInterval = ProcessInfo.processInfo.systemUptime + 60,
|
|
18
|
+
now: @escaping () -> TimeInterval = { ProcessInfo.processInfo.systemUptime },
|
|
19
|
+
pause: ((TimeInterval) -> Void)? = nil,
|
|
20
|
+
makeInterface: @escaping () throws -> MacWifiInterface = { try PublicCoreWlanInterface() })
|
|
21
|
+
{
|
|
22
|
+
self.deadline = deadline
|
|
23
|
+
self.now = now
|
|
24
|
+
self.pause = pause
|
|
15
25
|
self.makeInterface = makeInterface
|
|
16
26
|
}
|
|
17
27
|
|
|
@@ -20,12 +30,15 @@ final class MacHotspotAssociation {
|
|
|
20
30
|
let interface = try makeInterface()
|
|
21
31
|
self.interface = interface
|
|
22
32
|
if try interface.currentSSID() != ssid {
|
|
23
|
-
let network = try
|
|
24
|
-
try
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
33
|
+
let network = try discoverNetwork(ssid: ssid, interface: interface)
|
|
34
|
+
try checkDiscoveryCurrent()
|
|
35
|
+
if let network, try interface.currentSSID() != ssid {
|
|
36
|
+
try checkDiscoveryCurrent()
|
|
37
|
+
// Record the intended association before this blocking call. A late
|
|
38
|
+
// successful join still belongs to us and must be cleaned up.
|
|
39
|
+
joinedSSID = ssid
|
|
40
|
+
try interface.associate(network: network, password: password)
|
|
41
|
+
}
|
|
29
42
|
}
|
|
30
43
|
try checkCurrent()
|
|
31
44
|
guard try interface.currentSSID() == ssid else {
|
|
@@ -35,7 +48,7 @@ final class MacHotspotAssociation {
|
|
|
35
48
|
}
|
|
36
49
|
|
|
37
50
|
func cancel() {
|
|
38
|
-
|
|
51
|
+
condition.lock(); cancelled = true; condition.broadcast(); condition.unlock()
|
|
39
52
|
}
|
|
40
53
|
|
|
41
54
|
/// Run on the same worker as association, after its blocking call returns.
|
|
@@ -57,8 +70,40 @@ final class MacHotspotAssociation {
|
|
|
57
70
|
try interface?.currentSSID()
|
|
58
71
|
}
|
|
59
72
|
|
|
73
|
+
static func observeCurrentSSID() throws -> String? {
|
|
74
|
+
try PublicCoreWlanInterface().currentSSID()
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
private func discoverNetwork(ssid: String, interface: MacWifiInterface) throws -> NSObject? {
|
|
78
|
+
while true {
|
|
79
|
+
try checkDiscoveryCurrent()
|
|
80
|
+
// Recheck Location authorization before and after each blocking scan.
|
|
81
|
+
if try interface.currentSSID() == ssid { return nil }
|
|
82
|
+
let network = try interface.network(ssid: ssid)
|
|
83
|
+
try checkDiscoveryCurrent()
|
|
84
|
+
if try interface.currentSSID() == ssid { return nil }
|
|
85
|
+
if let network { return network }
|
|
86
|
+
// Successful empty scans remain pending within this same join window.
|
|
87
|
+
// Native scan errors escape immediately; association is never retried.
|
|
88
|
+
let remaining = max(0, deadline - now())
|
|
89
|
+
if let pause { pause(min(1, remaining)) }
|
|
90
|
+
else {
|
|
91
|
+
condition.lock()
|
|
92
|
+
if !cancelled { _ = condition.wait(until: Date(timeIntervalSinceNow: min(1, remaining))) }
|
|
93
|
+
condition.unlock()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private func checkDiscoveryCurrent() throws {
|
|
99
|
+
try checkCurrent()
|
|
100
|
+
guard now() < deadline else {
|
|
101
|
+
throw LocalMediaError("The requested glasses hotspot was not found by Mac Wi-Fi before the hotspot join timed out")
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
60
105
|
private func checkCurrent() throws {
|
|
61
|
-
|
|
106
|
+
condition.lock(); let stopped = cancelled; condition.unlock()
|
|
62
107
|
if stopped { throw CancellationError() }
|
|
63
108
|
}
|
|
64
109
|
}
|
|
@@ -66,7 +111,7 @@ final class MacHotspotAssociation {
|
|
|
66
111
|
protocol MacWifiInterface: AnyObject {
|
|
67
112
|
func name() throws -> String
|
|
68
113
|
func currentSSID() throws -> String?
|
|
69
|
-
func network(ssid: String) throws -> NSObject
|
|
114
|
+
func network(ssid: String) throws -> NSObject?
|
|
70
115
|
func associate(network: NSObject, password: String) throws
|
|
71
116
|
func disassociate()
|
|
72
117
|
}
|
|
@@ -122,7 +167,7 @@ private final class PublicCoreWlanInterface: MacWifiInterface {
|
|
|
122
167
|
return String(data: data, encoding: .utf8)
|
|
123
168
|
}
|
|
124
169
|
|
|
125
|
-
func network(ssid: String) throws -> NSObject {
|
|
170
|
+
func network(ssid: String) throws -> NSObject? {
|
|
126
171
|
let selector = NSSelectorFromString("scanForNetworksWithSSID:error:")
|
|
127
172
|
let invoke = unsafeBitCast(interface.method(for: selector), to: MacWifiInvocation.Scan.self)
|
|
128
173
|
var error: NSError?
|
|
@@ -137,7 +182,7 @@ private final class PublicCoreWlanInterface: MacWifiInterface {
|
|
|
137
182
|
data == Data(ssid.utf8) else { continue }
|
|
138
183
|
return network
|
|
139
184
|
}
|
|
140
|
-
|
|
185
|
+
return nil
|
|
141
186
|
}
|
|
142
187
|
|
|
143
188
|
func associate(network: NSObject, password: String) throws {
|
|
@@ -19,7 +19,7 @@ public final class GlassesHotspotNetwork {
|
|
|
19
19
|
private var ownsConfiguration = false
|
|
20
20
|
private var cancelled = false
|
|
21
21
|
private var joinReply: ((Result<String, Error>) -> Void)?
|
|
22
|
-
private var
|
|
22
|
+
private var leaveCompletion: HotspotLeaveCompletion?
|
|
23
23
|
private var interfaceMonitors: [NWPathMonitor] = []
|
|
24
24
|
/// Wi-Fi identity learned since this join started; replaced on every join and cleared on leave.
|
|
25
25
|
private var wifiIdentity = HotspotWifiIdentity()
|
|
@@ -33,6 +33,8 @@ public final class GlassesHotspotNetwork {
|
|
|
33
33
|
private var macAuthorization: MacHotspotAuthorization?
|
|
34
34
|
private var ownershipToken: UUID?
|
|
35
35
|
private var finishingLeave = false
|
|
36
|
+
private var waitForInternetAfterLeave = false
|
|
37
|
+
private let internetObservationOwner = HotspotInternetObservationOwner()
|
|
36
38
|
public var onLost: ((String) -> Void)?
|
|
37
39
|
public var onPermissionRequired: (() -> Void)?
|
|
38
40
|
public init() {}
|
|
@@ -52,6 +54,7 @@ public final class GlassesHotspotNetwork {
|
|
|
52
54
|
self.ownershipToken = token
|
|
53
55
|
self.generation += 1
|
|
54
56
|
let gen = self.generation
|
|
57
|
+
let deadline = ProcessInfo.processInfo.systemUptime + 60
|
|
55
58
|
self.ssid = ssid
|
|
56
59
|
self.lastHotspotSSID = ssid
|
|
57
60
|
self.gatewayAddress = gateway
|
|
@@ -62,11 +65,11 @@ public final class GlassesHotspotNetwork {
|
|
|
62
65
|
self.joinReply = completion
|
|
63
66
|
self.startInterfaceMonitors(generation: gen)
|
|
64
67
|
if ProcessInfo.processInfo.isiOSAppOnMac {
|
|
65
|
-
self.authorizeMacAssociation(ssid: ssid, passphrase: passphrase, generation: gen)
|
|
68
|
+
self.authorizeMacAssociation(ssid: ssid, passphrase: passphrase, generation: gen, deadline: deadline)
|
|
66
69
|
} else {
|
|
67
70
|
self.applyConfiguration(ssid: ssid, passphrase: passphrase, generation: gen)
|
|
68
71
|
}
|
|
69
|
-
self.queue.asyncAfter(deadline: .now() +
|
|
72
|
+
self.queue.asyncAfter(deadline: .now() + max(0, deadline - ProcessInfo.processInfo.systemUptime)) {
|
|
70
73
|
// Once associated, the user may still be reading the Local Network alert.
|
|
71
74
|
// Its response time is not a hotspot association timeout.
|
|
72
75
|
guard gen == self.generation, self.joinReply != nil, self.localAccess == nil else { return }
|
|
@@ -82,12 +85,20 @@ public final class GlassesHotspotNetwork {
|
|
|
82
85
|
}
|
|
83
86
|
}
|
|
84
87
|
|
|
85
|
-
public func leave(completion: @escaping (Result<Void, Error>) -> Void) {
|
|
88
|
+
public func leave(waitForInternet: Bool = false, completion: @escaping (Result<Void, Error>) -> Void) {
|
|
86
89
|
queue.async {
|
|
90
|
+
if !waitForInternet { self.internetObservationOwner.cancel() }
|
|
91
|
+
self.waitForInternetAfterLeave = self.waitForInternetAfterLeave || waitForInternet
|
|
87
92
|
self.cancelled = true
|
|
88
93
|
self.macAssociation?.cancel()
|
|
89
94
|
self.macAuthorization?.cancel()
|
|
90
|
-
self.
|
|
95
|
+
if self.leaveCompletion == nil {
|
|
96
|
+
self.leaveCompletion = HotspotLeaveCompletion {
|
|
97
|
+
if let token = self.ownershipToken { HotspotNetworkOwnership.shared.release(token) }
|
|
98
|
+
self.ownershipToken = nil
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
self.leaveCompletion?.append(completion)
|
|
91
102
|
self.finishJoin(.failure(LocalMediaError("Hotspot join cancelled")))
|
|
92
103
|
if let ssid = self.ssid, self.ownsConfiguration { NEHotspotConfigurationManager.shared.removeConfiguration(forSSID: ssid) }
|
|
93
104
|
if !self.applying { self.finishLeave() }
|
|
@@ -157,44 +168,77 @@ public final class GlassesHotspotNetwork {
|
|
|
157
168
|
}
|
|
158
169
|
|
|
159
170
|
public func awaitInternet(allowWifiAfterRelease: Bool = false, completion: @escaping (Bool, String) -> Void) {
|
|
160
|
-
queue.async {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
171
|
+
queue.async { self.startInternetObservation(allowWifiAfterRelease: allowWifiAfterRelease, completion: completion) }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// Called on the owner queue, including directly from completeLeave. A queued
|
|
175
|
+
/// cancellation therefore cannot overtake observation initialization.
|
|
176
|
+
private func startInternetObservation(allowWifiAfterRelease: Bool, completion: @escaping (Bool, String) -> Void) {
|
|
177
|
+
let gen = generation
|
|
178
|
+
let observation = HotspotInternetObservation(generation: gen, glassesSSID: lastHotspotSSID)
|
|
179
|
+
let monitor = NWPathMonitor()
|
|
180
|
+
let sampleTimer = DispatchSource.makeTimerSource(queue: queue)
|
|
181
|
+
var currentPath: Network.NWPath?
|
|
182
|
+
let finish: (Bool, String) -> Void = { usable, detail in
|
|
183
|
+
guard observation.settle() else { return }
|
|
184
|
+
monitor.pathUpdateHandler = nil
|
|
185
|
+
monitor.cancel()
|
|
186
|
+
sampleTimer.setEventHandler {}
|
|
187
|
+
sampleTimer.cancel()
|
|
188
|
+
currentPath = nil
|
|
189
|
+
let reply = {
|
|
190
|
+
self.internetObservationOwner.clear(observation)
|
|
167
191
|
completion(usable, detail)
|
|
168
192
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
193
|
+
// Even a timed-out/cancelled observation joins its dispatched
|
|
194
|
+
// CoreWLAN read before releasing this session's owner.
|
|
195
|
+
observation.afterSSIDRead(reply)
|
|
196
|
+
}
|
|
197
|
+
internetObservationOwner.install(observation) { finish(false, "Hotspot internet observation cancelled") }
|
|
198
|
+
var currentPathRevision = 0
|
|
199
|
+
let sample = {
|
|
200
|
+
guard let path = currentPath else { return }
|
|
201
|
+
if let route = HotspotInternetPolicy.route(satisfied: path.status == .satisfied,
|
|
202
|
+
cellular: path.usesInterfaceType(.cellular),
|
|
203
|
+
ethernet: path.usesInterfaceType(.wiredEthernet))
|
|
204
|
+
{
|
|
205
|
+
finish(true, route)
|
|
206
|
+
return
|
|
207
|
+
}
|
|
208
|
+
// Sample returned membership while the route remains on Wi-Fi;
|
|
209
|
+
// NWPath need not report a new interface when the SSID changes.
|
|
210
|
+
guard allowWifiAfterRelease, path.status == .satisfied,
|
|
211
|
+
path.usesInterfaceType(.wifi), observation.beginSSIDRead() else { return }
|
|
212
|
+
let pathRevision = currentPathRevision
|
|
213
|
+
let observed: (String?) -> Void = { ssid in
|
|
214
|
+
self.queue.async {
|
|
215
|
+
observation.completeSSIDRead()
|
|
216
|
+
guard gen == self.generation else { finish(false, "Hotspot session changed"); return }
|
|
217
|
+
guard let route = observation.restoredWifiRoute(ssid: ssid, pathRevision: pathRevision, currentGeneration: self.generation)
|
|
218
|
+
else { return }
|
|
174
219
|
finish(true, route)
|
|
175
|
-
return
|
|
176
|
-
}
|
|
177
|
-
// Once the hotspot is released, a return to the user's Wi-Fi is also valid.
|
|
178
|
-
// Do not mistake the departing glasses AP's local-only path for restored internet.
|
|
179
|
-
if allowWifiAfterRelease, path.status == .satisfied, path.usesInterfaceType(.wifi) {
|
|
180
|
-
NEHotspotNetwork.fetchCurrent { network in
|
|
181
|
-
self.queue.async {
|
|
182
|
-
guard let route = HotspotInternetPolicy.route(satisfied: true, cellular: false, ethernet: false,
|
|
183
|
-
restoredWifiSSID: network?.ssid, glassesSSID: self.lastHotspotSSID)
|
|
184
|
-
else { return }
|
|
185
|
-
finish(true, route)
|
|
186
|
-
}
|
|
187
|
-
}
|
|
188
220
|
}
|
|
189
221
|
}
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
222
|
+
if ProcessInfo.processInfo.isiOSAppOnMac {
|
|
223
|
+
self.macWorker.async { observed(try? MacHotspotAssociation.observeCurrentSSID()) }
|
|
224
|
+
} else { NEHotspotNetwork.fetchCurrent { observed($0?.ssid) } }
|
|
225
|
+
}
|
|
226
|
+
monitor.pathUpdateHandler = { path in
|
|
227
|
+
guard let pathRevision = observation.beginPath(currentGeneration: self.generation) else { finish(false, "Hotspot session changed"); return }
|
|
228
|
+
currentPath = path
|
|
229
|
+
currentPathRevision = pathRevision
|
|
230
|
+
sample()
|
|
231
|
+
}
|
|
232
|
+
sampleTimer.setEventHandler(handler: sample)
|
|
233
|
+
sampleTimer.schedule(deadline: .now() + 0.5, repeating: 0.5)
|
|
234
|
+
sampleTimer.resume()
|
|
235
|
+
monitor.start(queue: queue)
|
|
236
|
+
queue.asyncAfter(deadline: .now() + 15) {
|
|
237
|
+
finish(false, allowWifiAfterRelease ? "Internet did not return after leaving the glasses hotspot" : "Cellular or Ethernet internet did not become the default route")
|
|
194
238
|
}
|
|
195
239
|
}
|
|
196
240
|
|
|
197
|
-
private func authorizeMacAssociation(ssid: String, passphrase: String, generation gen: Int) {
|
|
241
|
+
private func authorizeMacAssociation(ssid: String, passphrase: String, generation gen: Int, deadline: TimeInterval) {
|
|
198
242
|
let authorization = MacHotspotAuthorization()
|
|
199
243
|
macAuthorization = authorization
|
|
200
244
|
authorization.start { result in
|
|
@@ -202,7 +246,7 @@ public final class GlassesHotspotNetwork {
|
|
|
202
246
|
guard gen == self.generation, !self.cancelled else { return }
|
|
203
247
|
self.macAuthorization = nil
|
|
204
248
|
switch result {
|
|
205
|
-
case .success: self.applyMacAssociation(ssid: ssid, passphrase: passphrase, generation: gen)
|
|
249
|
+
case .success: self.applyMacAssociation(ssid: ssid, passphrase: passphrase, generation: gen, deadline: deadline)
|
|
206
250
|
case let .failure(error):
|
|
207
251
|
self.finishJoin(.failure(error)); self.finishLeave()
|
|
208
252
|
}
|
|
@@ -210,8 +254,8 @@ public final class GlassesHotspotNetwork {
|
|
|
210
254
|
}
|
|
211
255
|
}
|
|
212
256
|
|
|
213
|
-
private func applyMacAssociation(ssid: String, passphrase: String, generation gen: Int) {
|
|
214
|
-
let association = MacHotspotAssociation()
|
|
257
|
+
private func applyMacAssociation(ssid: String, passphrase: String, generation gen: Int, deadline: TimeInterval) {
|
|
258
|
+
let association = MacHotspotAssociation(deadline: deadline)
|
|
215
259
|
macAssociation = association
|
|
216
260
|
applying = true
|
|
217
261
|
macWorker.async {
|
|
@@ -427,9 +471,9 @@ public final class GlassesHotspotNetwork {
|
|
|
427
471
|
// Retain the owner when we cannot prove which association
|
|
428
472
|
// cleanup would disconnect. A later leave can retry safely.
|
|
429
473
|
self.finishingLeave = false
|
|
430
|
-
let
|
|
431
|
-
self.
|
|
432
|
-
|
|
474
|
+
let completion = self.leaveCompletion
|
|
475
|
+
self.leaveCompletion = nil
|
|
476
|
+
completion?.complete(.failure(error))
|
|
433
477
|
}
|
|
434
478
|
}
|
|
435
479
|
}
|
|
@@ -437,6 +481,7 @@ public final class GlassesHotspotNetwork {
|
|
|
437
481
|
}
|
|
438
482
|
|
|
439
483
|
private func completeLeave() {
|
|
484
|
+
internetObservationOwner.cancel()
|
|
440
485
|
generation += 1
|
|
441
486
|
macAuthorization?.cancel(); macAuthorization = nil
|
|
442
487
|
localAccess?.cancel(); localAccess = nil
|
|
@@ -448,12 +493,25 @@ public final class GlassesHotspotNetwork {
|
|
|
448
493
|
ssid = nil
|
|
449
494
|
binding = nil
|
|
450
495
|
gatewayAddress = nil
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
496
|
+
let finish = {
|
|
497
|
+
self.waitForInternetAfterLeave = false
|
|
498
|
+
self.finishingLeave = false
|
|
499
|
+
let completion = self.leaveCompletion
|
|
500
|
+
self.leaveCompletion = nil
|
|
501
|
+
if let completion { completion.complete(.success(())) } else {
|
|
502
|
+
if let token = self.ownershipToken { HotspotNetworkOwnership.shared.release(token) }
|
|
503
|
+
self.ownershipToken = nil
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if waitForInternetAfterLeave, ProcessInfo.processInfo.isiOSAppOnMac {
|
|
507
|
+
// Keep this owner until the default route returns or the existing
|
|
508
|
+
// bounded observation ends. Route readiness is not HTTP success;
|
|
509
|
+
// the following ordinary update check still owns that result.
|
|
510
|
+
startInternetObservation(allowWifiAfterRelease: true) { ready, reason in
|
|
511
|
+
self.logger.info("HOTSPOT_LEAVE internet_recovery ready=\(ready) reason=\(reason, privacy: .public)")
|
|
512
|
+
finish()
|
|
513
|
+
}
|
|
514
|
+
} else { finish() }
|
|
457
515
|
}
|
|
458
516
|
|
|
459
517
|
/// Every attached interface's kernel index and link type (its AF_LINK row) and every IPv4
|
|
@@ -31,10 +31,10 @@ public final class GlassesMediaRelayModule: Module {
|
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
AsyncFunction("leaveLocalHotspot") { (id: String, promise: Promise) in
|
|
34
|
+
AsyncFunction("leaveLocalHotspot") { (id: String, waitForInternet: Bool, promise: Promise) in
|
|
35
35
|
self.queue.async {
|
|
36
36
|
guard self.localHotspotOwner == id else { promise.resolve(nil); return }
|
|
37
|
-
self.localHotspot.leave { result in
|
|
37
|
+
self.localHotspot.leave(waitForInternet: waitForInternet) { result in
|
|
38
38
|
self.queue.async {
|
|
39
39
|
switch result {
|
|
40
40
|
case .success:
|