Is there a way to prevent scanning of a certain plugin?

Is there any provision for specifying plugins to ignore when scanning the plugin directories?

I am working on a plugin that is a plugin host. It does its own scanning of plugins, and there’s no reason for it to scan itself, or include itself in the list of plugins that it can load.

I am using the scanning code from the AudioPluginHost, which uses:
KnownPluginList
PluginDirectoryScanner
PluginListComponent

I don’t really see a way to exclude anything from being scanned, but maybe I’m missing it…

We have a text file with a list of plugins to exclude… then in doNextScan() we check if the plugin name is in the exclude list and skip it.

Rail

So you are subclassing PluginListComponent and overriding doNextScan()?

EDIT: It does not seem that that would work. Are you sure you’re not thinking of scanNextFile() ?

In PluginListComponent::doNextScan(), at the top of the function pluginBeingScanned is either unknown (for the first plugin) or contains the previous scanned plugin’s name:

    bool doNextScan()
    {

        if (scanner->scanNextFile (true, pluginBeingScanned))
        {
            progress = scanner->getProgress();
            return true;
        }

        finished = true;
        return false;
    }

What actually gets the file and fills in the name of the next one to be scanned - and then scans it - is scanner->scanNextFile() :

bool PluginDirectoryScanner::scanNextFile (bool dontRescanIfAlreadyInList,
                                           String& nameOfPluginBeingScanned)
{
    const int index = --nextIndex;

    if (index >= 0)
    {
        auto file = filesOrIdentifiersToScan [index];

        if (file.isNotEmpty() && ! (dontRescanIfAlreadyInList && list.isListingUpToDate (file, format)))
        {
            nameOfPluginBeingScanned = format.getNameOfPluginFromIdentifier (file);

            OwnedArray<PluginDescription> typesFound;

            // Add this plugin to the end of the dead-man's pedal list in case it crashes...
            auto crashedPlugins = readDeadMansPedalFile (deadMansPedalFile);
            crashedPlugins.removeString (file);
            crashedPlugins.add (file);
            setDeadMansPedalFile (crashedPlugins);

            list.scanAndAddFile(file, dontRescanIfAlreadyInList, typesFound, format);

            // Managed to load without crashing, so remove it from the dead-man's-pedal..
            crashedPlugins.removeString (file);
            setDeadMansPedalFile (crashedPlugins);

            if (typesFound.size() == 0 && ! list.getBlacklistedFiles().contains (file))
                failedFiles.add (file);
        }
    }

    updateProgress();
    return index > 0;
}

So it seems you would need to skip it in there, no?

Looked a bit more. Perhaps you are using:

scanner->getNextPluginFileThatWillBeScanned()

To get the name in doNextScan() ?

It seems impossible to inherit and override this whole scanning process. You must be using a locally-modified copy of JUCE to do this…

It’s a bit brute force but couldn’t you just remove your plugin from the list after a scan completes?

I’m not doing either…

I have a PluginScanner class derived from Timer with a PluginDirectoryScanner member… and in PluginScanner::startScanning() I start the Timer… and in PluginScanner::timerCallback() I basically:

    stopTimer();

    :

    if (doNextScan())
        {
        :
        startTimer (100);
        }
    else
        m_bFinished = true;

in PluginScanner::doNextScan() I basically check the next plugin using:

String szNextPlugin = m_Scanner.getNextPluginFileThatWillBeScanned();

and check that against the StringArray of plugins to ignore.

    if (m_DontScanArray.contains (szNextPlugin, true))
        {
        :

        return m_Scanner.skipNextFile();
        }


    m_Scanner.scanNextFile (......);

Rail

Thanks, I was trying to figure out how to do that.

Are you using the PluginListComponent? I based my scanning code on the AudioPluginHost, including the whole PluginListWindow.

This is probably a C++ thing I don’t know how to do, but how do I insert my own sub-classed PluginScanner into this process? In PluginListComponent is a pointer to a class Scanner (PluginListComponent::Scanner). How to replace that with my own scanner?

