#include "HelperConnection.h" #include "Constants.h" #include #include #include #include #include #include #include extern char** environ; #include "json.hpp" using json = nlohmann::json; HelperConnection::HelperConnection() { myPid = getpid(); signal(SIGPIPE, SIG_IGN); } HelperConnection::~HelperConnection() { killHelper(); } bool HelperConnection::spawnHelper() { killHelper(); auto shmName = IPC::getShmName(myPid); if (!sharedAudio.create(shmName)) { DBG("HelperConnection: Failed to create shared memory"); return false; } if (!inputSem.create(IPC::getInputSemName(myPid)) || !outputSem.create(IPC::getOutputSemName(myPid))) { DBG("HelperConnection: Failed to create semaphores"); return false; } auto socketPath = IPC::getSocketPath(myPid); unlink(socketPath.c_str()); serverSocketFd = socket(AF_UNIX, SOCK_STREAM, 0); if (serverSocketFd < 0) { DBG("HelperConnection: Failed to create socket"); return false; } struct sockaddr_un addr; memset(&addr, 0, sizeof(addr)); addr.sun_family = AF_UNIX; strncpy(addr.sun_path, socketPath.c_str(), sizeof(addr.sun_path) - 1); if (bind(serverSocketFd, (struct sockaddr*)&addr, sizeof(addr)) < 0 || listen(serverSocketFd, 1) < 0) { DBG("HelperConnection: Failed to bind/listen"); close(serverSocketFd); serverSocketFd = -1; return false; } // Find helper .app bundle inside .vst3/Contents/Resources/ auto helperApp = juce::File::getSpecialLocation(juce::File::currentExecutableFile) .getParentDirectory() // MacOS/ .getParentDirectory() // Contents/ .getChildFile("Resources") .getChildFile("PluginBridgeHelper.app") .getChildFile("Contents") .getChildFile("MacOS") .getChildFile(PluginBridgeConstants::kHelperBinaryName); if (!helperApp.exists()) { // Fallback: bare binary helperApp = juce::File::getSpecialLocation(juce::File::currentExecutableFile) .getParentDirectory() .getParentDirectory() .getChildFile("Resources") .getChildFile(PluginBridgeConstants::kHelperBinaryName); } if (!helperApp.exists()) { DBG("HelperConnection: Helper not found at: " + helperApp.getFullPathName()); return false; } state.store(State::Starting); auto pidStr = juce::String(myPid).toStdString(); auto helperPathStr = helperApp.getFullPathName().toStdString(); pid_t pid; char* argv[] = { (char*)helperPathStr.c_str(), (char*)"--pid", (char*)pidStr.c_str(), nullptr }; int spawnResult = posix_spawn(&pid, helperPathStr.c_str(), nullptr, nullptr, argv, environ); if (spawnResult != 0) { DBG("HelperConnection: Failed to spawn: " + juce::String(strerror(spawnResult))); state.store(State::Disconnected); return false; } helperPid = pid; helperRunning.store(true); struct pollfd pfd; pfd.fd = serverSocketFd; pfd.events = POLLIN; if (poll(&pfd, 1, IPC::kHelperTimeoutMs) <= 0) { DBG("HelperConnection: Helper didn't connect in time"); killHelper(); return false; } clientSocketFd = accept(serverSocketFd, nullptr, nullptr); if (clientSocketFd < 0) { DBG("HelperConnection: Failed to accept connection"); killHelper(); return false; } int nosigpipe = 1; setsockopt(clientSocketFd, SOL_SOCKET, SO_NOSIGPIPE, &nosigpipe, sizeof(nosigpipe)); ipcRunning.store(true); ipcThread = std::thread([this]() { ipcListenLoop(); }); startTimerHz(4); int waitMs = 0; while (state.load() == State::Starting && waitMs < IPC::kHelperTimeoutMs) { juce::Thread::sleep(50); waitMs += 50; } return state.load() == State::Connected; } void HelperConnection::killHelper() { stopTimer(); ipcRunning.store(false); if (clientSocketFd >= 0) { close(clientSocketFd); clientSocketFd = -1; } if (serverSocketFd >= 0) { close(serverSocketFd); serverSocketFd = -1; } if (ipcThread.joinable()) ipcThread.join(); if (helperPid > 0 && helperRunning.load()) { kill(helperPid, SIGTERM); int status; waitpid(helperPid, &status, WNOHANG); usleep(100000); kill(helperPid, SIGKILL); waitpid(helperPid, &status, 0); } helperPid = 0; helperRunning.store(false); sharedAudio.destroy(); inputSem.destroy(); outputSem.destroy(); unlink(IPC::getSocketPath(myPid).c_str()); state.store(State::Disconnected); } std::string HelperConnection::sendCommand(const std::string& jsonCmd) { std::lock_guard lock(commandMutex); if (clientSocketFd < 0 || !helperRunning.load()) return "{\"ok\":false,\"error\":\"not connected\"}"; auto data = jsonCmd + "\n"; if (::send(clientSocketFd, data.c_str(), data.size(), 0) < 0) return "{\"ok\":false,\"error\":\"send failed\"}"; responseReady.store(false); int waitMs = 0; while (!responseReady.load() && waitMs < 5000) { juce::Thread::sleep(10); waitMs += 10; } if (!responseReady.load()) return "{\"ok\":false,\"error\":\"timeout\"}"; return lastResponse; } void HelperConnection::timerCallback() { if (helperPid <= 0) return; int status; pid_t result = waitpid(helperPid, &status, WNOHANG); if (result == helperPid) { helperRunning.store(false); state.store(State::Crashed); stopTimer(); DBG("HelperConnection: Helper died"); if (onHelperCrashed) onHelperCrashed(); } } void HelperConnection::ipcListenLoop() { std::string buffer; char chunk[4096]; while (ipcRunning.load()) { struct pollfd pfd; pfd.fd = clientSocketFd; pfd.events = POLLIN; int ret = poll(&pfd, 1, 200); if (ret <= 0) continue; ssize_t n = recv(clientSocketFd, chunk, sizeof(chunk) - 1, 0); if (n <= 0) { if (ipcRunning.load()) { helperRunning.store(false); state.store(State::Crashed); if (onHelperCrashed) juce::MessageManager::callAsync([this]() { if (onHelperCrashed) onHelperCrashed(); }); } return; } chunk[n] = '\0'; buffer += chunk; size_t pos; while ((pos = buffer.find('\n')) != std::string::npos) { auto msg = buffer.substr(0, pos); buffer.erase(0, pos + 1); if (msg.empty()) continue; try { auto j = json::parse(msg); if (j.contains("notify")) handleNotification(msg); else { lastResponse = msg; responseReady.store(true); } } catch (...) {} } } } void HelperConnection::handleNotification(const std::string& jsonStr) { try { auto j = json::parse(jsonStr); auto notify = j.value("notify", ""); if (notify == "ready") { state.store(State::Connected); if (onHelperReady) juce::MessageManager::callAsync([this]() { if (onHelperReady) onHelperReady(); }); } else if (notify == "gui_ready") { auto portName = juce::String(j.value("layerPortName", "")); int width = j.value("width", 0); int height = j.value("height", 0); if (onGuiReady) juce::MessageManager::callAsync([this, portName, width, height]() { if (onGuiReady) onGuiReady(portName, width, height); }); } else if (notify == "resized") { int width = j.value("width", 0); int height = j.value("height", 0); if (onGuiResized) juce::MessageManager::callAsync([this, width, height]() { if (onGuiResized) onGuiResized(width, height); }); } else if (notify == "param_changed") { int index = j.value("index", -1); float value = j.value("value", 0.0f); if (onParamChanged) onParamChanged(index, value); } else if (notify == "crashed") { state.store(State::Crashed); if (onHelperCrashed) juce::MessageManager::callAsync([this]() { if (onHelperCrashed) onHelperCrashed(); }); } } catch (...) {} }