| @@ -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<std::mutex> 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<std::mutex> 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<PluginDescription> 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<MemoryBlock> 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<PluginScannerSubprocess>(); | |||
| 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<MainHostWindow> mainWindow; | |||
| std::unique_ptr<PluginScannerSubprocess> storedScannerSubprocess; | |||
| }; | |||
| static PluginHostApp& getApp() { return *dynamic_cast<PluginHostApp*>(JUCEApplication::getInstance()); } | |||
| @@ -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<PluginDescription>& result, | |||
| const String& fileOrIdentifier) override | |||
| { | |||
| if (scanInProcess) | |||
| { | |||
| superprocess = nullptr; | |||
| format.findAllTypesForFile (result, fileOrIdentifier); | |||
| return true; | |||
| } | |||
| if (superprocess == nullptr) | |||
| { | |||
| superprocess = std::make_unique<Superprocess> (*this); | |||
| std::unique_lock<std::mutex> lock (mutex); | |||
| connectionLost = false; | |||
| } | |||
| MemoryBlock block; | |||
| MemoryOutputStream stream { block, true }; | |||
| stream.writeString (format.getName()); | |||
| stream.writeString (fileOrIdentifier); | |||
| if (superprocess->sendMessageToSlave (block)) | |||
| { | |||
| std::unique_lock<std::mutex> 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<PluginDescription>(); | |||
| 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<std::mutex> lock (owner.mutex); | |||
| owner.pluginDescription = std::move (xml); | |||
| owner.gotResponse = true; | |||
| owner.condvar.notify_one(); | |||
| } | |||
| void handleConnectionLost() override | |||
| { | |||
| const std::lock_guard<std::mutex> 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> superprocess; | |||
| std::mutex mutex; | |||
| std::condition_variable condvar; | |||
| std::unique_ptr<XmlElement> pluginDescription; | |||
| bool gotResponse = false; | |||
| bool connectionLost = false; | |||
| std::atomic<bool> 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<CustomPluginScanner>()); | |||
| graphHolder.reset (new GraphDocumentComponent (formatManager, deviceManager, knownPluginList)); | |||
| setContentNonOwned (graphHolder.get(), false); | |||
| @@ -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, | |||
| @@ -40,6 +40,7 @@ | |||
| #include "juce_audio_processors.h" | |||
| #include <juce_gui_extra/juce_gui_extra.h> | |||
| #include <set> | |||
| //============================================================================== | |||
| #if JUCE_MAC | |||
| @@ -396,6 +396,9 @@ public: | |||
| numThreads (threads), | |||
| allowAsync (allowPluginsWhichRequireAsynchronousInstantiation) | |||
| { | |||
| const auto blacklisted = owner.list.getBlacklistedFiles(); | |||
| initiallyBlacklistedFiles = std::set<String> (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<ThreadPool> pool; | |||
| std::set<String> 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<String> allBlacklistedFiles (blacklisted.begin(), blacklisted.end()); | |||
| std::vector<String> 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<String>& 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 | |||
| @@ -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<Scanner> currentScanner; | |||
| void scanFinished (const StringArray&); | |||
| void scanFinished (const StringArray&, const std::vector<String>&); | |||
| 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; | |||