I have a command line app for scanning which is run as a separate process… so the PluginListComponent is in the plugin which spawns the scanner process.

Rail

I’ve heard others say they run the scanning as a separate app (so that when it crashes, and it will, it doesn’t take down the host). Would love to see an example of that! It seems a lot of us go about re-inventing the wheel on many of these issues…

Pluginval can do that and it is open source. I always meant to look into the sources, but haven’t found time yet…

Probably somewhere around here:

1 Like

OK, so without writing and launching an external process, and without investigating that whole thing, I wanted to see if I could insert a “custom scanner” into the process of PluginListComponent. It seems really difficult and klugey. I’m wondering if I’m just wrong on this whole idea…

Basically, if you wanted to ignore a certain plugin, you need to override doNextScan():

    bool doNextScan()
    {
        // -----------------------------
        // this is essentially the only modification that required sub-classing
        // the whole thing
        String s = scanner->getNextPluginFileThatWillBeScanned();
        if (s.contains(“myPlugin”))
        {
            scanner->skipNextFile();
            progress = scanner->getProgress();
            DBG("    >>>>PLComponent2: doNextScan SKIPPING " + s + ", progress " + String(progress));            			return true;
        }
        // -----------------------------
        
        if (scanner->scanNextFile (true, pluginBeingScanned))
        {
            progress = scanner->getProgress();
            return true;
        }
        
        finished = true;
        return false;
    }

But doNextScan() is a member function of a private class of PluginListComponent::Scanner. You can’t sub-class it, unless I’m wrong.

So to get to that point, you need to sub-class PluginListComponent, and duplicate/replace the entire Scanner class. Not only that, you need to duplicate a bunch of private member variables of PluginListComponent and have them shadow/hide the ones in the super-class, because you need to pass them to the scanFor() function and they are private.

And the real klugey thing was replacing the actions (lambdas) in the Options Popup Menu so that they pass a pointer to the sub-class and call the override of scanFor():

PopupMenu PluginListComponent2::createOptionsMenu()
{
    // first, create the entire menu in the super-class
    PopupMenu menu = PluginListComponent::createOptionsMenu();
    
    // find the "scan for" items and replace the lambdas
    // with pointers to this object (sub-class)
    PopupMenu::MenuItemIterator iter(menu);
    while(iter.next())
    {
        auto& item = iter.getItem();    // find the first "scan for" item
        if (item.text.contains("Scan for new or updated"))
        {
            // run the same loop for format types as the original,
            // and replace all of the lambdas
            for (auto format : formatManager.getFormats())
            {
                // can't use the same variable inside this loop for some reason
                auto& thisItem = iter.getItem();
                if (format->canScanForPlugins())
                    thisItem.setAction ([this, format]  { scanFor (*format); });
                if (! iter.next())  // advance to next item; if none will exit
                    break;
            }
            break;
        }
    }
    return menu;
}

Maybe I’m just being an idiot here, I’m not an expert with inheritance… but here’s what I had to do (in total) to simply get to an override of doNextScan() (it DOES work, BTW):

PluginListComponent2.h

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

    PluginListComponent2.h
    Created: 27 Feb 2020 7:14:45pm

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

#pragma once
#include "JuceHeader.h"

class PluginListComponent2 : public PluginListComponent
{
public:
    PluginListComponent2(AudioPluginFormatManager& formatManager,
                         KnownPluginList& listToRepresent,
                         const File& deadMansPedalFile,
                         PropertiesFile* propertiesToUse,
                         bool allowPluginsWhichRequireAsynchronousInstantiation = false);


    PopupMenu createOptionsMenu();

    void scanFor (AudioPluginFormat&);

