RAM2118 commited on
Commit
767fa15
·
verified ·
1 Parent(s): 224d9fb

implement Helper test-load: spawns Helper before in-process load, blocks crashy plugins

Browse files

Now loadPlugin() does:
1. Check blocklist → skip if blocked
2. Spawn PluginBridgeHelper --test '/path' → wait for exit
3. Exit code 0 → safe → load in-process
4. Exit code != 0 or crash → add to blocklist → show error → Ableton stays alive

This prevents Kickstart 2 / Ozone 12 / any crashy plugin from ever
crashing Ableton. The Helper takes the hit instead.

Source/Plugin/PluginBridgeProcessor.cpp CHANGED
@@ -2,6 +2,13 @@
2
  #include "PluginBridgeEditor.h"
3
  #include "McpServer.h"
4
 
 
 
 
 
 
 
 
5
  #include "json.hpp"
6
  using json = nlohmann::json;
7
 
@@ -68,17 +75,110 @@ juce::AudioProcessorEditor* PluginBridgeProcessor::createEditor()
68
  return new PluginBridgeEditor(*this);
69
  }
70
 
71
- // --- Plugin Loading (IN-PROCESS) ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
72
 
73
  void PluginBridgeProcessor::loadPlugin(const juce::String& path)
74
  {
75
  unloadPlugin();
76
 
77
- // Check blocklist
78
  if (blocklist.isBlocked(path))
79
  {
80
  if (onLoadFailed)
81
- onLoadFailed("Plugin is blocklisted (crashed previously)");
82
  return;
83
  }
84
 
@@ -90,7 +190,17 @@ void PluginBridgeProcessor::loadPlugin(const juce::String& path)
90
  return;
91
  }
92
 
93
- // Find plugin description
 
 
 
 
 
 
 
 
 
 
94
  juce::OwnedArray<juce::PluginDescription> typesFound;
95
 
96
  for (int i = 0; i < formatManager.getNumFormats(); ++i)
@@ -114,7 +224,6 @@ void PluginBridgeProcessor::loadPlugin(const juce::String& path)
114
  return;
115
  }
116
 
117
- // Create plugin instance
118
  juce::String errorMessage;
119
  auto instance = formatManager.createPluginInstance(
120
  *typesFound[0], currentSampleRate, currentBlockSize, errorMessage);
@@ -122,7 +231,6 @@ void PluginBridgeProcessor::loadPlugin(const juce::String& path)
122
  if (instance == nullptr)
123
  {
124
  DBG("PluginBridgeProcessor: Failed to load: " + errorMessage);
125
- // Add to blocklist if it fails
126
  blocklist.addBlocked(path);
127
  if (onLoadFailed)
128
  onLoadFailed("Failed to load: " + errorMessage);
@@ -360,17 +468,6 @@ int PluginBridgeProcessor::getMcpServerPort() const
360
  return mcpServer ? mcpServer->getPort() : 0;
361
  }
362
 
363
- // --- Test Safety (Helper-based) ---
364
-
365
- PluginBridgeProcessor::TestResult PluginBridgeProcessor::testPluginSafety(const juce::String& path)
366
- {
367
- // TODO: Spawn helper to test-load plugin
368
- // For now, all plugins are treated as safe unless blocklisted
369
- if (blocklist.isBlocked(path))
370
- return TestResult::Unsafe;
371
- return TestResult::Safe;
372
- }
373
-
374
  // --- Entry Point ---
375
 
376
  juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()
 
2
  #include "PluginBridgeEditor.h"
3
  #include "McpServer.h"
4
 
5
+ #include <spawn.h>
6
+ #include <sys/wait.h>
7
+ #include <unistd.h>
8
+ #include <signal.h>
9
+
10
+ extern char** environ;
11
+
12
  #include "json.hpp"
13
  using json = nlohmann::json;
14
 
 
75
  return new PluginBridgeEditor(*this);
76
  }
77
 
