项目文件夹

文件
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 行
5.4 KiB
Swift

此文件含有模棱两可的 Unicode 字符
此文件含有可能会与其他字符混淆的 Unicode 字符。 如果您是想特意这样的,可以安全地忽略该警告。 使用 Escape 按钮显示他们。
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
)
}
}
}