    void scanFor (AudioPluginFormat&, const StringArray& filesOrIdentifiersToScan);

private:
    // all of these duplicate and hide member variables in the super-class
    // because we need to pass them in scanFor() and they are private
    AudioPluginFormatManager& formatManager;
    KnownPluginList& list;
    TableListBox table;
    File deadMansPedalFile;
    PropertiesFile* propertiesToUse;
    String dialogTitle, dialogText;
    bool allowAsync;
    int numThreads;

    // this hides and overrides the Scanner with our Scanner class
    class Scanner;
    std::unique_ptr<Scanner> currentScanner;
    
    void scanFinished (const StringArray&);

    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (PluginListComponent2)
};

PluginListComponent2.cpp

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

    PluginListComponent2.cpp
    Created: 27 Feb 2020 7:14:45pm
    Author:  Stephen Kay

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

#include "PluginListComponent2.h"


//==============================================================================
class PluginListComponent2::Scanner    : private Timer
{
public:
    Scanner (PluginListComponent2& plc, AudioPluginFormat& format, const StringArray& filesOrIdentifiers,
             PropertiesFile* properties, bool allowPluginsWhichRequireAsynchronousInstantiation, int threads,
             const String& title, const String& text)
    : owner (plc), formatToScan (format), filesOrIdentifiersToScan (filesOrIdentifiers), propertiesToUse (properties),
    pathChooserWindow (TRANS("Select folders to scan..."), String(), AlertWindow::NoIcon),
    progressWindow (title, text, AlertWindow::NoIcon),
    numThreads (threads), allowAsync (allowPluginsWhichRequireAsynchronousInstantiation)
    {
        FileSearchPath path (formatToScan.getDefaultLocationsToSearch());
        
        // You need to use at least one thread when scanning plug-ins asynchronously
        jassert (! allowAsync || (numThreads > 0));
        
        // If the filesOrIdentifiersToScan argument isn't empty, we should only scan these
        // If the path is empty, then paths aren't used for this format.
        if (filesOrIdentifiersToScan.isEmpty() && path.getNumPaths() > 0)
        {
#if ! JUCE_IOS
            if (propertiesToUse != nullptr)
                path = getLastSearchPath (*propertiesToUse, formatToScan);
#endif
            
            pathList.setSize (500, 300);
            pathList.setPath (path);
            
            pathChooserWindow.addCustomComponent (&pathList);
            pathChooserWindow.addButton (TRANS("Scan"),   1, KeyPress (KeyPress::returnKey));
            pathChooserWindow.addButton (TRANS("Cancel"), 0, KeyPress (KeyPress::escapeKey));
            
            pathChooserWindow.enterModalState (true,
                                               ModalCallbackFunction::forComponent (startScanCallback,
                                                                                    &pathChooserWindow, this),
                                               false);
        }
        else
        {
            startScan();
        }
    }
    
    ~Scanner() override
    {
        if (pool != nullptr)
        {
            pool->removeAllJobs (true, 60000);
            pool.reset();
        }
    }
    
private:
    PluginListComponent2& owner;
    AudioPluginFormat& formatToScan;
    StringArray filesOrIdentifiersToScan;
    PropertiesFile* propertiesToUse;
    std::unique_ptr<PluginDirectoryScanner> scanner;
    AlertWindow pathChooserWindow, progressWindow;
    FileSearchPathListComponent pathList;
    String pluginBeingScanned;
    double progress = 0;
    int numThreads;
    bool allowAsync, finished = false, timerReentrancyCheck = false;
    std::unique_ptr<ThreadPool> pool;
    
    static void startScanCallback (int result, AlertWindow* alert, Scanner* scanner)
    {
        if (alert != nullptr && scanner != nullptr)
        {
            if (result != 0)
                scanner->warnUserAboutStupidPaths();
            else
                scanner->finishedScan();
        }
    }
    
