Loading...
Searching...
No Matches
PluginScanner.hpp
1#pragma once
2
3// Shared out-of-process plug-in scan machinery for the VST2 / VST3 / CLAP /
4// LV2 application plug-ins. One puppet process is spawned per plug-in file
5// (a crashing plug-in must not take score down); the puppet reports back
6// over a local WebSocket.
7//
8// Design points, distilled from years of per-backend bugs:
9//
10// * The server listens on an *ephemeral* port and every reply must echo a
11// per-scanner random token. The old fixed ports (37587..37590) meant
12// that when several score-derived processes ran at once, every scan
13// reply landed in whichever instance owned the port: that instance
14// appended (and persisted) duplicates of every plug-in on each run,
15// while the scanning instance timed out and marked its plug-ins
16// invalid. Replies with a wrong/missing token are dropped before they
17// can touch any plug-in database.
18// * Event-driven refill: a fixed pool of at most maxInFlight() live
19// puppets, refilled whenever one resolves. (The previous VST2/VST3
20// implementation polled on a 1s timer, kept the in-flight count in a
21// translation-unit static that leaked on rescan, and only checked
22// timeouts while saturated - hung puppets in the last batch leaked
23// forever.)
24// * A puppet's exit and the WebSocket delivery of its reply race each
25// other: the process routinely finishes - sometimes with a non-zero
26// exit code from the close-handshake - before the reply is dispatched.
27// A finished-without-reply record is therefore kept in a short grace
28// period instead of being declared failed on the spot; failure is only
29// reported if the grace period elapses with no reply. This subsumes the
30// LV2 m_scanned_ok workaround and fixes the CLAP double-invalid.
31// * scanFailed() is emitted at most once per path, whatever combination
32// of errorOccurred / finished / timeout fires.
33
34#include <score_plugin_media_export.h>
35
36#include <QObject>
37#include <QPointer>
38#include <QtCore/qglobal.h>
39#if QT_CONFIG(process)
40#include <QProcess>
41#include <QProcessEnvironment>
42#endif
43#include <QStringList>
44
45#include <verdigris>
46
47#include <functional>
48#include <map>
49#include <memory>
50#include <vector>
51
52#include <QJsonObject>
53
54class QWebSocketServer;
55
56#if QT_CONFIG(process)
57namespace Media
58{
59class SCORE_PLUGIN_MEDIA_EXPORT PluginScanner : public QObject
60{
61 W_OBJECT(PluginScanner)
62public:
64 explicit PluginScanner(QString serverName, QObject* parent = nullptr);
65 ~PluginScanner();
66
67 void setPuppet(const QString& executable);
68 void setMaxInFlight(int n);
70 void setProcessTimeout(int ms);
73 void setReplyGracePeriod(int ms);
75 void setEnvironmentProvider(std::function<QProcessEnvironment()> f);
76
79 void scan(QStringList pluginPaths);
80
81 bool scanning() const noexcept;
82
84 const QString& token() const noexcept { return m_token; }
86 quint16 port() const noexcept;
87
90 void processIncomingMessage(const QString& message);
91
94 void scanned(QString path, QJsonObject obj)
95 E_SIGNAL(SCORE_PLUGIN_MEDIA_EXPORT, scanned, path, obj);
97 void scanFailed(QString path, QString reason)
98 E_SIGNAL(SCORE_PLUGIN_MEDIA_EXPORT, scanFailed, path, reason);
100 void done() E_SIGNAL(SCORE_PLUGIN_MEDIA_EXPORT, done);
101
102private:
103 struct Record
104 {
105 QString path;
106 QPointer<QProcess> process;
107 bool live{}; // started and not yet finished/reaped
108 bool replied{}; // a token-valid reply was attributed to it
109 };
110
111 bool ensureListening();
112 void refill();
113 void startOne(const QString& path);
114 void resolveProcess(int id);
115 void releaseProcess(QProcess* proc);
116 void beginGracePeriod(int id);
117 void failIfStillUnresolved(int id, const QString& reason);
118 void checkDone();
119 void cancelCurrentScan();
120
121 QWebSocketServer* m_server{};
122 QString m_serverName;
123 QString m_token;
124 QString m_puppet;
125 std::function<QProcessEnvironment()> m_env;
126
127 std::map<int, Record> m_records;
128 std::vector<QString> m_queue;
129 int m_nextId{};
130 int m_live{};
131 int m_maxInFlight{8};
132 int m_timeoutMs{10000};
133 int m_graceMs{2000};
134 bool m_scanRunning{};
135};
136}
137#endif