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:
Kyle Neideck 2016-04-30 21:01:37 +10:00
parent 960fe0d28d
commit b58ad2a1f8
4 changed files with 89 additions and 77 deletions

View file

@ -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();
}
};
DebugMsg("BGMPlayThrough::BGMDeviceListenerProc: Handling "
"kAudioDevicePropertyDeviceIsRunning notification in dispatched block");
CAMutex::Locker stateLocker(refCon->mStateMutex);
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)
// 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;

View file

@ -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:

View file

@ -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.
{
CAMutex::Locker theStateLocker(mStateMutex);
// 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);
// 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.
// 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.");
// 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 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())
// We only tell the hardware to start if this is the first time IO has been started.
if(didStartIO)
{
UInt64 theXPCError = WaitForBGMAppToStartOutputDevice();
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.");
}
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));
}
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));
}
}
}

View file

@ -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)