    // Try to dissuade people from to scanning their entire C: drive, or other system folders.
    void warnUserAboutStupidPaths()
    {
        for (int i = 0; i < pathList.getPath().getNumPaths(); ++i)
        {
            auto f = pathList.getPath()[i];
            
            if (isStupidPath (f))
            {
                AlertWindow::showOkCancelBox (AlertWindow::WarningIcon,
                                              TRANS("Plugin Scanning"),
                                              TRANS("If you choose to scan folders that contain non-plugin files, "
                                                    "then scanning may take a long time, and can cause crashes when "
                                                    "attempting to load unsuitable files.")
                                              + newLine
                                              + TRANS ("Are you sure you want to scan the folder \"XYZ\"?")
                                              .replace ("XYZ", f.getFullPathName()),
                                              TRANS ("Scan"),
                                              String(),
                                              nullptr,
                                              ModalCallbackFunction::create (warnAboutStupidPathsCallback, this));
                return;
            }
        }
        
        startScan();
    }
    
    static bool isStupidPath (const File& f)
    {
        Array<File> roots;
        File::findFileSystemRoots (roots);
        
        if (roots.contains (f))
            return true;
        
        File::SpecialLocationType pathsThatWouldBeStupidToScan[]
        = { File::globalApplicationsDirectory,
            File::userHomeDirectory,
            File::userDocumentsDirectory,
            File::userDesktopDirectory,
            File::tempDirectory,
            File::userMusicDirectory,
            File::userMoviesDirectory,
            File::userPicturesDirectory };
        
        for (auto location : pathsThatWouldBeStupidToScan)
        {
            auto sillyFolder = File::getSpecialLocation (location);
            
            if (f == sillyFolder || sillyFolder.isAChildOf (f))
                return true;
        }
        
        return false;
    }
    
    static void warnAboutStupidPathsCallback (int result, Scanner* scanner)
    {
        if (result != 0)
            scanner->startScan();
        else
            scanner->finishedScan();
    }
    
    void startScan()
    {
        pathChooserWindow.setVisible (false);
        
        scanner.reset (new PluginDirectoryScanner (owner.list, formatToScan, pathList.getPath(),
                                                   true, owner.deadMansPedalFile, allowAsync));
        
        if (! filesOrIdentifiersToScan.isEmpty())
        {
            scanner->setFilesOrIdentifiersToScan (filesOrIdentifiersToScan);
        }
        else if (propertiesToUse != nullptr)
        {
            setLastSearchPath (*propertiesToUse, formatToScan, pathList.getPath());
            propertiesToUse->saveIfNeeded();
        }
        
        progressWindow.addButton (TRANS("Cancel"), 0, KeyPress (KeyPress::escapeKey));
        progressWindow.addProgressBarComponent (progress);
        progressWindow.enterModalState();
        
        if (numThreads > 0)
        {
            pool.reset (new ThreadPool (numThreads));
            
            for (int i = numThreads; --i >= 0;)
                pool->addJob (new ScanJob (*this), true);
        }
        
        startTimer (20);
    }
    
    void finishedScan()
    {
        owner.scanFinished (scanner != nullptr ? scanner->getFailedFiles()
                            : StringArray());
    }
    
    void timerCallback() override
    {
        if (timerReentrancyCheck)
            return;
        
        if (pool == nullptr)
        {
            const ScopedValueSetter<bool> setter (timerReentrancyCheck, true);
            
            if (doNextScan())
                startTimer (20);
        }
        
        if (! progressWindow.isCurrentlyModal())
            finished = true;
        
        if (finished)
            finishedScan();
        else
            progressWindow.setMessage (TRANS("Testing") + ":\n\n" + pluginBeingScanned);
    }
    
    bool doNextScan()
    {
        // -----------------------------
        // this is essentially the only modification that required sub-classing
        // the whole thing
        String s = scanner->getNextPluginFileThatWillBeScanned();
        if (s.contains(“myPlugin”))
        {
            scanner->skipNextFile();
            progress = scanner->getProgress();
            DBG("    >>>>PLComponent2: doNextScan SKIPPING " + s + ", progress " + String(progress));  
            return true;
        }
        // -----------------------------
        
        if (scanner->scanNextFile (true, pluginBeingScanned))
        {
            progress = scanner->getProgress();
            return true;
        }
        
        finished = true;
        return false;
    }
    