78
+ // --- Helper Test Load ---
79
+ // Spawns PluginBridgeHelper in a separate process to test if a plugin is safe.
80
+ // If the helper crashes, Ableton stays alive and the plugin gets blocklisted.
81
+
82
+ static juce::File findHelperBinary()
83
+ {
84
+ // Helper is bundled inside PluginBridge.vst3/Contents/Resources/
85
+ auto helper = juce::File::getSpecialLocation(juce::File::currentExecutableFile)
86
+ .getParentDirectory() // MacOS/
87
+ .getParentDirectory() // Contents/
88
+ .getChildFile("Resources")
89
+ .getChildFile(PluginBridgeConstants::kHelperBinaryName);
90
+
91
+ if (helper.exists())
92
+ return helper;
93
+
94
+ // Fallback: same directory as executable
95
+ helper = juce::File::getSpecialLocation(juce::File::currentExecutableFile)
96
+ .getParentDirectory()
97
+ .getChildFile(PluginBridgeConstants::kHelperBinaryName);
98
+
99
+ return helper;
100
+ }
101
+
102
+ PluginBridgeProcessor::TestResult PluginBridgeProcessor::testPluginSafety(const juce::String& path)
103
+ {
104
+ if (blocklist.isBlocked(path))
105
+ return TestResult::Unsafe;
106
+
107
+ auto helperBinary = findHelperBinary();
108
+ if (!helperBinary.exists())
109
+ {
110
+ DBG("testPluginSafety: Helper binary not found, skipping test");
111
+ return TestResult::Safe; // Can't test — assume safe
112
+ }
113
+
114
+ auto helperPathStr = helperBinary.getFullPathName().toStdString();
115
+ auto pluginPathStr = path.toStdString();
116
+
117
+ pid_t pid;
118
+ char* argv[] = {
119
+ (char*)helperPathStr.c_str(),
120
+ (char*)"--test",
121
+ (char*)pluginPathStr.c_str(),
122
+ nullptr
123
+ };
124
+
125
+ int spawnResult = posix_spawn(&pid, helperPathStr.c_str(), nullptr, nullptr, argv, environ);
126
+ if (spawnResult != 0)
127
+ {
128
+ DBG("testPluginSafety: Failed to spawn helper");
129
+ return TestResult::Safe; // Can't test — assume safe
130
+ }
131
+
132
+ // Wait for helper to exit (max 10 seconds)
133
+ int status = 0;
134
+ int waitResult = 0;
135
+ int waitedMs = 0;
136
+ const int timeoutMs = 10000;
137
+
138
+ while (waitedMs < timeoutMs)
139
+ {
140
+ waitResult = waitpid(pid, &status, WNOHANG);
141
+ if (waitResult == pid)
142
+ break;
143
+ if (waitResult < 0)
144
+ break;
145
+
146
+ usleep(50000); // 50ms
147
+ waitedMs += 50;
148
+ }
149
+
150
+ // Timeout — kill it and assume unsafe
151
+ if (waitedMs >= timeoutMs)
152
+ {
153
+ kill(pid, SIGKILL);
154
+ waitpid(pid, &status, 0);
155
+ DBG("testPluginSafety: Helper timed out for " + path);
156
+ return TestResult::Unsafe;
157
+ }
158
+
159
+ // Check exit status
160
+ if (WIFEXITED(status) && WEXITSTATUS(status) == 0)
161
+ {
162
+ DBG("testPluginSafety: Plugin is SAFE: " + path);
163
+ return TestResult::Safe;
164
+ }
165
+
166
+ // Crashed or non-zero exit
167
+ DBG("testPluginSafety: Plugin is UNSAFE: " + path);
168
+ return TestResult::Unsafe;
169
+ }
170
+
171
+ // --- Plugin Loading (IN-PROCESS, with safety test first) ---
172
 
173
  void PluginBridgeProcessor::loadPlugin(const juce::String& path)
174
  {
175
  unloadPlugin();
176
 
177
+ // 1. Check blocklist
178
  if (blocklist.isBlocked(path))
179
  {
180
  if (onLoadFailed)
181
+ onLoadFailed("Not compatible (crashed previously)");
182
  return;
183
  }
184
 
 
190
  return;
191
  }
192
 
193
+ // 2. Test safety in Helper process FIRST
194
+ auto testResult = testPluginSafety(path);
195
+ if (testResult == TestResult::Unsafe)
196
+ {
197
+ blocklist.addBlocked(path);
198
+ if (onLoadFailed)
199
+ onLoadFailed("Not compatible with PluginBridge");
200
+ return;
201
+ }
202
+
203
+ // 3. Safe — load in-process
204
  juce::OwnedArray<juce::PluginDescription> typesFound;
205
 
206
  for (int i = 0; i < formatManager.getNumFormats(); ++i)
 
224
  return;
225
  }
226
 
 
227
  juce::String errorMessage;
228
  auto instance = formatManager.createPluginInstance(
229
  *typesFound[0], currentSampleRate, currentBlockSize, errorMessage);
 
231
  if (instance == nullptr)
232
  {
233
  DBG("PluginBridgeProcessor: Failed to load: " + errorMessage);
 
234
  blocklist.addBlocked(path);
235
  if (onLoadFailed)
236
  onLoadFailed("Failed to load: " + errorMessage);
 
468
  return mcpServer ? mcpServer->getPort() : 0;
469
  }
470
 
 
 
 
 
 
 
 
 
 
 
 
471
  // --- Entry Point ---
472
 
473
  juce::AudioProcessor* JUCE_CALLTYPE createPluginFilter()