diff --git a/extras/AudioPluginHost/Source/HostStartup.cpp b/extras/AudioPluginHost/Source/HostStartup.cpp index 77428db4da..6c59e5ba5e 100644 --- a/extras/AudioPluginHost/Source/HostStartup.cpp +++ b/extras/AudioPluginHost/Source/HostStartup.cpp @@ -31,16 +31,96 @@ #error "If you're building the audio plugin host, you probably want to enable VST and/or AU support" #endif +class PluginScannerSubprocess : private ChildProcessSlave, + private AsyncUpdater +{ +public: + PluginScannerSubprocess() + { + formatManager.addDefaultFormats(); + } + + using ChildProcessSlave::initialiseFromCommandLine; + +private: + void handleMessageFromMaster (const MemoryBlock& mb) override + { + { + const std::lock_guard lock (mutex); + pendingBlocks.emplace (mb); + } + + triggerAsyncUpdate(); + } + + void handleConnectionLost() override + { + JUCEApplicationBase::quit(); + } + + // It's important to run the plugin scan on the main thread! + void handleAsyncUpdate() override + { + for (;;) + { + const auto block = [&]() -> MemoryBlock + { + const std::lock_guard lock (mutex); + + if (pendingBlocks.empty()) + return {}; + + auto out = std::move (pendingBlocks.front()); + pendingBlocks.pop(); + return out; + }(); + + if (block.isEmpty()) + return; + + MemoryInputStream stream { block, false }; + const auto formatName = stream.readString(); + const auto identifier = stream.readString(); + + OwnedArray results; + + for (auto* format : formatManager.getFormats()) + if (format->getName() == formatName) + format->findAllTypesForFile (results, identifier); + + XmlElement xml ("LIST"); + + for (const auto& desc : results) + xml.addChildElement (desc->createXml().release()); + + const auto str = xml.toString(); + sendMessageToMaster ({ str.toRawUTF8(), str.getNumBytesAsUTF8() }); + } + } + + AudioPluginFormatManager formatManager; + + std::mutex mutex; + std::queue pendingBlocks; +}; //============================================================================== class PluginHostApp : public JUCEApplication, private AsyncUpdater { public: - PluginHostApp() {} + PluginHostApp() = default; - void initialise (const String&) override + void initialise (const String& commandLine) override { + auto scannerSubprocess = std::make_unique(); + + if (scannerSubprocess->initialiseFromCommandLine (commandLine, processUID)) + { + storedScannerSubprocess = std::move (scannerSubprocess); + return; + } + // initialise our settings file.. PropertiesFile::Options options; @@ -142,6 +222,7 @@ public: private: std::unique_ptr mainWindow; + std::unique_ptr storedScannerSubprocess; }; static PluginHostApp& getApp() { return *dynamic_cast(JUCEApplication::getInstance()); } diff --git a/extras/AudioPluginHost/Source/UI/MainHostWindow.cpp b/extras/AudioPluginHost/Source/UI/MainHostWindow.cpp index 6ba07140d2..b5a5519b9c 100644 --- a/extras/AudioPluginHost/Source/UI/MainHostWindow.cpp +++ b/extras/AudioPluginHost/Source/UI/MainHostWindow.cpp @@ -27,6 +27,201 @@ #include "MainHostWindow.h" #include "../Plugins/InternalPlugins.h" +constexpr const char* scanModeKey = "pluginScanMode"; + +class CustomPluginScanner : public KnownPluginList::CustomScanner, + private ChangeListener +{ +public: + CustomPluginScanner() + { + if (auto* file = getAppProperties().getUserSettings()) + file->addChangeListener (this); + + changeListenerCallback (nullptr); + } + + ~CustomPluginScanner() override + { + if (auto* file = getAppProperties().getUserSettings()) + file->removeChangeListener (this); + } + + bool findPluginTypesFor (AudioPluginFormat& format, + OwnedArray& result, + const String& fileOrIdentifier) override + { + if (scanInProcess) + { + superprocess = nullptr; + format.findAllTypesForFile (result, fileOrIdentifier); + return true; + } + + if (superprocess == nullptr) + { + superprocess = std::make_unique (*this); + + std::unique_lock lock (mutex); + connectionLost = false; + } + + MemoryBlock block; + MemoryOutputStream stream { block, true }; + stream.writeString (format.getName()); + stream.writeString (fileOrIdentifier); + + if (superprocess->sendMessageToSlave (block)) + { + std::unique_lock lock (mutex); + gotResponse = false; + pluginDescription = nullptr; + + for (;;) + { + if (condvar.wait_for (lock, + std::chrono::milliseconds (50), + [this] { return gotResponse || shouldExit(); })) + { + break; + } + } + + if (shouldExit()) + { + superprocess = nullptr; + return true; + } + + if (connectionLost) + { + superprocess = nullptr; + return false; + } + + if (pluginDescription != nullptr) + { + for (const auto* item : pluginDescription->getChildIterator()) + { + auto desc = std::make_unique(); + + if (desc->loadFromXml (*item)) + result.add (std::move (desc)); + } + } + + return true; + } + + superprocess = nullptr; + return false; + } + + void scanFinished() override + { + superprocess = nullptr; + } + +private: + class Superprocess : public ChildProcessMaster + { + public: + explicit Superprocess (CustomPluginScanner& o) + : owner (o) + { + launchSlaveProcess (File::getSpecialLocation (File::currentExecutableFile), processUID, 0, 0); + } + + private: + void handleMessageFromSlave (const MemoryBlock& mb) override + { + auto xml = parseXML (mb.toString()); + + const std::lock_guard lock (owner.mutex); + owner.pluginDescription = std::move (xml); + owner.gotResponse = true; + owner.condvar.notify_one(); + } + + void handleConnectionLost() override + { + const std::lock_guard lock (owner.mutex); + owner.pluginDescription = nullptr; + owner.gotResponse = true; + owner.connectionLost = true; + owner.condvar.notify_one(); + } + + CustomPluginScanner& owner; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Superprocess) + }; + + void changeListenerCallback (ChangeBroadcaster*) override + { + if (auto* file = getAppProperties().getUserSettings()) + scanInProcess = (file->getIntValue (scanModeKey) == 0); + } + + std::unique_ptr superprocess; + std::mutex mutex; + std::condition_variable condvar; + std::unique_ptr pluginDescription; + bool gotResponse = false; + bool connectionLost = false; + + std::atomic scanInProcess { true }; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomPluginScanner) +}; + +//============================================================================== +class CustomPluginListComponent : public PluginListComponent +{ +public: + CustomPluginListComponent (AudioPluginFormatManager& manager, + KnownPluginList& listToRepresent, + const File& pedal, + PropertiesFile* props, + bool async) + : PluginListComponent (manager, listToRepresent, pedal, props, async) + { + addAndMakeVisible (validationModeLabel); + addAndMakeVisible (validationModeBox); + + validationModeLabel.attachToComponent (&validationModeBox, true); + validationModeLabel.setJustificationType (Justification::right); + validationModeLabel.setSize (100, 30); + + auto unusedId = 1; + + for (const auto mode : { "In-process", "Out-of-process" }) + validationModeBox.addItem (mode, unusedId++); + + validationModeBox.setSelectedItemIndex (getAppProperties().getUserSettings()->getIntValue (scanModeKey)); + + validationModeBox.onChange = [this] + { + getAppProperties().getUserSettings()->setValue (scanModeKey, validationModeBox.getSelectedItemIndex()); + }; + + resized(); + } + + void resized() override + { + PluginListComponent::resized(); + + const auto& buttonBounds = getOptionsButton().getBounds(); + validationModeBox.setBounds (buttonBounds.withWidth (130).withRightX (getWidth() - buttonBounds.getX())); + } + +private: + Label validationModeLabel { {}, "Scan mode" }; + ComboBox validationModeBox; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomPluginListComponent) +}; //============================================================================== class MainHostWindow::PluginListWindow : public DocumentWindow @@ -41,10 +236,11 @@ public: auto deadMansPedalFile = getAppProperties().getUserSettings() ->getFile().getSiblingFile ("RecentlyCrashedPluginsList"); - setContentOwned (new PluginListComponent (pluginFormatManager, - owner.knownPluginList, - deadMansPedalFile, - getAppProperties().getUserSettings(), true), true); + setContentOwned (new CustomPluginListComponent (pluginFormatManager, + owner.knownPluginList, + deadMansPedalFile, + getAppProperties().getUserSettings(), + true), true); setResizable (true, false); setResizeLimits (300, 400, 800, 1500); @@ -96,6 +292,8 @@ MainHostWindow::MainHostWindow() centreWithSize (800, 600); #endif + knownPluginList.setCustomScanner (std::make_unique()); + graphHolder.reset (new GraphDocumentComponent (formatManager, deviceManager, knownPluginList)); setContentNonOwned (graphHolder.get(), false); diff --git a/extras/AudioPluginHost/Source/UI/MainHostWindow.h b/extras/AudioPluginHost/Source/UI/MainHostWindow.h index 1a43449831..7919946f03 100644 --- a/extras/AudioPluginHost/Source/UI/MainHostWindow.h +++ b/extras/AudioPluginHost/Source/UI/MainHostWindow.h @@ -71,6 +71,8 @@ void setAutoScaleValueForPlugin (const String&, AutoScale); bool shouldAutoScalePlugin (const PluginDescription&); void addPluginAutoScaleOptionsSubMenu (AudioPluginInstance*, PopupMenu&); +constexpr const char* processUID = "juceaudiopluginhost"; + //============================================================================== class MainHostWindow : public DocumentWindow, public MenuBarModel, diff --git a/modules/juce_audio_processors/juce_audio_processors.cpp b/modules/juce_audio_processors/juce_audio_processors.cpp index 5625f23f3f..8592e27547 100644 --- a/modules/juce_audio_processors/juce_audio_processors.cpp +++ b/modules/juce_audio_processors/juce_audio_processors.cpp @@ -40,6 +40,7 @@ #include "juce_audio_processors.h" #include +#include //============================================================================== #if JUCE_MAC diff --git a/modules/juce_audio_processors/scanning/juce_PluginListComponent.cpp b/modules/juce_audio_processors/scanning/juce_PluginListComponent.cpp index 7b6265bd3f..b56ea63866 100644 --- a/modules/juce_audio_processors/scanning/juce_PluginListComponent.cpp +++ b/modules/juce_audio_processors/scanning/juce_PluginListComponent.cpp @@ -396,6 +396,9 @@ public: numThreads (threads), allowAsync (allowPluginsWhichRequireAsynchronousInstantiation) { + const auto blacklisted = owner.list.getBlacklistedFiles(); + initiallyBlacklistedFiles = std::set (blacklisted.begin(), blacklisted.end()); + FileSearchPath path (formatToScan.getDefaultLocationsToSearch()); // You need to use at least one thread when scanning plug-ins asynchronously @@ -450,6 +453,7 @@ private: const int numThreads; bool allowAsync, finished = false, timerReentrancyCheck = false; std::unique_ptr pool; + std::set initiallyBlacklistedFiles; static void startScanCallback (int result, AlertWindow* alert, Scanner* scanner) { @@ -561,8 +565,16 @@ private: void finishedScan() { - owner.scanFinished (scanner != nullptr ? scanner->getFailedFiles() - : StringArray()); + const auto blacklisted = owner.list.getBlacklistedFiles(); + std::set allBlacklistedFiles (blacklisted.begin(), blacklisted.end()); + + std::vector newBlacklistedFiles; + std::set_difference (allBlacklistedFiles.begin(), allBlacklistedFiles.end(), + initiallyBlacklistedFiles.begin(), initiallyBlacklistedFiles.end(), + std::back_inserter (newBlacklistedFiles)); + + owner.scanFinished (scanner != nullptr ? scanner->getFailedFiles() : StringArray(), + newBlacklistedFiles); } void timerCallback() override @@ -636,21 +648,33 @@ bool PluginListComponent::isScanning() const noexcept return currentScanner != nullptr; } -void PluginListComponent::scanFinished (const StringArray& failedFiles) +void PluginListComponent::scanFinished (const StringArray& failedFiles, + const std::vector& newBlacklistedFiles) { - StringArray shortNames; + StringArray warnings; + + const auto addWarningText = [&warnings] (const auto& range, const auto& prefix) + { + if (range.size() == 0) + return; + + StringArray names; + + for (auto& f : range) + names.add (File::createFileWithoutCheckingPath (f).getFileName()); + + warnings.add (prefix + ":\n\n" + names.joinIntoString (", ")); + }; - for (auto& f : failedFiles) - shortNames.add (File::createFileWithoutCheckingPath (f).getFileName()); + addWarningText (newBlacklistedFiles, TRANS ("The following files encountered fatal errors during validation")); + addWarningText (failedFiles, TRANS ("The following files appeared to be plugin files, but failed to load correctly")); currentScanner.reset(); // mustn't delete this before using the failed files array - if (shortNames.size() > 0) + if (! warnings.isEmpty()) AlertWindow::showMessageBoxAsync (MessageBoxIconType::InfoIcon, TRANS("Scan complete"), - TRANS("Note that the following files appeared to be plugin files, but failed to load correctly") - + ":\n\n" - + shortNames.joinIntoString (", ")); + warnings.joinIntoString ("\n\n")); } } // namespace juce diff --git a/modules/juce_audio_processors/scanning/juce_PluginListComponent.h b/modules/juce_audio_processors/scanning/juce_PluginListComponent.h index 307f6fd898..d9090869c2 100644 --- a/modules/juce_audio_processors/scanning/juce_PluginListComponent.h +++ b/modules/juce_audio_processors/scanning/juce_PluginListComponent.h @@ -109,6 +109,9 @@ public: */ TextButton& getOptionsButton() { return optionsButton; } + /** @internal */ + void resized() override; + private: //============================================================================== AudioPluginFormatManager& formatManager; @@ -127,12 +130,11 @@ private: class Scanner; std::unique_ptr currentScanner; - void scanFinished (const StringArray&); + void scanFinished (const StringArray&, const std::vector&); void updateList(); void removeMissingPlugins(); void removePluginItem (int index); - void resized() override; bool isInterestedInFileDrag (const StringArray&) override; void filesDropped (const StringArray&, int, int) override; void changeListenerCallback (ChangeBroadcaster*) override;