| @@ -31,16 +31,96 @@ | |||||
| #error "If you're building the audio plugin host, you probably want to enable VST and/or AU support" | #error "If you're building the audio plugin host, you probably want to enable VST and/or AU support" | ||||
| #endif | #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, | class PluginHostApp : public JUCEApplication, | ||||
| private AsyncUpdater | private AsyncUpdater | ||||
| { | { | ||||
| public: | 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.. | // initialise our settings file.. | ||||
| PropertiesFile::Options options; | PropertiesFile::Options options; | ||||
| @@ -142,6 +222,7 @@ public: | |||||
| private: | private: | ||||
| std::unique_ptr<MainHostWindow> mainWindow; | std::unique_ptr<MainHostWindow> mainWindow; | ||||
| std::unique_ptr<PluginScannerSubprocess> storedScannerSubprocess; | |||||
| }; | }; | ||||
| static PluginHostApp& getApp() { return *dynamic_cast<PluginHostApp*>(JUCEApplication::getInstance()); } | static PluginHostApp& getApp() { return *dynamic_cast<PluginHostApp*>(JUCEApplication::getInstance()); } | ||||
| @@ -27,6 +27,201 @@ | |||||
| #include "MainHostWindow.h" | #include "MainHostWindow.h" | ||||
| #include "../Plugins/InternalPlugins.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 | class MainHostWindow::PluginListWindow : public DocumentWindow | ||||
| @@ -41,10 +236,11 @@ public: | |||||
| auto deadMansPedalFile = getAppProperties().getUserSettings() | auto deadMansPedalFile = getAppProperties().getUserSettings() | ||||
| ->getFile().getSiblingFile ("RecentlyCrashedPluginsList"); | ->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); | setResizable (true, false); | ||||
| setResizeLimits (300, 400, 800, 1500); | setResizeLimits (300, 400, 800, 1500); | ||||
| @@ -96,6 +292,8 @@ MainHostWindow::MainHostWindow() | |||||
| centreWithSize (800, 600); | centreWithSize (800, 600); | ||||
| #endif | #endif | ||||
| knownPluginList.setCustomScanner (std::make_unique<CustomPluginScanner>()); | |||||
| graphHolder.reset (new GraphDocumentComponent (formatManager, deviceManager, knownPluginList)); | graphHolder.reset (new GraphDocumentComponent (formatManager, deviceManager, knownPluginList)); | ||||
| setContentNonOwned (graphHolder.get(), false); | setContentNonOwned (graphHolder.get(), false); | ||||
| @@ -71,6 +71,8 @@ void setAutoScaleValueForPlugin (const String&, AutoScale); | |||||
| bool shouldAutoScalePlugin (const PluginDescription&); | bool shouldAutoScalePlugin (const PluginDescription&); | ||||
| void addPluginAutoScaleOptionsSubMenu (AudioPluginInstance*, PopupMenu&); | void addPluginAutoScaleOptionsSubMenu (AudioPluginInstance*, PopupMenu&); | ||||
| constexpr const char* processUID = "juceaudiopluginhost"; | |||||
| //============================================================================== | //============================================================================== | ||||
| class MainHostWindow : public DocumentWindow, | class MainHostWindow : public DocumentWindow, | ||||
| public MenuBarModel, | public MenuBarModel, | ||||
| @@ -40,6 +40,7 @@ | |||||
| #include "juce_audio_processors.h" | #include "juce_audio_processors.h" | ||||
| #include <juce_gui_extra/juce_gui_extra.h> | #include <juce_gui_extra/juce_gui_extra.h> | ||||
| #include <set> | |||||
| //============================================================================== | //============================================================================== | ||||
| #if JUCE_MAC | #if JUCE_MAC | ||||
| @@ -396,6 +396,9 @@ public: | |||||
| numThreads (threads), | numThreads (threads), | ||||
| allowAsync (allowPluginsWhichRequireAsynchronousInstantiation) | allowAsync (allowPluginsWhichRequireAsynchronousInstantiation) | ||||
| { | { | ||||
| const auto blacklisted = owner.list.getBlacklistedFiles(); | |||||
| initiallyBlacklistedFiles = std::set<String> (blacklisted.begin(), blacklisted.end()); | |||||
| FileSearchPath path (formatToScan.getDefaultLocationsToSearch()); | FileSearchPath path (formatToScan.getDefaultLocationsToSearch()); | ||||
| // You need to use at least one thread when scanning plug-ins asynchronously | // You need to use at least one thread when scanning plug-ins asynchronously | ||||
| @@ -450,6 +453,7 @@ private: | |||||
| const int numThreads; | const int numThreads; | ||||
| bool allowAsync, finished = false, timerReentrancyCheck = false; | bool allowAsync, finished = false, timerReentrancyCheck = false; | ||||
| std::unique_ptr<ThreadPool> pool; | std::unique_ptr<ThreadPool> pool; | ||||
| std::set<String> initiallyBlacklistedFiles; | |||||
| static void startScanCallback (int result, AlertWindow* alert, Scanner* scanner) | static void startScanCallback (int result, AlertWindow* alert, Scanner* scanner) | ||||
| { | { | ||||
| @@ -561,8 +565,16 @@ private: | |||||
| void finishedScan() | 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 | void timerCallback() override | ||||
| @@ -636,21 +648,33 @@ bool PluginListComponent::isScanning() const noexcept | |||||
| return currentScanner != nullptr; | 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 | currentScanner.reset(); // mustn't delete this before using the failed files array | ||||
| if (shortNames.size() > 0) | |||||
| if (! warnings.isEmpty()) | |||||
| AlertWindow::showMessageBoxAsync (MessageBoxIconType::InfoIcon, | AlertWindow::showMessageBoxAsync (MessageBoxIconType::InfoIcon, | ||||
| TRANS("Scan complete"), | 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 | } // namespace juce | ||||
| @@ -109,6 +109,9 @@ public: | |||||
| */ | */ | ||||
| TextButton& getOptionsButton() { return optionsButton; } | TextButton& getOptionsButton() { return optionsButton; } | ||||
| /** @internal */ | |||||
| void resized() override; | |||||
| private: | private: | ||||
| //============================================================================== | //============================================================================== | ||||
| AudioPluginFormatManager& formatManager; | AudioPluginFormatManager& formatManager; | ||||
| @@ -127,12 +130,11 @@ private: | |||||
| class Scanner; | class Scanner; | ||||
| std::unique_ptr<Scanner> currentScanner; | std::unique_ptr<Scanner> currentScanner; | ||||
| void scanFinished (const StringArray&); | |||||
| void scanFinished (const StringArray&, const std::vector<String>&); | |||||
| void updateList(); | void updateList(); | ||||
| void removeMissingPlugins(); | void removeMissingPlugins(); | ||||
| void removePluginItem (int index); | void removePluginItem (int index); | ||||
| void resized() override; | |||||
| bool isInterestedInFileDrag (const StringArray&) override; | bool isInterestedInFileDrag (const StringArray&) override; | ||||
| void filesDropped (const StringArray&, int, int) override; | void filesDropped (const StringArray&, int, int) override; | ||||
| void changeListenerCallback (ChangeBroadcaster*) override; | void changeListenerCallback (ChangeBroadcaster*) override; | ||||