Problem scanning AUs from a spawned child process


#1

Hi all,

I’m having a weird issue involving two separate processes. I have 2 binaries both using Juce:
[list]
[] One scans the available AU plugins using PluginsDirectoryScanner, let’s call it “plugin lister”[/]
[] The other one is a simplistic Introjucer-generated project which just spawns the first binary when I click on a button, using File::startAsProcess[/][/list]
Although the first one wasn’t made with Introjucer, I checked that they both use different ObjC classes prefix.

My problem is the following: when I run the plugin-lister from a shell or directly from the debugger, it runs fine, but when I run it as a process from the other application, it always crashes when analyzing my Native Instruments plugins.
Note that AULab and other hosts open the plugins with no issues, and I can open them myself as well when I run the plugin-lister in standalone. If I move them away from my components directory, the plugin-lister succeeds, even when launched with startAsProcess.

For the stack lovers, here are the crash details:

Code Type:       X86 (Native)
Parent Process:  TestIntrojucer [3897]
OS Version:      Mac OS X 10.7.2 (11C74)
Exception Type:  EXC_BAD_ACCESS (SIGBUS)
Exception Codes: KERN_PROTECTION_FAILURE at 0x0000000000000000

Thread 0 Crashed:: Juce Message Thread  Dispatch queue: com.apple.main-thread
0   libsystem_c.dylib             	0x9611bd50 strlen + 16
1   Absynth 5.MusicDevice.component	0x10c65df9 NI::GP::DETAIL::Registry2KeyCF::open(NI::GP::String const&, unsigned int) + 233
2   Absynth 5.MusicDevice.component	0x10c663ef NI::GP::Registry2::tKey::open(NI::GP::String const&, unsigned int) + 31
3   Absynth 5.MusicDevice.component	0x10e08dc3 NI::SC3::Engine::createConfiguration() + 307
4   Absynth 5.MusicDevice.component	0x10e106b5 NI::SC3::Engine::init() + 149
5   Absynth 5.MusicDevice.component	0x10dbfb6d NI::PA::StaticCommunicator::connect() + 77
6   Absynth 5.MusicDevice.component	0x10dad4fe NI::PA::Lib::init(bool, bool, bool) + 142
7   Absynth 5.MusicDevice.component	0x10708a1e AbsynthAppModule::init() + 238
8   Absynth 5.MusicDevice.component	0x10c85792 NI::UIA::DETAIL::AppModuleStartup::init() + 50
9   Absynth 5.MusicDevice.component	0x10734aa9 NIAudioUnitSynthEntry + 169
10  com.apple.CoreServices.CarbonCore	0x987385f5 CallComponent + 223
11  com.apple.CoreServices.CarbonCore	0x9873863e CallComponentDispatch + 29
12  com.apple.CoreServices.CarbonCore	0x987a4898 CallComponentOpen + 43
13  com.apple.CoreServices.CarbonCore	0x987372b4 OpenAComponent + 426
14  com.apple.CoreServices.CarbonCore	0x98737334 OpenComponent + 24
15  PluginLister                  	0x0018e1dc juce::AudioUnitPluginInstance::AudioUnitPluginInstance(juce::String const&) + 508
16  PluginLister                      	0x00179564 juce::AudioUnitPluginInstance::AudioUnitPluginInstance(juce::String const&) + 36
17  PluginLister                      	0x0016f2a4 juce::AudioUnitPluginFormat::createInstanceFromDescription(juce::PluginDescription const&) + 100
18  PluginLister                      	0x0016f049 juce::AudioUnitPluginFormat::findAllTypesForFile(juce::OwnedArray<juce::PluginDescription, juce::DummyCriticalSection>&, juce::String const&) + 137
19  PluginLister                      	0x00170851 juce::KnownPluginList::scanAndAddFile(juce::String const&, bool, juce::OwnedArray<juce::PluginDescription, juce::DummyCriticalSection>&, juce::AudioPluginFormat&) + 657
20  PluginLister                      	0x005a2bb3 juce::PluginsDirectoryScanner::scanNextFile(bool)

Any idea?
I’m on the modules branch from like 2 or 3 days ago.


#2

Bazinga!

I think I finally have a trail to follow:
It seems the problem only occurs when invoking the “plugin lister” binary with a command line argument.

I made 2 very simple Introjucer binaries to illustrate. I’m cleaning up the mess and posting the code here in case someone can try to reproduce.


#3

Complete project files:
[list]
[] http://dl.dropbox.com/u/5916264/TestIntrojucer.zip[/]
[] http://dl.dropbox.com/u/5916264/PluginLister.zip[/][/list]

First, the basic “Plugin Lister”:

