Skip to content
This repository has been archived by the owner on Sep 16, 2023. It is now read-only.

Commit

Permalink
Fix NSFileHandleOperationException crash, iina#3590
Browse files Browse the repository at this point in the history
The fix changes IINA to wait for mpv to cleanly shutdown before proceeding
with application shutdown.

The commit in the pull request:
- Adds a isDestroyed semaphore to MPVController
- Changes MPVController.handleEvent to signal the semaphore once the
  mpv context has been destroyed
- Adds a waitForDestruction method to  MPVController to wait for mpv to
  be destroyed
- Adds a waitForTermination method to PlayerCore that calls
  MPVController.waitForDestruction to wait for mpv to be destroyed
- Changes AppDelegate.applicationShouldTerminate to submit a task to wait
  for all players to terminate
- Changes applicationShouldTerminate to return terminateLater to tell the
  framework to hold up application termination until told to proceed
  • Loading branch information
low-batt authored and CarterLi committed Feb 21, 2022
1 parent 293d8c1 commit e2d37e6
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 1 deletion.
25 changes: 24 additions & 1 deletion iina/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ class AppDelegate: NSObject, NSApplicationDelegate {

private var commandLineStatus = CommandLineStatus()

private lazy var terminateQueue = DispatchQueue(label: "com.colliderli.iina.terminate", qos: .userInitiated)

// Windows

lazy var openURLWindow: OpenURLWindowController = OpenURLWindowController()
Expand Down Expand Up @@ -310,7 +312,28 @@ class AppDelegate: NSObject, NSApplicationDelegate {
for pc in PlayerCore.playerCores {
pc.terminateMPV()
}
return .terminateNow
// The call to terminateMPV instructed mpv to quit, but that is happening asynchronously in the
// background. Must wait for mpv to finish terminating before allowing Cocoa to terminate the
// application. This must be done in another thread to avoid blocking the main thread.
terminateQueue.async {
// Normally mpv will quickly terminate, but we will impose a time limit to insure termination
// of the application is not blocked.
Logger.log("Waiting for mpv termination")
let timeout = DispatchTime.now() + DispatchTimeInterval.milliseconds(500)
for pc in PlayerCore.playerCores {
let result = pc.waitForTermination(timeout: timeout)
if result == .timedOut {
Logger.log("Timeout waiting for termination of player core", level: .warning)
}
}
// Tell Cocoa to proceed with termination. This has to be done on the main thread.
DispatchQueue.main.async {
Logger.log("Proceeding with termination")
NSApp.reply(toApplicationShouldTerminate: true)
}
}
// Tell Cocoa that it is ok to proceed with termination, but wait for our reply.
return .terminateLater
}

func applicationShouldHandleReopen(_ sender: NSApplication, hasVisibleWindows flag: Bool) -> Bool {
Expand Down
14 changes: 14 additions & 0 deletions iina/MPVController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ class MPVController: NSObject {
MPVProperty.idleActive: MPV_FORMAT_FLAG
]

private let isDestroyed = DispatchSemaphore(value: 0)

init(playerCore: PlayerCore) {
self.player = playerCore
super.init()
Expand Down Expand Up @@ -582,6 +584,17 @@ class MPVController: NSObject {
}
}

/// Wait until the mpv context has been destroyed.
///
/// The method `mpvQuit` **must** be called before calling this method. That method sends a quit command to mpv which will
/// execute asynchronously. This method waits until mpv emits the event `MPV_EVENT_SHUTDOWN` and the `handleEvent`
/// method destroys the mpv context.
/// - parameter timeout: The latest time to wait for the mpv context to be destroyed
/// - returns: Whether the mpv context has been destroyed or waiting timed out
func waitForDestruction(timeout: DispatchTime) -> DispatchTimeoutResult {
isDestroyed.wait(timeout: timeout)
}

// Handle the event
private func handleEvent(_ event: UnsafePointer<mpv_event>!) {
let eventId = event.pointee.event_id
Expand All @@ -596,6 +609,7 @@ class MPVController: NSObject {
} else {
mpv_destroy(mpv)
mpv = nil
isDestroyed.signal()
}

case MPV_EVENT_LOG_MESSAGE:
Expand Down
10 changes: 10 additions & 0 deletions iina/PlayerCore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,16 @@ class PlayerCore: NSObject {
isMpvTerminated = true
}

/// Wait until this player core has been terminated.
///
/// The method `terminateMPV` **must** be called before calling this method. That method calls `mpvQuit` which
/// executes asynchronously. This method waits until mpv has been destroyed.
/// - parameter timeout: The latest time to wait for this player core to terminate
/// - returns: Whether this player core has terminated or waiting timed out
func waitForTermination(timeout: DispatchTime) -> DispatchTimeoutResult {
mpv.waitForDestruction(timeout: timeout)
}

// invalidate timer
func invalidateTimer() {
self.syncPlayTimeTimer?.invalidate()
Expand Down

0 comments on commit e2d37e6

Please sign in to comment.