148 lines
5.4 KiB
Swift
148 lines
5.4 KiB
Swift
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
|
||
)
|
||
}
|
||
}
|
||
}
|