diff --git a/Sources/EdgeControl/App/EdgeControlApp.swift b/Sources/EdgeControl/App/EdgeControlApp.swift index 2af62ed..531ad65 100644 --- a/Sources/EdgeControl/App/EdgeControlApp.swift +++ b/Sources/EdgeControl/App/EdgeControlApp.swift @@ -151,7 +151,9 @@ enum EdgeControlExecutable { widgetBridge.start() model.widgetDataBridge = widgetBridge - // Plugin desktop widget renderer: headless WKWebView snapshots + // Plugin desktop widget renderer: headless WKWebView snapshots. + // PluginWidgetManifest.write() now runs its disk I/O on a + // background queue so this call no longer gates start(). let pluginRenderer = PluginWidgetRenderer(pluginManager: pluginManager, model: model) pluginRenderer.start() model.pluginWidgetRenderer = pluginRenderer diff --git a/Sources/EdgeControl/Models/WidgetData.swift b/Sources/EdgeControl/Models/WidgetData.swift index a654b02..27b5107 100644 --- a/Sources/EdgeControl/Models/WidgetData.swift +++ b/Sources/EdgeControl/Models/WidgetData.swift @@ -148,10 +148,20 @@ public struct PluginWidgetManifest: Codable, Sendable { public func write() { guard let dir = Self.containerDirectory else { return } - try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) - let url = dir.appendingPathComponent("plugins.json") guard let data = try? JSONEncoder.widgetEncoder.encode(self) else { return } - try? data.write(to: url, options: .atomic) + // App-group container writes have been observed hanging the calling + // thread on __open inside Foundation's atomic-write path (sandbox / + // container-manager state we don't control). Move the entire dance + // — directory creation, freshness check, atomic write — to a + // background queue with a fire-and-forget contract so the main + // thread never blocks on this call. Worst case the manifest is a + // tick stale, which is fine for desktop widgets that already poll. + DispatchQueue.global(qos: .utility).async { + try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true) + let url = dir.appendingPathComponent("plugins.json") + if let existing = try? Data(contentsOf: url), existing == data { return } + try? data.write(to: url, options: .atomic) + } } /// Get snapshot image path for a plugin at a given size