Files
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

148 lines
5.4 KiB
Swift
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
import MacToolsPluginKit
struct DynamicPluginLoadResult {
let record: PluginPackageRecord
let plugins: [any MacToolsPlugin]
let errorMessage: String?
}
@MainActor
protocol DynamicPluginLoading {
func loadInstalledPlugins(from records: [PluginPackageRecord]) -> [DynamicPluginLoadResult]
}
@MainActor
final class DynamicPluginLoader: DynamicPluginLoading {
private let packageStore: PluginPackageStore
private let trustValidator: PluginTrustValidating
init(
packageStore: PluginPackageStore,
trustValidator: PluginTrustValidating = SameTeamPluginTrustValidator()
) {
self.packageStore = packageStore
self.trustValidator = trustValidator
}
func loadInstalledPlugins(from records: [PluginPackageRecord]) -> [DynamicPluginLoadResult] {
records.map { record in
guard case .enabled = record.state else {
return DynamicPluginLoadResult(record: record, plugins: [], errorMessage: nil)
}
do {
let provider = try PluginInvocationGuard
.value(operation: "load provider for \(record.id)") {
try loadProvider(for: record)
}
.get()
let context = packageStore.runtimeContext(for: record)
let plugins = try PluginInvocationGuard
.value(operation: "make plugins for \(record.id)") {
provider.makePlugins()
}
.get()
try Self.validateLoadedPlugins(plugins, for: record)
for plugin in plugins {
try PluginInvocationGuard
.run(operation: "activate plugin \(plugin.metadata.id)") {
plugin.activate(context: context)
}
.get()
}
return DynamicPluginLoadResult(record: record, plugins: plugins, errorMessage: nil)
} catch {
return DynamicPluginLoadResult(
record: record,
plugins: [],
errorMessage: error.localizedDescription
)
}
}
}
private func loadProvider(for record: PluginPackageRecord) throws -> any PluginProvider {
try trustValidator.validatePluginBundle(at: record.bundleURL)
guard let bundle = Bundle(url: record.bundleURL) else {
throw DynamicPluginLoaderError.unreadableBundle(record.bundleURL)
}
guard bundle.load() else {
throw DynamicPluginLoaderError.loadFailed(record.bundleURL)
}
let context = packageStore.runtimeContext(for: record)
if let className = record.manifest.factoryClass,
let factoryClass = NSClassFromString(className) as? MacToolsPluginBundleFactory.Type {
return try factoryClass.makeProvider(context: context)
}
guard let factoryClass = bundle.principalClass as? MacToolsPluginBundleFactory.Type else {
throw DynamicPluginLoaderError.missingFactory(record.manifest.displayName)
}
return try factoryClass.makeProvider(context: context)
}
static func validateLoadedPlugins(
_ plugins: [any MacToolsPlugin],
for record: PluginPackageRecord
) throws {
guard plugins.count == 1 else {
throw DynamicPluginLoaderError.invalidPluginCount(
expected: record.manifest.id,
actual: plugins.count
)
}
guard let plugin = plugins.first else {
return
}
guard plugin.metadata.id == record.manifest.id else {
throw DynamicPluginLoaderError.pluginIdentifierMismatch(
expected: record.manifest.id,
actual: plugin.metadata.id
)
}
}
}
enum DynamicPluginLoaderError: LocalizedError, Equatable {
case unreadableBundle(URL)
case loadFailed(URL)
case missingFactory(String)
case invalidPluginCount(expected: String, actual: Int)
case pluginIdentifierMismatch(expected: String, actual: String)
var errorDescription: String? {
switch self {
case let .unreadableBundle(url):
return AppL10n.pluginsFormat("plugin.error.loader.unreadableBundleFormat", defaultValue: "无法读取插件 bundle%@", url.path)
case let .loadFailed(url):
return AppL10n.pluginsFormat("plugin.error.loader.loadFailedFormat", defaultValue: "插件代码加载失败:%@", url.path)
case let .missingFactory(name):
return AppL10n.pluginsFormat("plugin.error.loader.missingFactoryFormat", defaultValue: "插件缺少入口工厂:%@", name)
case let .invalidPluginCount(expected, actual):
return AppL10n.pluginsFormat(
"plugin.error.loader.invalidPluginCountFormat",
defaultValue: "插件包 %@ 必须返回 1 个插件,实际返回 %d 个。",
expected,
actual
)
case let .pluginIdentifierMismatch(expected, actual):
return AppL10n.pluginsFormat(
"plugin.error.loader.identifierMismatchFormat",
defaultValue: "插件 ID 不匹配,manifest 为 %@,运行时代码为 %@。",
expected,
actual
)
}
}
}