215 lines
7.5 KiB
C++
215 lines
7.5 KiB
C++
// native_bridge/src/SandboxVst3Host.cpp
|
|
#include "SandboxVst3Host.h"
|
|
|
|
#include <chrono>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <iostream>
|
|
#include <vector>
|
|
|
|
// F-PROC-1 (audit 5.2): Windows CRT command-line parsing escape for an
|
|
// argument inside double quotes. A path containing a double quote would
|
|
// otherwise break out of --path "..." and inject arbitrary argv (e.g.
|
|
// `" && calc`). Metacharacters (& | > <) are inert here: CreateProcessA
|
|
// does not run a shell.
|
|
static std::string quote_arg(const std::string& s) {
|
|
std::string out = "\"";
|
|
size_t bs = 0;
|
|
for (char c : s) {
|
|
if (c == '\\') { ++bs; continue; }
|
|
if (c == '"') { out.append(bs * 2 + 1, '\\'); out += '"'; bs = 0; }
|
|
else { out.append(bs, '\\'); out += c; bs = 0; }
|
|
}
|
|
out.append(bs * 2, '\\');
|
|
out += '"';
|
|
return out;
|
|
}
|
|
|
|
static bool proc_alive(HANDLE h) {
|
|
#ifdef _WIN32
|
|
if (!h) return false;
|
|
DWORD code = 0;
|
|
if (!GetExitCodeProcess(h, &code)) return false;
|
|
return code == STILL_ACTIVE;
|
|
#else
|
|
return true;
|
|
#endif
|
|
}
|
|
|
|
SandboxVst3Host::SandboxVst3Host() {}
|
|
|
|
SandboxVst3Host::~SandboxVst3Host() {
|
|
stop_.store(true);
|
|
if (watchdog_.joinable()) watchdog_.join();
|
|
#ifdef _WIN32
|
|
if (childProc_) {
|
|
TerminateProcess(childProc_, 0);
|
|
CloseHandle(childProc_);
|
|
childProc_ = nullptr;
|
|
}
|
|
#endif
|
|
if (shm_) {
|
|
shm_close(shm_);
|
|
shm_ = nullptr;
|
|
ipc_ = nullptr;
|
|
}
|
|
}
|
|
|
|
bool SandboxVst3Host::loadPlugin(const std::string& path, double sampleRate, uint32_t channel) {
|
|
path_ = path;
|
|
sampleRate_ = sampleRate;
|
|
channel_ = channel;
|
|
char name[128];
|
|
snprintf(name, sizeof(name), "SonicForge_PluginHost_%lu_%u",
|
|
(unsigned long)GetCurrentProcessId(), channel);
|
|
shmName_ = name;
|
|
if (!spawnChild()) return false;
|
|
watchdog_ = std::thread([this]() { watchdogLoop(); });
|
|
return true;
|
|
}
|
|
|
|
bool SandboxVst3Host::spawnChild() {
|
|
// Mapping created by the bridge (parent); the child opens it with --open.
|
|
if (shm_) {
|
|
shm_close(shm_);
|
|
shm_ = nullptr;
|
|
ipc_ = nullptr;
|
|
}
|
|
shm_ = shm_create(shmName_.c_str(), sizeof(SandboxHostIPC));
|
|
if (!shm_) {
|
|
std::cerr << "[SandboxVst3Host] shm_create failed ch=" << channel_ << std::endl;
|
|
return false;
|
|
}
|
|
ipc_ = shm_sandbox_ptr(shm_);
|
|
|
|
// plugin_host.exe sits next to daw_vst_bridge.exe (same dir, install/).
|
|
char exePath[MAX_PATH] = {};
|
|
GetModuleFileNameA(nullptr, exePath, MAX_PATH);
|
|
std::string dir(exePath);
|
|
size_t slash = dir.find_last_of("\\/");
|
|
std::string hostExe = (slash == std::string::npos)
|
|
? "plugin_host.exe"
|
|
: dir.substr(0, slash + 1) + "plugin_host.exe";
|
|
// F-PROC-1: lpApplicationName = hostExe (exe path is never parsed as
|
|
// command line), and the VST path goes through quote_arg so a hostile
|
|
// path cannot inject extra arguments.
|
|
std::string cmd = "--open --shm " + shmName_ +
|
|
" --path " + quote_arg(path_) +
|
|
" --sr " + std::to_string((int)sampleRate_) +
|
|
" --block " + std::to_string(block_) +
|
|
" --parent " + std::to_string((unsigned long)GetCurrentProcessId());
|
|
std::cerr << "[SandboxVst3Host] spawn ch=" << channel_ << " " << cmd << std::endl;
|
|
|
|
STARTUPINFOA si = {};
|
|
si.cb = sizeof(si);
|
|
PROCESS_INFORMATION pi = {};
|
|
std::vector<char> buf(cmd.begin(), cmd.end());
|
|
buf.push_back('\0');
|
|
if (!CreateProcessA(hostExe.c_str(), buf.data(), nullptr, nullptr, FALSE,
|
|
CREATE_NO_WINDOW, nullptr, nullptr, &si, &pi)) {
|
|
std::cerr << "[SandboxVst3Host] CreateProcessA failed err=" << (int)GetLastError()
|
|
<< " ch=" << channel_ << std::endl;
|
|
return false;
|
|
}
|
|
CloseHandle(pi.hThread);
|
|
#ifdef _WIN32
|
|
if (childProc_) CloseHandle(childProc_);
|
|
childProc_ = pi.hProcess;
|
|
#endif
|
|
|
|
// Wait for the child heartbeat (Nexus load can take 30s+).
|
|
uint32_t hb = ipc_->base.heartbeat;
|
|
auto t0 = std::chrono::steady_clock::now();
|
|
while (std::chrono::duration_cast<std::chrono::seconds>(
|
|
std::chrono::steady_clock::now() - t0).count() < 60) {
|
|
if (!proc_alive(childProc_)) {
|
|
std::cerr << "[SandboxVst3Host] child exited during load ch=" << channel_ << std::endl;
|
|
return false;
|
|
}
|
|
if (ipc_->base.heartbeat != hb) {
|
|
alive_.store(true);
|
|
return true;
|
|
}
|
|
Sleep(200);
|
|
}
|
|
std::cerr << "[SandboxVst3Host] child heartbeat timeout ch=" << channel_ << std::endl;
|
|
return false;
|
|
}
|
|
|
|
void SandboxVst3Host::watchdogLoop() {
|
|
int fails = 0;
|
|
while (!stop_.load()) {
|
|
Sleep(500);
|
|
if (stop_.load()) break;
|
|
if (!alive_.load()) continue;
|
|
if (proc_alive(childProc_)) {
|
|
fails = 0;
|
|
continue;
|
|
}
|
|
alive_.store(false);
|
|
DWORD ec = 0; GetExitCodeProcess(childProc_, &ec);
|
|
std::cerr << "[SandboxVst3Host] child died ch=" << channel_ << " rc=" << ec << " — respawning" << std::endl;
|
|
++fails;
|
|
if (fails >= 3) {
|
|
std::cerr << "[SandboxVst3Host] ch=" << channel_
|
|
<< " respawn limit hit — muted until reload" << std::endl;
|
|
continue; // stays dead; processAudioBlock returns silence
|
|
}
|
|
Sleep(1000);
|
|
if (stop_.load()) break;
|
|
if (spawnChild()) {
|
|
fails = 0;
|
|
std::cerr << "[SandboxVst3Host] ch=" << channel_ << " respawned" << std::endl;
|
|
}
|
|
}
|
|
}
|
|
|
|
bool SandboxVst3Host::init(double sampleRate, uint32_t maxBlockSize) {
|
|
sampleRate_ = sampleRate;
|
|
block_ = maxBlockSize;
|
|
return true;
|
|
}
|
|
|
|
void SandboxVst3Host::noteOn(uint32_t channel, uint32_t pitch, float velocity, uint32_t sampleOffset) {
|
|
if (!alive_.load() || !ipc_) return;
|
|
shm_write_midi(shm_, 0x9, (uint8_t)channel, (uint8_t)pitch,
|
|
(uint8_t)(velocity * 127.0f), sampleOffset);
|
|
}
|
|
|
|
void SandboxVst3Host::noteOff(uint32_t channel, uint32_t pitch, uint32_t sampleOffset) {
|
|
if (!alive_.load() || !ipc_) return;
|
|
shm_write_midi(shm_, 0x8, (uint8_t)channel, (uint8_t)pitch, 0, sampleOffset);
|
|
}
|
|
|
|
void SandboxVst3Host::controlChange(uint32_t channel, uint32_t cc, uint32_t value) {
|
|
if (!alive_.load() || !ipc_) return;
|
|
shm_write_midi(shm_, 0xB, (uint8_t)channel, (uint8_t)cc, 0, 0, (uint8_t)value, 0);
|
|
}
|
|
|
|
void SandboxVst3Host::programChange(uint32_t channel, uint32_t program) {
|
|
if (!alive_.load() || !ipc_) return;
|
|
shm_write_midi(shm_, 0xC, (uint8_t)channel, 0, 0, 0, (uint8_t)program, 0);
|
|
}
|
|
|
|
void SandboxVst3Host::pitchBend(uint32_t channel, uint32_t bend14) {
|
|
if (!alive_.load() || !ipc_) return;
|
|
shm_write_midi(shm_, 0xE, (uint8_t)channel, 0, 0, 0,
|
|
(uint8_t)(bend14 & 0x7F), (uint8_t)((bend14 >> 7) & 0x7F));
|
|
}
|
|
|
|
void SandboxVst3Host::processAudioBlock(float* outputL, float* outputR, uint32_t numSamples) {
|
|
if (!alive_.load() || !ipc_) {
|
|
std::memset(outputL, 0, numSamples * sizeof(float));
|
|
std::memset(outputR, 0, numSamples * sizeof(float));
|
|
return;
|
|
}
|
|
// G4.3: the child publishes the slot it finished rendering (writeSlot).
|
|
// Copy that block. A stale slot means the child is still working on the
|
|
// other one — we repeat the previous block instead of tearing.
|
|
uint32_t slot = ipc_->writeSlot & 1u;
|
|
for (uint32_t i = 0; i < numSamples; ++i) {
|
|
outputL[i] = ipc_->audioLeft[slot][i];
|
|
outputR[i] = ipc_->audioRight[slot][i];
|
|
}
|
|
}
|