Main.cpp

/*
  ==============================================================================

    This file was auto-generated by the Introjucer!

    It contains the basic startup code for a Juce application.

  ==============================================================================
*/

#include "../JuceLibraryCode/JuceHeader.h"


//==============================================================================
class PluginListerApplication  : public JUCEApplication
{
public:
    //==============================================================================
    PluginListerApplication()
  : recursiveScan(true)
    {
    }

    ~PluginListerApplication()
    {
    }

  void scanPlugins(int iFormatIndex, bool iHardScan)
  {
    juce::AudioPluginFormat* paCurrentFormat = juce::AudioPluginFormatManager::getInstance()->getFormat(iFormatIndex);
    jassert(paCurrentFormat);
    
    juce::PluginDirectoryScanner aScanner(pluginList, *paCurrentFormat, searchPath,
                                          recursiveScan, juce::File::nonexistent );
    
    DBG( aScanner.getNextPluginFileThatWillBeScanned() );
    while(aScanner.scanNextFile(!iHardScan))
    {
      DBG( aScanner.getNextPluginFileThatWillBeScanned() );
      DBG( aScanner.getProgress() ); 
    }
  }
  
  void scanAllPlugins(bool iHardScan = false)
  {
    int aTotalFormats = juce::AudioPluginFormatManager::getInstance()->getNumFormats();
    for( int i = 0; i < aTotalFormats; i++ )
    {
        scanPlugins(i, iHardScan);
    }
  }
  
    //==============================================================================
    void initialise (const String& commandLine)
    {
      // Add default plugin formats if not already done
      if( !juce::AudioPluginFormatManager::getInstance()->getNumFormats() )
      {
        juce::AudioPluginFormatManager::getInstance()->addDefaultFormats();
      }
      
      scanAllPlugins();
      
      setApplicationReturnValue(0);
      systemRequestedQuit();
    }

    void shutdown()
    {
        juce::AudioPluginFormatManager::deleteInstance();
    }

    //==============================================================================
    void systemRequestedQuit()
    {
        quit();
    }

    //==============================================================================
    const String getApplicationName()
    {
        return "PluginLister";
    }

    const String getApplicationVersion()
    {
        return ProjectInfo::versionString;
    }

    bool moreThanOneInstanceAllowed()
    {
        return true;
    }

    void anotherInstanceStarted (const String& commandLine)
    {
        
    }

private:
  juce::KnownPluginList pluginList;
  juce::FileSearchPath  searchPath;
  bool recursiveScan;
  
};

//==============================================================================
// This macro generates the main() routine that starts the app.
START_JUCE_APPLICATION(PluginListerApplication)

Then, the TestIntrojucer simple UI that allows to give the path to the PluginLister binary and run it with or without command line arguments:

Main.cpp

/*
  ==============================================================================

    This file was auto-generated by the Introjucer!

    It contains the basic startup code for a Juce application.

  ==============================================================================
*/

#include "../JuceLibraryCode/JuceHeader.h"
#include "MainWindow.h"


//==============================================================================
class TestIntrojucerApplication  : public JUCEApplication
{
public:
    //==============================================================================
    TestIntrojucerApplication()
    {
    }

    ~TestIntrojucerApplication()
    {
    }

    //==============================================================================
    void initialise (const String& commandLine)
    {
        // Do your application's initialisation code here..
        mainWindow = new MainAppWindow();
    }

    void shutdown()
    {
        // Do your application's shutdown code here..
        mainWindow = 0;
    }

    //==============================================================================
    void systemRequestedQuit()
    {
        quit();
    }

    //==============================================================================
    const String getApplicationName()
    {
        return "TestIntrojucer";
    }

    const String getApplicationVersion()
    {
        return ProjectInfo::versionString;
    }

    bool moreThanOneInstanceAllowed()
    {
        return true;
    }

    void anotherInstanceStarted (const String& commandLine)
    {
        
    }

private:
    ScopedPointer <MainAppWindow> mainWindow;
};

//==============================================================================
// This macro generates the main() routine that starts the app.
START_JUCE_APPLICATION(TestIntrojucerApplication)

MainWindow.h

[code]/*

This file was auto-generated!

It contains the basic outline for a simple desktop window.

==============================================================================
*/

#ifndef MAINWINDOW_H_CCC83D1E
#define MAINWINDOW_H_CCC83D1E

#include “…/JuceLibraryCode/JuceHeader.h”

