mirror of
https://github.com/MioMioOS/mio-plugin-music.git
synced 2026-06-11 03:44:31 +00:00
v2.2.2: fix UI hang on plugin install — bootstrap off main queue
Symptom: installing v2.2.1 caused Mio Island to freeze ("卡崩") on launch.
Not a true crash, just the main runloop stuck long enough to trip the
"app not responding" state.
Root cause: MediaRemoteAdapterSource.spawn() scheduled bootstrapGet()
on `DispatchQueue.main.asyncAfter(+0.3)`. bootstrapGet runs a Perl
subprocess that dlopens MediaRemoteAdapter.framework then calls
`get` — that cold path takes 500ms to 1s in the worst case. During
that entire window, `proc.waitUntilExit()` blocks the main thread.
No UI events drain, SwiftUI drops frames, window looks hung.
Fix:
- Move the `DispatchQueue.main.asyncAfter` to
`DispatchQueue.global(qos: .userInitiated).asyncAfter` so the Perl
cold path runs on a background queue.
- Since `currentInfo` is now mutated from both queues (bg in bootstrap,
main in parseLine/stream), hop the merge + onUpdate back onto main
after we parse the JSON on bg. Single writer, no data race.
- parseLine is unchanged — still runs on main via the FileHandle
readabilityHandler hop.
Verified 30s alive, debug log shows bootstrap + stream rx both
emitting correctly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
69776ecec2
commit
fbc64caec5
@ -15,9 +15,9 @@
|
|||||||
<key>CFBundlePackageType</key>
|
<key>CFBundlePackageType</key>
|
||||||
<string>BNDL</string>
|
<string>BNDL</string>
|
||||||
<key>CFBundleShortVersionString</key>
|
<key>CFBundleShortVersionString</key>
|
||||||
<string>2.2.1</string>
|
<string>2.2.2</string>
|
||||||
<key>CFBundleVersion</key>
|
<key>CFBundleVersion</key>
|
||||||
<string>10</string>
|
<string>11</string>
|
||||||
<key>NSPrincipalClass</key>
|
<key>NSPrincipalClass</key>
|
||||||
<string>MusicPlugin.MusicPlugin</string>
|
<string>MusicPlugin.MusicPlugin</string>
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
@ -211,7 +211,12 @@ final class MediaRemoteAdapterSource {
|
|||||||
// was opened; in that case the initial stream emit is null/empty,
|
// was opened; in that case the initial stream emit is null/empty,
|
||||||
// and no diff comes until something changes. A parallel `get`
|
// and no diff comes until something changes. A parallel `get`
|
||||||
// catches whatever is playing right now.
|
// catches whatever is playing right now.
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
//
|
||||||
|
// CRITICAL: runs on a BACKGROUND queue because bootstrapGet()
|
||||||
|
// calls Process.waitUntilExit() which blocks synchronously for
|
||||||
|
// 500ms-1s (Perl boot + framework load). Running that on main
|
||||||
|
// freezes the whole UI — looked like a crash/hang on launch.
|
||||||
|
DispatchQueue.global(qos: .userInitiated).asyncAfter(deadline: .now() + 0.3) { [weak self] in
|
||||||
self?.bootstrapGet()
|
self?.bootstrapGet()
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@ -220,6 +225,9 @@ final class MediaRemoteAdapterSource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Runs on a BACKGROUND queue. Parses JSON on bg, hops to main for
|
||||||
|
/// the merge + onUpdate so `currentInfo` is only ever mutated on the
|
||||||
|
/// main queue (same queue as stream's parseLine).
|
||||||
private func bootstrapGet() {
|
private func bootstrapGet() {
|
||||||
let proc = Process()
|
let proc = Process()
|
||||||
proc.executableURL = URL(fileURLWithPath: "/usr/bin/perl")
|
proc.executableURL = URL(fileURLWithPath: "/usr/bin/perl")
|
||||||
@ -236,18 +244,20 @@ final class MediaRemoteAdapterSource {
|
|||||||
debugLog("bootstrap get returned empty")
|
debugLog("bootstrap get returned empty")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// `get` emits one JSON object to stdout.
|
guard let payload = try? JSONDecoder().decode(AdapterStreamPayload.self, from: data) else {
|
||||||
if let payload = try? JSONDecoder().decode(AdapterStreamPayload.self, from: data) {
|
debugLog("bootstrap get · parse failed")
|
||||||
merge(payload)
|
return
|
||||||
debugLog("bootstrap get · title=\(currentInfo.title) playing=\(currentInfo.isPlaying)")
|
}
|
||||||
if currentInfo.hasTrack {
|
DispatchQueue.main.async { [weak self] in
|
||||||
DispatchQueue.main.async { [weak self] in
|
guard let self else { return }
|
||||||
guard let self else { return }
|
self.merge(payload)
|
||||||
self.onUpdate?(self.currentInfo)
|
self.debugLog("bootstrap get · title=\(self.currentInfo.title) playing=\(self.currentInfo.isPlaying)")
|
||||||
}
|
if self.currentInfo.hasTrack {
|
||||||
|
self.onUpdate?(self.currentInfo)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
|
// debugLog runs main-ok from any queue, just logs.
|
||||||
debugLog("bootstrap get failed: \(error)")
|
debugLog("bootstrap get failed: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user