@@ -15,6 +15,8 @@ final class PluginManager {
1515
1616 private( set) var plugins : [ PluginEntry ] = [ ]
1717
18+ private( set) var isInstalling = false
19+
1820 private( set) var needsRestart = false
1921
2022 private( set) var driverPlugins : [ String : any DriverPlugin ] = [ : ]
@@ -182,6 +184,14 @@ final class PluginManager {
182184 }
183185 }
184186
187+ private func replaceExistingPlugin( bundleId: String ) {
188+ guard let existingIndex = plugins. firstIndex ( where: { $0. id == bundleId } ) else { return }
189+ // Order matters: unregisterCapabilities reads from `plugins` to find the principal class
190+ unregisterCapabilities ( pluginId: bundleId)
191+ plugins [ existingIndex] . bundle. unload ( )
192+ plugins. remove ( at: existingIndex)
193+ }
194+
185195 private func unregisterCapabilities( pluginId: String ) {
186196 driverPlugins = driverPlugins. filter { _, value in
187197 guard let entry = plugins. first ( where: { $0. id == pluginId } ) else { return true }
@@ -232,14 +242,20 @@ final class PluginManager {
232242 // MARK: - Install / Uninstall
233243
234244 func installPlugin( from url: URL ) async throws -> PluginEntry {
245+ guard !isInstalling else {
246+ throw PluginError . installFailed ( " Another plugin installation is already in progress " )
247+ }
248+ isInstalling = true
249+ defer { isInstalling = false }
250+
235251 if url. pathExtension == " tableplugin " {
236- return try await installBundle ( from: url)
252+ return try installBundle ( from: url)
237253 } else {
238254 return try await installFromZip ( from: url)
239255 }
240256 }
241257
242- private func installBundle( from url: URL ) async throws -> PluginEntry {
258+ private func installBundle( from url: URL ) throws -> PluginEntry {
243259 guard let sourceBundle = Bundle ( url: url) else {
244260 throw PluginError . invalidBundle ( " Cannot create bundle from \( url. lastPathComponent) " )
245261 }
@@ -251,19 +267,17 @@ final class PluginManager {
251267 throw PluginError . pluginConflict ( existingName: existing. name)
252268 }
253269
254- if let existingIndex = plugins. firstIndex ( where: { $0. id == newBundleId } ) {
255- unregisterCapabilities ( pluginId: newBundleId)
256- plugins [ existingIndex] . bundle. unload ( )
257- plugins. remove ( at: existingIndex)
258- }
270+ replaceExistingPlugin ( bundleId: newBundleId)
259271
260272 let fm = FileManager . default
261273 let destURL = userPluginsDir. appendingPathComponent ( url. lastPathComponent)
262274
263- if fm. fileExists ( atPath: destURL. path) {
264- try fm. removeItem ( at: destURL)
275+ if url. standardizedFileURL != destURL. standardizedFileURL {
276+ if fm. fileExists ( atPath: destURL. path) {
277+ try fm. removeItem ( at: destURL)
278+ }
279+ try fm. copyItem ( at: url, to: destURL)
265280 }
266- try fm. copyItem ( at: url, to: destURL)
267281
268282 let entry = try loadPlugin ( at: destURL, source: . userInstalled)
269283
@@ -321,6 +335,8 @@ final class PluginManager {
321335 throw PluginError . pluginConflict ( existingName: existing. name)
322336 }
323337
338+ replaceExistingPlugin ( bundleId: newBundleId)
339+
324340 let destURL = userPluginsDir. appendingPathComponent ( extracted. lastPathComponent)
325341
326342 if fm. fileExists ( atPath: destURL. path) {
0 commit comments