Skip to content

Commit d5aae7f

Browse files
authored
refactor(plugins): delete dead plugin manager code (#3482)
1 parent 436ac0e commit d5aae7f

19 files changed

Lines changed: 197 additions & 788 deletions

‎.swiftlint.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,7 @@ custom_rules:
558558
plugin_principal_class_gate:
559559
name: "Principal Class Behind The Gate"
560560
included: ['.*/TablePro/Core/Plugins/[^/]+\.swift$']
561-
regex: '\bfunc\s+(?!(?:registerBundle|activateLazyBundle|validateDependencies)\b)\w+(?:(?!\bfunc\b)[\s\S])*?(\.principalClass\b)'
561+
regex: '\bfunc\s+(?!(?:registerBundle|activateLazyBundle)\b)\w+(?:(?!\bfunc\b)[\s\S])*?(\.principalClass\b)'
562562
capture_group: 1
563563
message: "Bundle.principalClass loads the executable. Read it only in a function that already called assertLoadable."
564564
severity: error

‎TablePro/Core/Plugins/PluginInstaller.swift‎

Lines changed: 69 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,3 @@
1-
//
2-
// PluginInstaller.swift
3-
// TablePro
4-
//
5-
6-
import CryptoKit
71
import Foundation
82
import os
93
import TableProUtilities
@@ -22,14 +16,14 @@ actor PluginInstaller {
2216
_ registryPlugin: RegistryPlugin,
2317
binary: RegistryBinary,
2418
into userPluginsDir: URL,
25-
progressHandler: @escaping @Sendable (StagedInstallState) async -> Void
19+
progress: @escaping @Sendable (Double) async -> Void
2620
) async throws -> URL {
2721
try await runCoalesced(pluginId: registryPlugin.id) {
2822
try await self.performDownloadAndCommit(
2923
registryPlugin,
3024
binary: binary,
3125
into: userPluginsDir,
32-
progressHandler: progressHandler
26+
progress: progress
3327
)
3428
}
3529
}
@@ -39,19 +33,18 @@ actor PluginInstaller {
3933
binary: RegistryBinary,
4034
into userPluginsDir: URL,
4135
hasLiveConnections: Bool,
42-
progressHandler: @escaping @Sendable (StagedInstallState) async -> Void
36+
progress: @escaping @Sendable (Double) async -> Void
4337
) async throws -> PluginUpdateResult {
4438
if hasLiveConnections {
4539
let stagedURL = try await runCoalesced(pluginId: registryPlugin.id) {
4640
try await self.performDownloadAndStage(
4741
registryPlugin,
4842
binary: binary,
4943
into: userPluginsDir,
50-
progressHandler: progressHandler
44+
progress: progress
5145
)
5246
}
5347
stagedUpdates[registryPlugin.id] = stagedURL
54-
await progressHandler(.staged(at: stagedURL))
5548
return .staged(at: stagedURL)
5649
}
5750

@@ -60,7 +53,7 @@ actor PluginInstaller {
6053
registryPlugin,
6154
binary: binary,
6255
into: userPluginsDir,
63-
progressHandler: progressHandler
56+
progress: progress
6457
)
6558
}
6659
return .installed(pluginURL: finalURL)
@@ -122,23 +115,21 @@ actor PluginInstaller {
122115
_ registryPlugin: RegistryPlugin,
123116
binary: RegistryBinary,
124117
into userPluginsDir: URL,
125-
progressHandler: @escaping @Sendable (StagedInstallState) async -> Void
118+
progress: @escaping @Sendable (Double) async -> Void
126119
) async throws -> URL {
127120
let extracted = try await downloadExtractVerify(
128121
registryPlugin,
129122
binary: binary,
130123
into: userPluginsDir,
131-
progressHandler: progressHandler
124+
progress: progress
132125
)
133126

134127
defer {
135128
try? FileManager.default.removeItem(at: extracted.workingDir)
136129
}
137130

138131
let destURL = userPluginsDir.appendingPathComponent(extracted.bundleURL.lastPathComponent, isDirectory: true)
139-
let finalURL = try Self.atomicReplace(stagedBundleURL: extracted.bundleURL, destURL: destURL)
140-
await progressHandler(.installed(pluginURL: finalURL))
141-
return finalURL
132+
return try Self.atomicReplace(stagedBundleURL: extracted.bundleURL, destURL: destURL)
142133
}
143134

144135
// MARK: - Download + stage (live connections)
@@ -147,13 +138,13 @@ actor PluginInstaller {
147138
_ registryPlugin: RegistryPlugin,
148139
binary: RegistryBinary,
149140
into userPluginsDir: URL,
150-
progressHandler: @escaping @Sendable (StagedInstallState) async -> Void
141+
progress: @escaping @Sendable (Double) async -> Void
151142
) async throws -> URL {
152143
let extracted = try await downloadExtractVerify(
153144
registryPlugin,
154145
binary: binary,
155146
into: userPluginsDir,
156-
progressHandler: progressHandler
147+
progress: progress
157148
)
158149
return extracted.bundleURL
159150
}
@@ -169,20 +160,14 @@ actor PluginInstaller {
169160
_ registryPlugin: RegistryPlugin,
170161
binary: RegistryBinary,
171162
into userPluginsDir: URL,
172-
progressHandler: @escaping @Sendable (StagedInstallState) async -> Void
163+
progress: @escaping @Sendable (Double) async -> Void
173164
) async throws -> ExtractedBundle {
174165
let stagingRoot = Self.stagingRoot(for: userPluginsDir)
175166
try FileManager.default.createDirectory(at: stagingRoot, withIntermediateDirectories: true)
176167
let workingDir = stagingRoot.appendingPathComponent(UUID().uuidString, isDirectory: true)
177168
try FileManager.default.createDirectory(at: workingDir, withIntermediateDirectories: true)
178169

179-
let context = await MainActor.run {
180-
(
181-
kit: PluginManager.currentPluginKitVersion,
182-
minimumKit: PluginManager.minimumCompatiblePluginKitVersion,
183-
session: RegistryClient.shared.session
184-
)
185-
}
170+
let session = await MainActor.run { RegistryClient.shared.session }
186171

187172
// The registry manifest is fetched over the network, so the URL it names is untrusted
188173
// input. The code-signature check is the real gate, but nothing should be fetched over
@@ -192,30 +177,16 @@ actor PluginInstaller {
192177
throw PluginError.downloadFailed("Invalid download URL")
193178
}
194179

195-
/// Zero here is "not started", not "nought per cent done", and nothing else arrives until
196-
/// the whole file is down: measured on a 25.7 MB download with a known `Content-Length`,
197-
/// `download(from:delegate:)` delivered no `URLSessionDownloadDelegate` byte callbacks at
198-
/// all, because the async form routes only `URLSessionTaskDelegate` messages to a per-task
199-
/// delegate. Anything drawing this has to stay indeterminate until a fraction moves.
200-
await progressHandler(.downloading(fraction: 0))
201-
202-
let (tempDownloadURL, response) = try await context.session.download(from: downloadURL)
180+
let (tempDownloadURL, response) = try await Self.download(downloadURL, with: session, progress: progress)
203181
guard let http = response as? HTTPURLResponse, (200...299).contains(http.statusCode) else {
204182
let code = (response as? HTTPURLResponse)?.statusCode ?? 0
205183
throw PluginError.downloadFailed("HTTP \(code)")
206184
}
207185

208-
await progressHandler(.downloading(fraction: 0.5))
209-
210-
let payload = try Data(contentsOf: tempDownloadURL)
211-
let digest = SHA256.hash(data: payload)
212-
let hex = digest.hexEncoded
213-
guard hex == binary.sha256.lowercased() else {
186+
guard DownloadedBinary.sha256Hex(ofFileAt: tempDownloadURL.path) == binary.sha256.lowercased() else {
214187
throw PluginError.checksumMismatch
215188
}
216189

217-
await progressHandler(.downloading(fraction: 1.0))
218-
219190
let zipURL = workingDir.appendingPathComponent("\(registryPlugin.id).zip")
220191
try FileManager.default.moveItem(at: tempDownloadURL, to: zipURL)
221192

@@ -227,19 +198,37 @@ actor PluginInstaller {
227198
}
228199

229200
try PluginCodeSignatureVerifier.verify(bundle: stagedBundle)
230-
231-
try Self.validateStagedABI(
232-
bundleURL: bundleURL,
233-
currentKit: context.kit,
234-
minimumKit: context.minimumKit
235-
)
201+
try PluginManager.validateBundleVersions(stagedBundle)
236202
Self.stripQuarantine(at: bundleURL)
237203

238204
return ExtractedBundle(workingDir: workingDir, bundleURL: bundleURL)
239205
}
240206

241207
// MARK: - Helpers (nonisolated)
242208

209+
nonisolated static func download(
210+
_ url: URL,
211+
with session: URLSession,
212+
progress: @escaping @Sendable (Double) async -> Void
213+
) async throws -> (URL, URLResponse) {
214+
let observer = DownloadProgressObserver()
215+
let forwarding = Task {
216+
for await fraction in observer.fractions {
217+
await progress(fraction)
218+
}
219+
}
220+
do {
221+
let result = try await session.download(from: url, delegate: observer)
222+
observer.finish(isComplete: true)
223+
await forwarding.value
224+
return result
225+
} catch {
226+
observer.finish(isComplete: false)
227+
forwarding.cancel()
228+
throw error
229+
}
230+
}
231+
243232
nonisolated static func stagingRoot(for userPluginsDir: URL) -> URL {
244233
userPluginsDir.deletingLastPathComponent()
245234
.appendingPathComponent("PluginStaging", isDirectory: true)
@@ -276,27 +265,6 @@ actor PluginInstaller {
276265
DownloadedBinary.stripQuarantine(at: url)
277266
}
278267

279-
nonisolated static func validateStagedABI(
280-
bundleURL: URL,
281-
currentKit: Int,
282-
minimumKit: Int
283-
) throws {
284-
guard let bundle = Bundle(url: bundleURL),
285-
let info = bundle.infoDictionary
286-
else {
287-
throw PluginError.invalidBundle("Cannot read Info.plist")
288-
}
289-
guard let version = info["TableProPluginKitVersion"] as? Int else {
290-
throw PluginError.pluginOutdated(pluginVersion: 0, requiredVersion: currentKit)
291-
}
292-
if version > currentKit {
293-
throw PluginError.incompatibleVersion(required: version, current: currentKit)
294-
}
295-
if version < minimumKit {
296-
throw PluginError.pluginOutdated(pluginVersion: version, requiredVersion: currentKit)
297-
}
298-
}
299-
300268
nonisolated static func atomicReplace(stagedBundleURL: URL, destURL: URL) throws -> URL {
301269
var resultURL: NSURL?
302270
let backupName = "\(destURL.lastPathComponent).bak"
@@ -311,8 +279,6 @@ actor PluginInstaller {
311279
options: [],
312280
resultingItemURL: &resultURL
313281
)
314-
let backupURL = destDir.appendingPathComponent(backupName)
315-
try? FileManager.default.removeItem(at: backupURL)
316282
} else {
317283
try FileManager.default.moveItem(at: stagedBundleURL, to: destURL)
318284
}
@@ -321,11 +287,37 @@ actor PluginInstaller {
321287
}
322288
}
323289

324-
enum StagedInstallState: Sendable {
325-
case downloading(fraction: Double)
326-
case staged(at: URL)
327-
case installed(pluginURL: URL)
328-
case failed(any Error)
290+
// The async download sends a task delegate no byte callbacks, but the task's own progress moves.
291+
private final class DownloadProgressObserver: NSObject, URLSessionTaskDelegate, Sendable {
292+
let fractions: AsyncStream<Double>
293+
private let continuation: AsyncStream<Double>.Continuation
294+
private let observation = OSAllocatedUnfairLock<NSKeyValueObservation?>(uncheckedState: nil)
295+
296+
override init() {
297+
(fractions, continuation) = AsyncStream.makeStream(of: Double.self)
298+
super.init()
299+
}
300+
301+
func urlSession(_ session: URLSession, didCreateTask task: URLSessionTask) {
302+
let continuation = continuation
303+
let observation = task.progress.observe(\.fractionCompleted) { progress, _ in
304+
// Without a Content-Length the fraction creeps on a guess; the UI stays indeterminate.
305+
guard task.countOfBytesExpectedToReceive > 0 else { return }
306+
continuation.yield(progress.fractionCompleted)
307+
}
308+
self.observation.withLockUnchecked { $0 = observation }
309+
}
310+
311+
func finish(isComplete: Bool) {
312+
observation.withLockUnchecked { observation in
313+
observation?.invalidate()
314+
observation = nil
315+
}
316+
if isComplete {
317+
continuation.yield(1)
318+
}
319+
continuation.finish()
320+
}
329321
}
330322

331323
enum PluginUpdateResult: Sendable {

‎TablePro/Core/Plugins/PluginManager+AutoUpdate.swift‎

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
//
2-
// PluginManager+AutoUpdate.swift
3-
// TablePro
4-
//
5-
61
import Combine
72
import Foundation
83
import os
@@ -101,7 +96,6 @@ extension PluginManager {
10196
do {
10297
let outcome = try await updateFromRegistry(
10398
registryPlugin,
104-
existingPluginLoaded: false,
10599
refreshManifest: false,
106100
progress: { _ in }
107101
)
@@ -271,10 +265,6 @@ extension PluginManager {
271265
rejectedPlugins.contains { $0.isOutdated && $0.providedDatabaseTypeIds.contains(typeId) }
272266
}
273267

274-
func outdatedReconcileReason(forTypeId typeId: String) -> String? {
275-
rejectedPlugins.first { $0.isOutdated && $0.providedDatabaseTypeIds.contains(typeId) }?.reason
276-
}
277-
278268
func prepareForConnecting(to type: DatabaseType, registryClient: RegistryClient = .shared) async throws {
279269
let typeId = type.pluginTypeId
280270
if driverPlugin(for: type) == nil, !hasFinishedInitialLoad {

‎TablePro/Core/Plugins/PluginManager+Install.swift‎

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,3 @@
1-
//
2-
// PluginManager+Install.swift
3-
// TablePro
4-
//
5-
61
import Foundation
72
import os
83

@@ -26,18 +21,11 @@ extension PluginManager {
2621
let registryPlugin = await registryClient.refreshedPlugin(matching: registryPlugin)
2722
let binary = try validateRegistryCompatibility(registryPlugin)
2823

29-
let userPluginsDir = self.userPluginsDir
30-
let stateHandler: @Sendable (StagedInstallState) async -> Void = { state in
31-
if case .downloading(let fraction) = state {
32-
await MainActor.run { progress(fraction) }
33-
}
34-
}
35-
3624
let finalURL = try await PluginInstaller.shared.install(
3725
registryPlugin,
3826
binary: binary,
3927
into: userPluginsDir,
40-
progressHandler: stateHandler
28+
progress: { fraction in await progress(fraction) }
4129
)
4230

4331
saveRegistryMetadata(pluginId: registryPlugin.id, pluginURL: finalURL)
@@ -48,7 +36,6 @@ extension PluginManager {
4836

4937
func updateFromRegistry(
5038
_ registryPlugin: RegistryPlugin,
51-
existingPluginLoaded: Bool = true,
5239
refreshManifest: Bool = true,
5340
progress: @escaping @MainActor @Sendable (Double) -> Void
5441
) async throws -> PluginUpdateOutcome {
@@ -70,20 +57,12 @@ extension PluginManager {
7057
: registryPlugin
7158
let binary = try validateRegistryCompatibility(registryPlugin)
7259

73-
let hasLive = pluginHasLiveConnections(registryPlugin)
74-
let userPluginsDir = self.userPluginsDir
75-
let stateHandler: @Sendable (StagedInstallState) async -> Void = { state in
76-
if case .downloading(let fraction) = state {
77-
await MainActor.run { progress(fraction) }
78-
}
79-
}
80-
8160
let result = try await PluginInstaller.shared.update(
8261
registryPlugin,
8362
binary: binary,
8463
into: userPluginsDir,
85-
hasLiveConnections: hasLive,
86-
progressHandler: stateHandler
64+
hasLiveConnections: pluginHasLiveConnections(registryPlugin),
65+
progress: { fraction in await progress(fraction) }
8766
)
8867

8968
switch result {
@@ -224,11 +203,7 @@ extension PluginManager {
224203
throw PluginError.invalidBundle("Cannot create bundle from \(bundleURL.lastPathComponent)")
225204
}
226205
try PluginCodeSignatureVerifier.verify(bundle: bundle)
227-
try PluginInstaller.validateStagedABI(
228-
bundleURL: bundleURL,
229-
currentKit: Self.currentPluginKitVersion,
230-
minimumKit: Self.minimumCompatiblePluginKitVersion
231-
)
206+
try Self.validateBundleVersions(bundle)
232207
PluginInstaller.stripQuarantine(at: bundleURL)
233208

234209
let bundleId = bundle.bundleIdentifier ?? bundleURL.lastPathComponent

0 commit comments

Comments
 (0)