//==============================================================================
class MainAppWindow : public DocumentWindow,
public ButtonListener
{
public:
//==============================================================================
MainAppWindow();
~MainAppWindow();

void closeButtonPressed();
void buttonClicked (Button* buttonThatWasClicked);


/* Note: Be careful when overriding DocumentWindow methods - the base class
   uses a lot of them, so by overriding you might break its functionality.
   It's best to do all your work in you content component instead, but if
   you really have to override any DocumentWindow methods, make sure your
   implementation calls the superclass's method.
*/

private:
juce::ScopedPointerjuce::Component contentComponent;
juce::ScopedPointerjuce::TextEditor pathToBinary;
juce::ScopedPointerjuce::ToggleButton invokeOption;
juce::ScopedPointer scanButton;

void scanPlugins();
//==============================================================================
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (MainAppWindow)

};

#endif // MAINWINDOW_H_CCC83D1E
[/code]

MainWindow.cpp

[code]/*

This file was auto-generated!

It contains the basic outline for a simple desktop window.

==============================================================================
*/

#include “MainWindow.h”

//==============================================================================
MainAppWindow::MainAppWindow()
: DocumentWindow (JUCEApplication::getInstance()->getApplicationName(),
Colours::lightgrey,
DocumentWindow::allButtons),
contentComponent( new juce::Component() ),
scanButton(0)
{
contentComponent->addAndMakeVisible( pathToBinary = new TextEditor() );
pathToBinary->setText("/Users/adrien/Library/Developer/Xcode/DerivedData/PluginLister-byjyaxznnscaoibqtwahtfrerpyb/Build/Products/Debug/PluginLister.app/Contents/MacOS/PluginLister");
pathToBinary->setBounds(0, 30, 500, 20);

contentComponent->addAndMakeVisible( invokeOption = new ToggleButton(L"startAsProcess with parameters") );
invokeOption->setBounds(0, 50, 500, 50);

contentComponent->addAndMakeVisible( scanButton = new TextButton(L"scan button") );
scanButton->setButtonText(L"Scan");
scanButton->addListener(this);
scanButton->setBounds(0, 100, 500, 50);

centreWithSize(500, 200);
contentComponent->setSize( getWidth(), getHeight() );
contentComponent->setVisible(true);
setContentNonOwned( contentComponent, true );
setVisible(true);
}

MainAppWindow::~MainAppWindow()
{
}

void MainAppWindow::buttonClicked(Button* buttonThatWasClicked)
{
if (buttonThatWasClicked == scanButton)
{
scanPlugins();
}
}
//
void MainAppWindow::closeButtonPressed()
{
JUCEApplication::getInstance()->systemRequestedQuit();
}

void MainAppWindow::scanPlugins()
{
juce::File aPluginFile(pathToBinary->getText());
juce::String aParameters("-test_parameter -dummy option");

if( invokeOption->getToggleState() )
{
aPluginFile.startAsProcess( aParameters );
}
else
{
aPluginFile.startAsProcess();
}

}

[/code]

If anybody wants to try it out themselves…


#4

OK I found a workaround.

Instead of spawning the process with an execve of /bin/sh like what is done in FileHelpers::launchExecutable, I use the methods from NSWorkspace like this:

NSWorkspace *workspace = [NSWorkspace sharedWorkspace]; NSURL *url = [NSURL fileURLWithPath:@"/Users/blah/blah/MyApplication.app"]; //Handle url==nil NSError *error = nil; NSArray *arguments = [NSArray arrayWithObjects:@"first_parameter", @"second", @"third", nil]; NSRunningApplication *appl = [workspace launchApplicationAtURL:url options:0 configuration:[NSDictionary dictionaryWithObject:arguments forKey:NSWorkspaceLaunchConfigurationArguments] error:&error]; //Handle error

The only annoying detail for compatibility is that it is only available since 10.6
Now Jules, since you’ve already implemented an NSWorkspace based call for processes that don’t have any arguments, would you mind putting something like this for the calls to external processes with command line parameters as well? Puss in boots eyes


#5

surely if you use the normal /bin/sh based launcher, but make it call the “open” command to launch your app, that’d have the same effect?


#6

That’d work, but to access the /bin/sh based launcher, I would need to go through all the checks of Process::openDocument(), which will fail because it checks whether the file exists and if it has been given parameters or not.

openDocument("/usr/bin/open MyApp.app --args dummy", juce::String::empty) //=> fails because no arguments, so it tries to call the NSWorkspace's openFile: method openDocument("/usr/bin/open MyApp.app", "--args dummy") //=> fails on file.exists()
I mean, I can use my own launcher in that particular case, but looking at the code of Process::openDocument(), I was thinking it could be improved to support launching bundles with arguments. For the moment, the isBundle() part considers the list of arguments as a list of files (or urls, whatever) to open with the same bundle. But I admit that if you want the method to still be able to open a list of documents, this is going to be complicated…