mirror of
https://github.com/kyleneideck/BackgroundMusic
synced 2024-11-23 04:33:03 +00:00
Fix possible deadlock when starting IO.
BGM_Device::StartIO was holding the state mutex longer than it needed to, which meant HasProperty, GetProperty, etc. couldn't return. If BGMPlayThrough was notified about IO starting after StartIO locked the mutex, BGMPlayThrough would get stuck trying to get one of BGMDevice's properties. Fixes #46.
This commit is contained in:
parent
960fe0d28d
commit
b58ad2a1f8
4 changed files with 89 additions and 77 deletions
|
@ -459,41 +459,32 @@ OSStatus BGMPlayThrough::BGMDeviceListenerProc(AudioObjectID inObjectID,
|
|||
{
|
||||
DebugMsg("BGMPlayThrough::BGMDeviceListenerProc: Got kAudioDevicePropertyDeviceIsRunning notification");
|
||||
|
||||
auto deviceIsRunningHandler = [refCon] {
|
||||
// IsRunning doesn't always return true when IO is starting. Not sure why. But using
|
||||
// RunningSomewhereOtherThanBGMApp instead seems to be working so far.
|
||||
//
|
||||
//if(refCon->mInputDevice->IsRunning())
|
||||
if(RunningSomewhereOtherThanBGMApp(refCon->mInputDevice))
|
||||
// This is dispatched because it can block and
|
||||
// - we might be on a real-time thread, or
|
||||
// - BGMXPCListener::waitForOutputDeviceToStartWithReply might get called on the same thread just
|
||||
// before this and timeout waiting for this to run.
|
||||
//
|
||||
// TODO: We should find a way to do this without dispatching because dispatching isn't real-time safe.
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
if(refCon->mActive)
|
||||
{
|
||||
#if DEBUG
|
||||
refCon->mToldOutputDeviceToStartAt = mach_absolute_time();
|
||||
#endif
|
||||
refCon->Start();
|
||||
}
|
||||
};
|
||||
|
||||
CAMutex::Tryer stateTrier(refCon->mStateMutex);
|
||||
if(stateTrier.HasLock())
|
||||
{
|
||||
// In the vast majority of cases (when we actually start playthrough here) we get the state lock
|
||||
// and can invoke the handler directly
|
||||
deviceIsRunningHandler();
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO: This should be rare, but we still shouldn't dispatch on the IO thread because it isn't
|
||||
// real-time safe.
|
||||
dispatch_async(dispatch_get_global_queue(QOS_CLASS_USER_INTERACTIVE, 0), ^{
|
||||
if(refCon->mActive)
|
||||
DebugMsg("BGMPlayThrough::BGMDeviceListenerProc: Handling "
|
||||
"kAudioDevicePropertyDeviceIsRunning notification in dispatched block");
|
||||
CAMutex::Locker stateLocker(refCon->mStateMutex);
|
||||
|
||||
// IsRunning doesn't always return true when IO is starting. Not sure why. But using
|
||||
// RunningSomewhereOtherThanBGMApp instead seems to be working so far.
|
||||
//
|
||||
//if(refCon->mInputDevice->IsRunning())
|
||||
if(RunningSomewhereOtherThanBGMApp(refCon->mInputDevice))
|
||||
{
|
||||
DebugMsg("BGMPlayThrough::BGMDeviceListenerProc: Handling "
|
||||
"kAudioDevicePropertyDeviceIsRunning notification in dispatched block");
|
||||
CAMutex::Locker stateLocker(refCon->mStateMutex);
|
||||
deviceIsRunningHandler();
|
||||
#if DEBUG
|
||||
refCon->mToldOutputDeviceToStartAt = mach_absolute_time();
|
||||
#endif
|
||||
refCon->Start();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
|
|
|
@ -175,8 +175,22 @@
|
|||
}
|
||||
|
||||
- (void) waitForOutputDeviceToStartWithReply:(void (^)(NSError*))reply {
|
||||
OSStatus err = [audioDevices waitForOutputDeviceToStart];
|
||||
NSString* description;
|
||||
OSStatus err;
|
||||
|
||||
try {
|
||||
err = [audioDevices waitForOutputDeviceToStart];
|
||||
} catch (CAException e) {
|
||||
DebugMsg("BGMXPCListener::waitForOutputDeviceToStartWithReply: Caught CAException (%d). Replying kBGMXPC_HardwareError.",
|
||||
e.GetError());
|
||||
err = kBGMXPC_HardwareError;
|
||||
} catch (...) {
|
||||
DebugMsg("BGMXPCListener::waitForOutputDeviceToStartWithReply: Caught unknown exception. Replying kBGMXPC_InternalError.");
|
||||
err = kBGMXPC_InternalError;
|
||||
#if DEBUG
|
||||
throw;
|
||||
#endif
|
||||
}
|
||||
|
||||
switch (err) {
|
||||
case noErr:
|
||||
|
|
|
@ -1821,51 +1821,58 @@ void BGM_Device::Control_SetPropertyData(AudioObjectID inObjectID, pid_t inClien
|
|||
|
||||
void BGM_Device::StartIO(UInt32 inClientID)
|
||||
{
|
||||
CAMutex::Locker theStateLocker(mStateMutex);
|
||||
bool clientIsBGMApp, bgmAppHasClientRegistered;
|
||||
|
||||
// An overview of the process this function is part of:
|
||||
// - A client starts IO.
|
||||
// - The plugin host (the HAL) calls the StartIO function in BGM_PluginInterface, which calls this function.
|
||||
// - BGMDriver sends a message to BGMApp telling it to start the (real) audio hardware.
|
||||
// - BGMApp starts the hardware and, after the hardware is ready, replies to BGMDriver's message.
|
||||
// - BGMDriver lets the host know that it's ready to do IO by returning from StartIO.
|
||||
|
||||
// Update our client data.
|
||||
//
|
||||
// We add the work to the task queue, rather than doing it here, because BeginIOOperation and EndIOOperation also
|
||||
// add this task to the queue and the updates should be done in order.
|
||||
bool didStartIO = mTaskQueue.QueueSync_StartClientIO(&mClients, inClientID);
|
||||
|
||||
// We only tell the hardware to start if this is the first time IO has been started
|
||||
if(didStartIO)
|
||||
{
|
||||
kern_return_t theError = _HW_StartIO();
|
||||
ThrowIfKernelError(theError,
|
||||
CAException(theError),
|
||||
"BGM_Device::StartIO: Failed to start because of an error calling down to the driver.");
|
||||
{
|
||||
CAMutex::Locker theStateLocker(mStateMutex);
|
||||
|
||||
// We only return from StartIO after BGMApp is ready to pass the audio through to the output device. That way
|
||||
// the HAL doesn't start sending us data before BGMApp can play it, which would mean we'd have to either drop
|
||||
// frames or increase latency.
|
||||
if(!mClients.IsBGMApp(inClientID) && mClients.BGMAppHasClientRegistered())
|
||||
// An overview of the process this function is part of:
|
||||
// - A client starts IO.
|
||||
// - The plugin host (the HAL) calls the StartIO function in BGM_PluginInterface, which calls this function.
|
||||
// - BGMDriver sends a message to BGMApp telling it to start the (real) audio hardware.
|
||||
// - BGMApp starts the hardware and, after the hardware is ready, replies to BGMDriver's message.
|
||||
// - BGMDriver lets the host know that it's ready to do IO by returning from StartIO.
|
||||
|
||||
// Update our client data.
|
||||
//
|
||||
// We add the work to the task queue, rather than doing it here, because BeginIOOperation and EndIOOperation
|
||||
// also add this task to the queue and the updates should be done in order.
|
||||
bool didStartIO = mTaskQueue.QueueSync_StartClientIO(&mClients, inClientID);
|
||||
|
||||
// We only tell the hardware to start if this is the first time IO has been started.
|
||||
if(didStartIO)
|
||||
{
|
||||
UInt64 theXPCError = WaitForBGMAppToStartOutputDevice();
|
||||
|
||||
if(theXPCError == kBGMXPC_Success)
|
||||
{
|
||||
DebugMsg("BGM_Device::StartIO: Ready for IO.");
|
||||
}
|
||||
else if(theXPCError == kBGMXPC_MessageFailure)
|
||||
{
|
||||
// This most likely means BGMXPCHelper isn't installed or has crashed. IO will probably still work,
|
||||
// but we may drop frames while the audio hardware starts up.
|
||||
DebugMsg("BGM_Device::StartIO: Couldn't reach BGMApp via XPC. Attempting to start IO anyway.");
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugMsg("BGM_Device::StartIO: BGMApp failed to start the output device. theXPCError=%llu", theXPCError);
|
||||
Throw(CAException(kAudioHardwareUnspecifiedError));
|
||||
}
|
||||
kern_return_t theError = _HW_StartIO();
|
||||
ThrowIfKernelError(theError,
|
||||
CAException(theError),
|
||||
"BGM_Device::StartIO: Failed to start because of an error calling down to the driver.");
|
||||
}
|
||||
|
||||
clientIsBGMApp = mClients.IsBGMApp(inClientID);
|
||||
bgmAppHasClientRegistered = mClients.BGMAppHasClientRegistered();
|
||||
}
|
||||
|
||||
// We only return from StartIO after BGMApp is ready to pass the audio through to the output device. That way
|
||||
// the HAL doesn't start sending us data before BGMApp can play it, which would mean we'd have to either drop
|
||||
// frames or increase latency.
|
||||
if(!clientIsBGMApp && bgmAppHasClientRegistered)
|
||||
{
|
||||
UInt64 theXPCError = WaitForBGMAppToStartOutputDevice();
|
||||
|
||||
if(theXPCError == kBGMXPC_Success)
|
||||
{
|
||||
DebugMsg("BGM_Device::StartIO: Ready for IO.");
|
||||
}
|
||||
else if(theXPCError == kBGMXPC_MessageFailure)
|
||||
{
|
||||
// This most likely means BGMXPCHelper isn't installed or has crashed. IO will probably still work,
|
||||
// but we may drop frames while the audio hardware starts up.
|
||||
DebugMsg("BGM_Device::StartIO: Couldn't reach BGMApp via XPC. Attempting to start IO anyway.");
|
||||
}
|
||||
else
|
||||
{
|
||||
DebugMsg("BGM_Device::StartIO: BGMApp failed to start the output device. theXPCError=%llu", theXPCError);
|
||||
Throw(CAException(kAudioHardwareUnspecifiedError));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,8 +84,8 @@ UInt64 WaitForBGMAppToStartOutputDevice()
|
|||
theConnection.interruptionHandler = failureHandler;
|
||||
theConnection.invalidationHandler = failureHandler;
|
||||
|
||||
// This remote call to BGMXPCHelper will send a reply when the output device is ready to receive IO. Note that we shouldn't trust
|
||||
// the reply string.
|
||||
// This remote call to BGMXPCHelper will send a reply when the output device is ready to receive IO. Note that, for security
|
||||
// reasons, we shouldn't trust the reply object.
|
||||
[[theConnection remoteObjectProxyWithErrorHandler:^(NSError* error) {
|
||||
#if !DEBUG
|
||||
#pragma unused (error)
|
||||
|
|
Loading…
Reference in a new issue