    struct ScanJob  : public ThreadPoolJob
    {
        ScanJob (Scanner& s)  : ThreadPoolJob ("pluginscan"), scanner (s) {}
        
        JobStatus runJob()
        {
            while (scanner.doNextScan() && ! shouldExit())
            {}
            
            return jobHasFinished;
        }
        
        Scanner& scanner;
        
        JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ScanJob)
    };
    
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Scanner)
};

PluginListComponent2::PluginListComponent2 (AudioPluginFormatManager& manager, KnownPluginList& listToEdit,
                                            const File& deadMansPedal, PropertiesFile* const props,
                                            bool allowPluginsWhichRequireAsynchronousInstantiation)
: PluginListComponent (manager, listToEdit,
                       deadMansPedal, props,
                       allowPluginsWhichRequireAsynchronousInstantiation),
formatManager (manager),
list (listToEdit),
deadMansPedalFile (deadMansPedal),
propertiesToUse (props),
allowAsync (allowPluginsWhichRequireAsynchronousInstantiation),
numThreads (allowAsync ? 1 : 0)
{
    
    //addAndMakeVisible(optionsButton);
    getOptionsButton().onClick = [this]
    {
        createOptionsMenu().showMenuAsync (PopupMenu::Options()
                                           .withDeletionCheck (*this)
                                           .withTargetComponent (getOptionsButton()));
    };
}

PopupMenu PluginListComponent2::createOptionsMenu()
{
    // first, create the entire menu in the super-class
    PopupMenu menu = PluginListComponent::createOptionsMenu();
    
    // find the "scan for" items and replace the lambdas
    // with pointers to this object (sub-class)
    PopupMenu::MenuItemIterator iter(menu);
    while(iter.next())
    {
        auto& item = iter.getItem();    // find the first "scan for" item
        if (item.text.contains("Scan for new or updated"))
        {
            // run the same loop for format types as the original,
            // and replace all of the lambdas
            for (auto format : formatManager.getFormats())
            {
                // can't use the same variable inside this loop for some reason
                auto& thisItem = iter.getItem();
                if (format->canScanForPlugins())
                    thisItem.setAction ([this, format]  { scanFor (*format); });
                if (! iter.next())  // advance to next item; if none will exit
                    break;
            }
            break;
        }
    }
    return menu;
}

void PluginListComponent2::scanFor (AudioPluginFormat& format)
{
    DBG("PL2:scanFor format " + format.getName());
    scanFor (format, StringArray());
}

void PluginListComponent2::scanFor (AudioPluginFormat& format, const StringArray& filesOrIdentifiersToScan)
{
    currentScanner.reset (new Scanner (*this, format, filesOrIdentifiersToScan, propertiesToUse, allowAsync, numThreads,
                                       dialogTitle.isNotEmpty() ? dialogTitle : TRANS("Scanning for plug-ins..."),
                                       dialogText.isNotEmpty()  ? dialogText  : TRANS("Searching for all possible plug-in files...")));
}

void PluginListComponent2::scanFinished (const StringArray& failedFiles)
{
    StringArray shortNames;
    
    for (auto& f : failedFiles)
        shortNames.add (File::createFileWithoutCheckingPath (f).getFileName());
    
    currentScanner.reset(); // mustn't delete this before using the failed files array
    
    if (shortNames.size() > 0)
        AlertWindow::showMessageBoxAsync (AlertWindow::InfoIcon,
                                          TRANS("Scan complete"),
                                          TRANS("Note that the following files appeared to be plugin files, but failed to load correctly")
                                          + ":\n\n"
                                          + shortNames.joinIntoString (", "));
}