Skip to content

Commit 8c8c3d0

Browse files
authored
ABI: Move tunnel hooks to native app (#1820)
Passing a native tunnel object to the Swift ABI is trivial in Swift, but it gets convoluted with non-Swift languages because of all the C/JNI machinery. Instead: - In AppABI.onSaveProfile(), rather than performing tunnel operations, emit a new ProfileEvent called ShouldReconnect - Observe the event in TunnelObservable, and reconnect the profile if necessary This demands trivial duplication of the native tunnel logic, as it must react to profile events at the app layer. However, this is infinitely easier than adding fragile C/JNI bindings to let Swift control the tunnel directly with Kotlin/JNI or C++.
1 parent 8f2bd4d commit 8c8c3d0

16 files changed

Lines changed: 179 additions & 80 deletions

File tree

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
*
3+
* Please note:
4+
* This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
5+
* Do not edit this file manually.
6+
*
7+
*/
8+
9+
@file:Suppress(
10+
"ArrayInDataClass",
11+
"EnumEntryName",
12+
"RemoveRedundantQualifierName",
13+
"UnusedImport"
14+
)
15+
16+
package com.algoritmico.passepartout.abi.models
17+
18+
import com.algoritmico.passepartout.abi.models.Event
19+
import io.partout.models.TaggedProfile
20+
21+
import kotlinx.serialization.Serializable
22+
import kotlinx.serialization.SerialName
23+
import kotlinx.serialization.Contextual
24+
25+
/**
26+
*
27+
*
28+
* @param profile
29+
*/
30+
@Serializable
31+
32+
@SerialName(value = "MixedEventShouldReconnect")
33+
data class MixedEventShouldReconnect (
34+
35+
@Contextual @SerialName(value = "profile")
36+
val profile: TaggedProfile
37+
38+
) : Event() {
39+
40+
41+
}
42+

app-apple/Passepartout/App/Context/AppContext+Testing.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,6 @@ extension AppContext {
8989
preferencesManager: preferencesManager,
9090
profileManager: profileManager,
9191
registry: registry,
92-
tunnelHooks: tunnelObservable,
9392
versionChecker: versionChecker,
9493
webReceiverManager: webReceiverManager,
9594
bindings: nil

app-apple/Sources/AppLibrary/Observables/AppContext.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,13 +85,17 @@ private extension AppContext {
8585
appContext.configObservable.onUpdate(event)
8686
case .iap(let event):
8787
appContext.iapObservable.onUpdate(event)
88+
case .mixed:
89+
break
8890
case .profile(let event):
8991
appContext.profileObservable.onUpdate(event)
9092
case .version(let event):
9193
appContext.versionObservable.onUpdate(event)
9294
case .webReceiver(let event):
9395
appContext.webReceiverObservable.onUpdate(event)
9496
}
97+
// Report all events to TunnelObservable
98+
appContext.tunnelObservable.onUpdate(mainEvent)
9599
}
96100
}
97101
}

app-apple/Sources/AppLibrary/Previews/AppContext+Previews.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,6 @@ extension AppContext {
7878
preferencesManager: preferencesManager,
7979
profileManager: profileManager,
8080
registry: registry,
81-
tunnelHooks: tunnelObservable,
8281
versionChecker: versionChecker,
8382
webReceiverManager: webReceiverManager,
8483
bindings: nil

app-cross/Sources/CommonLibrary/ABI/AppABI+Apple.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,7 +307,6 @@ extension AppABI {
307307
preferencesManager: preferencesManager,
308308
profileManager: profileManager,
309309
registry: registry,
310-
tunnelHooks: tunnelObservable,
311310
versionChecker: versionChecker,
312311
webReceiverManager: webReceiverManager,
313312
onEligibleFeaturesBlock: onEligibleFeaturesBlock,

app-cross/Sources/CommonLibrary/ABI/AppABI+Cross.swift

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,6 @@ extension AppABI {
5959
let profileRepository = try appConfiguration.newFileProfileRepository(path: profilesDir)
6060
let profileManager = ProfileManager(repository: profileRepository)
6161

62-
// FIXME: #1816, Control the tunnel from the ABI
63-
let tunnelHooks = NativeTunnelHooks()
64-
6562
// Dummy
6663
let iapManager = IAPManager()
6764
let versionChecker = VersionChecker()
@@ -79,7 +76,6 @@ extension AppABI {
7976
preferencesManager: nil,
8077
profileManager: profileManager,
8178
registry: registry,
82-
tunnelHooks: tunnelHooks,
8379
versionChecker: versionChecker,
8480
webReceiverManager: webReceiverManager,
8581
bindings: bindings

app-cross/Sources/CommonLibrary/ABI/AppABI.swift

Lines changed: 6 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ public final class AppABI: Sendable {
3131
private let iapManager: IAPManager
3232
private let logFormatter: LogFormatter
3333
private let profileManager: ProfileManager
34-
private let tunnelHooks: TunnelHooksProtocol
3534
private let versionChecker: VersionChecker
3635
private let webReceiverManager: WebReceiverManager
3736
// Purchases handler
@@ -57,7 +56,6 @@ public final class AppABI: Sendable {
5756
preferencesManager: PreferencesManager?,
5857
profileManager: ProfileManager,
5958
registry partoutRegistry: CodingRegistry,
60-
tunnelHooks: TunnelHooksProtocol,
6159
versionChecker: VersionChecker,
6260
webReceiverManager: WebReceiverManager,
6361
onEligibleFeaturesBlock: (@Sendable (Set<ABI.AppFeature>) async -> Void)? = nil,
@@ -70,7 +68,6 @@ public final class AppABI: Sendable {
7068
self.kvStore = kvStore
7169
self.logFormatter = logFormatter
7270
self.profileManager = profileManager
73-
self.tunnelHooks = tunnelHooks
7471
self.versionChecker = versionChecker
7572
self.webReceiverManager = webReceiverManager
7673
self.onEligibleFeaturesBlock = onEligibleFeaturesBlock
@@ -173,7 +170,7 @@ extension AppABI {
173170
}
174171

175172
func dispatch(_ event: ABI.Event, _ handler: ABI.EventHandler?) {
176-
guard let handler else { return }
173+
guard let handler = handler ?? self.handler else { return }
177174
handler.callback(handler.context, event)
178175
}
179176
}
@@ -454,12 +451,12 @@ private extension AppABI {
454451
}
455452

456453
func onForeground() async throws {
457-
458454
// onForeground() is redundant after launch
459455
let didLaunch = try await waitForTasks()
460456
guard !didLaunch else {
461457
return
462458
}
459+
assert(pendingTask == nil)
463460

464461
pspLog(.core, .notice, "Application did enter foreground")
465462
pendingTask = Task {
@@ -477,6 +474,7 @@ private extension AppABI {
477474

478475
func onEligibleFeatures(_ features: Set<ABI.AppFeature>) async throws {
479476
try await waitForTasks()
477+
assert(pendingTask == nil)
480478

481479
pspLog(.core, .notice, "Application did update eligible features")
482480
pendingTask = Task {
@@ -488,6 +486,7 @@ private extension AppABI {
488486

489487
func onSaveProfile(_ profile: Profile, previous: Profile?) async throws {
490488
try await waitForTasks()
489+
assert(pendingTask == nil)
491490

492491
pspLog(.core, .notice, "Application did save profile (\(profile.id))")
493492
guard let previous else {
@@ -499,33 +498,9 @@ private extension AppABI {
499498
pspLog(.core, .debug, "\tProfile \(profile.id) changes are not relevant, do nothing")
500499
return
501500
}
502-
guard tunnelHooks.isActiveProfile(withId: profile.id) else {
503-
pspLog(.core, .debug, "\tProfile \(profile.id) is not active, do nothing")
504-
return
505-
}
506-
let status = tunnelHooks.tunnelStatus(for: profile.id)
507-
guard [.active, .activating].contains(status) else {
508-
pspLog(.core, .debug, "\tConnection is not active (\(status)), do nothing")
509-
return
510-
}
511501

512-
pendingTask = Task {
513-
do {
514-
pspLog(.core, .info, "\tReconnect profile \(profile.id)")
515-
try await tunnelHooks.disconnect(from: profile.id)
516-
do {
517-
try await tunnelHooks.connect(to: profile)
518-
} catch ABI.AppError.interactiveLogin {
519-
pspLog(.core, .info, "\tProfile \(profile.id) is interactive, do not reconnect")
520-
} catch {
521-
pspLog(.core, .error, "\tUnable to reconnect profile \(profile.id): \(error)")
522-
}
523-
} catch {
524-
pspLog(.core, .error, "\tUnable to reinstate connection on save profile \(profile.id): \(error)")
525-
}
526-
}
527-
await pendingTask?.value
528-
pendingTask = nil
502+
// Suggest tunnel reconnection (may or may not happen)
503+
dispatch(.mixed(.shouldReconnect(.init(profile: profile))), nil)
529504
}
530505

531506
func onWebUpload(_ upload: ABI.WebFileUpload) async throws {

app-cross/Sources/CommonLibrary/Dependencies/NativeTunnelHooks.swift

Lines changed: 0 additions & 25 deletions
This file was deleted.

app-cross/Sources/CommonLibraryApple/TunnelObservable.swift

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
@preconcurrency import Partout
66

77
@MainActor @Observable
8-
public final class TunnelObservable: TunnelHooksProtocol {
8+
public final class TunnelObservable {
99
public struct Logging {
1010
public let maxDebugLogLevel: DebugLog.Level
1111
public let sinceLast: Double
@@ -38,6 +38,8 @@ public final class TunnelObservable: TunnelHooksProtocol {
3838

3939
private var subscriptions: [Task<Void, Never>]
4040

41+
private var pendingTasks: [Profile.ID: PendingTask]
42+
4143
// TODO: #218, keep "last used profile" until .multiple
4244
public init(
4345
tunnel: Tunnel,
@@ -51,7 +53,9 @@ public final class TunnelObservable: TunnelHooksProtocol {
5153
self.willInstall = willInstall
5254
activeProfiles = [:]
5355
subscriptions = []
56+
pendingTasks = [:]
5457
}
58+
5559
}
5660

5761
// MARK: - Actions
@@ -180,6 +184,56 @@ extension TunnelObservable {
180184
}
181185
subscriptions = [tunnelSubscription]
182186
}
187+
188+
public func onUpdate(_ event: ABI.Event) {
189+
guard case .mixed(let mixedEvent) = event else { return }
190+
guard case .shouldReconnect(let reconnectEvent) = mixedEvent else { return }
191+
let profile = reconnectEvent.profile
192+
193+
let status = tunnelStatus(for: profile.id)
194+
guard [.active, .activating].contains(status) else {
195+
pspLog(.core, .debug, "\tConnection is not active (\(status)), do nothing")
196+
return
197+
}
198+
199+
pendingTasks[profile.id]?.cancel()
200+
let pendingTask = PendingTask()
201+
pendingTasks[profile.id] = pendingTask
202+
pendingTask.task = Task { [weak self, weak pendingTask] in
203+
guard let self else { return }
204+
defer {
205+
if pendingTasks[profile.id] === pendingTask {
206+
pendingTasks[profile.id] = nil
207+
}
208+
}
209+
do {
210+
pspLog(.core, .info, "\tReconnect profile \(profile.id)")
211+
try await disconnect(from: profile.id)
212+
guard !Task.isCancelled else { return }
213+
do {
214+
try await connect(to: profile)
215+
} catch ABI.AppError.interactiveLogin {
216+
pspLog(.core, .info, "\tProfile \(profile.id) is interactive, do not reconnect")
217+
} catch is CancellationError {
218+
return
219+
} catch {
220+
pspLog(.core, .error, "\tUnable to reconnect profile \(profile.id): \(error)")
221+
}
222+
} catch is CancellationError {
223+
return
224+
} catch {
225+
pspLog(.core, .error, "\tUnable to reinstate connection on save profile \(profile.id): \(error)")
226+
}
227+
}
228+
}
229+
}
230+
231+
private final class PendingTask {
232+
var task: Task<Void, Never>?
233+
234+
func cancel() {
235+
task?.cancel()
236+
}
183237
}
184238

185239
// MARK: - Internal state
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
//
2+
// OpenAPIMixedEventShouldReconnect.swift
3+
//
4+
// Generated by openapi-generator
5+
// https://openapi-generator.tech
6+
//
7+
8+
import Partout
9+
10+
public struct OpenAPIMixedEventShouldReconnect: Sendable, Codable, Hashable {
11+
12+
public enum OpenAPIType: String, Sendable, Codable, CaseIterable {
13+
case mixedEventShouldReconnect = "MixedEventShouldReconnect"
14+
}
15+
public let type: OpenAPIType = .mixedEventShouldReconnect
16+
public var profile: TaggedProfile
17+
18+
public init(profile: TaggedProfile) {
19+
self.profile = profile
20+
}
21+
22+
public enum CodingKeys: String, CodingKey, CaseIterable {
23+
case type
24+
case profile
25+
}
26+
27+
// Encodable protocol methods
28+
29+
public func encode(to encoder: Encoder) throws {
30+
var container = encoder.container(keyedBy: CodingKeys.self)
31+
try container.encode(type, forKey: .type)
32+
try container.encode(profile, forKey: .profile)
33+
}
34+
}
35+

0 commit comments

Comments
 (0)