#include #include "PluginScannerSubprocess.hpp" #include "WorkerSubprocess.hpp" #include "CustomPluginScanner.hpp" #include "ConnectionWorker.hpp" #include "SafeWebsocketDeferredSend.h" #include "common.h" #include "properties.h" #include #include #include #include "json.h" #include #include #include "MacSupport.h" #include "SocketOwnership.hpp" #include "StateIntegrity.hpp" #include "Base64.h" #include "PluginSearchPathEditor.hpp" #include "DisableCrashReporter.hpp" #ifdef _MSC_VER #include "winsparkle.h" #include "winsparkle-key.h" #include "wincred.h" #include #include #include #endif extern "C" { #include "random.h" } using namespace juce; using namespace jsoncons; #ifdef _MSC_VER LRESULT CALLBACK KeyboardInputCallbackProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam); #endif class WorkerSuperprocess final : private ChildProcessCoordinator { public: WorkerSuperprocess(int bufferSize, int sampleRate, const std::string & bearerToken, const std::string_view & bridgeKeySeed, const std::string & architecture, std::function && portCallback, std::function && foregroundChangedCallback, std::function && disconnectCallback, std::function && trustQueryCallback, std::function && keyboardEnabledCallback) : portCallback(std::move(portCallback)), foregroundChangedCallback(std::move(foregroundChangedCallback)), disconnectCallback(std::move(disconnectCallback)), trustQueryCallback(std::move(trustQueryCallback)), keyboardEnabledCallback(std::move(keyboardEnabledCallback)), isForeground(false) { launchWorkerProcess (File::getSpecialLocation (File::currentExecutableFile), workerProcessUID, 8000, 0, architecture); auto message = std::make_unique("WorkerSubprocess"); message->setAttribute("bufferSize", bufferSize); message->setAttribute("sampleRate", sampleRate); message->setAttribute("bearerToken", bearerToken); message->setAttribute("bridgeKeySeed", convertToBase64(bridgeKeySeed)); message->setAttribute("architecture", architecture); auto xmlString = message->toString(); sendMessageToWorker({ xmlString.toRawUTF8(), xmlString.getNumBytesAsUTF8() }); } bool getIsForeground() const { return isForeground; } void sendKeyboardEnabled(bool value) { auto message = std::make_unique("KeyboardEnabled"); message->setAttribute("value", value); auto xmlString = message->toString(); sendMessageToWorker({ xmlString.toRawUTF8(), xmlString.getNumBytesAsUTF8() }); } void sendKeyboardActivity() { auto message = std::make_unique("KeyboardActivity"); auto xmlString = message->toString(); sendMessageToWorker({ xmlString.toRawUTF8(), xmlString.getNumBytesAsUTF8() }); } void sendShouldStayOnTop(bool value) { auto message = std::make_unique("ShouldStayOnTop"); message->setAttribute("value", value); auto xmlString = message->toString(); sendMessageToWorker({ xmlString.toRawUTF8(), xmlString.getNumBytesAsUTF8() }); } void sendTrustResolution(const XmlElement & trustStore) { auto message = std::make_unique("TrustResolution"); auto trustStoreCopy = std::make_unique(trustStore); message->addChildElement(trustStoreCopy.release()); auto xmlString = message->toString(); sendMessageToWorker({ xmlString.toRawUTF8(), xmlString.getNumBytesAsUTF8() }); } using ChildProcessCoordinator::sendMessageToWorker; private: void handleMessageFromWorker (const MemoryBlock& mb) override { auto elem = parseXML(mb.toString()); if(!elem) { return; } if(elem->getTagName() == "WorkerSubprocessStarted") { int port = elem->getIntAttribute("port"); if(port == 0 || !portCallback) { return; } portCallback(port); portCallback = {}; } else if(elem->getTagName() == "WorkerSubprocessForegroundChanged") { isForeground = elem->getBoolAttribute("foreground"); foregroundChangedCallback(); } else if(elem->getTagName() == "WorkerSubprocessTrustQuery") { trustQueryCallback(convertFromBase64(elem->getStringAttribute("publicKey"))); } else if (elem->getTagName() == "WorkerKeyboardEnabled") { keyboardEnabledCallback(elem->getBoolAttribute("value")); } } void handleConnectionLost() override { disconnectCallback(this); } std::function portCallback; std::function foregroundChangedCallback; std::function disconnectCallback; std::function trustQueryCallback; std::function keyboardEnabledCallback; bool isForeground; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (WorkerSuperprocess) }; class PluginScannerThread final : private Thread { public: PluginScannerThread(std::map> & currentLists, SearchPathSettings & searchPathSettings, std::list architectures, uWS::MoveOnlyFunction && progressCallback, uWS::MoveOnlyFunction>&)> && doneCallback, uWS::MoveOnlyFunction && errorCallback) : Thread("Plugin scanner thread"), currentLists(std::make_move_iterator(currentLists.begin()), std::make_move_iterator(currentLists.end())), searchPathSettings(searchPathSettings), architectures(std::move(architectures)), progressCallback(std::move(progressCallback)), doneCallback(std::move(doneCallback)), errorCallback(std::move(errorCallback)) { this->startThread(); } ~PluginScannerThread() { signalThreadShouldExit(); waitForThreadToExit(-1); } private: void run() override { try { realRun(); } catch(const std::exception & e) { errorCallback(e.what()); } catch(...) { errorCallback(""); } } struct ScanJob final : public ThreadPoolJob { ScanJob (PluginDirectoryScanner& pds, uWS::MoveOnlyFunction & progressCallback) : ThreadPoolJob ("pluginscan"), pds (pds), progressCallback(progressCallback) { } JobStatus runJob() { String pluginName; while(pds.scanNextFile(true, pluginName) && !shouldExit()) { if(pluginName.length() > 0) { progressCallback(pluginName.toStdString()); } pluginName.clear(); } return jobHasFinished; } PluginDirectoryScanner& pds; uWS::MoveOnlyFunction & progressCallback; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (ScanJob) }; void realRun() { constexpr const int NUM_JOBS = 4; auto manager = std::make_unique(); manager->addDefaultFormats(); std::list> formats; formats.emplace_back(std::make_unique()); formats.emplace_back(std::make_unique()); #ifndef _MSC_VER formats.emplace_back(std::make_unique()); #endif std::map> res; std::list alreadyHaveIdentifiers; for(auto & architecture : architectures) { auto pluginList = std::make_unique(); { auto it = currentLists.find(architecture); if(it != currentLists.end()) { pluginList->recreateFromXml(*it->second); } } std::cerr << "scan: Scanning for architecture " << architecture << " with blacklist:\n"; for(const auto & elem : pluginList->getBlacklistedFiles()) { std::cerr << "scan: " << elem << "\n"; } pluginList->setCustomScanner(std::make_unique(architecture, alreadyHaveIdentifiers)); for(auto & format : formats) { FileSearchPath searchPath; if(format->getName() == "VST") { if(searchPathSettings.useVstDefaultSearchPaths) { searchPath = format->getDefaultLocationsToSearch(); } for(auto & p : searchPathSettings.vstSearchPaths) { searchPath.add(File(p)); } } else if(format->getName() == "VST3") { if(searchPathSettings.useVst3DefaultSearchPaths) { searchPath = format->getDefaultLocationsToSearch(); } for(auto & p : searchPathSettings.vst3SearchPaths) { searchPath.add(File(p)); } } else { searchPath = format->getDefaultLocationsToSearch(); } searchPath.removeNonExistentPaths(); searchPath.removeRedundantPaths(); std::cout << "scan: Search path for " << format->getName() << ": " << searchPath.toString() << "\n"; PluginDirectoryScanner pds(*pluginList, *format, searchPath, true, File(), true); { ThreadPool pool{ ThreadPoolOptions{}.withNumberOfThreads (NUM_JOBS) }; for(int i = 0; i < NUM_JOBS; i++) { pool.addJob(new ScanJob(pds, progressCallback), true); } while(pool.getNumJobs() > 0 && !threadShouldExit()) { using namespace std::chrono_literals; std::this_thread::sleep_for(50ms); } pool.removeAllJobs(true, -1); if(threadShouldExit()) { throw std::runtime_error("Early thread termination requested"); } } } for(auto & desc : pluginList->getTypes()) { alreadyHaveIdentifiers.push_back(desc.fileOrIdentifier); } res.insert(std::make_pair(architecture, pluginList->createXml())); std::cerr << "scan: Scan finished for architecture " << architecture << " with blacklist:\n"; for(const auto & elem : pluginList->getBlacklistedFiles()) { std::cerr << "scan: " << elem << "\n"; } std::cerr << "---\n"; } doneCallback(res); } std::map> currentLists; SearchPathSettings searchPathSettings; std::list architectures; uWS::MoveOnlyFunction progressCallback; uWS::MoveOnlyFunction>&)> doneCallback; uWS::MoveOnlyFunction errorCallback; }; #ifndef _MSC_VER class BridgePluginTrayIconComponent final : public SystemTrayIconComponent { public: BridgePluginTrayIconComponent(MacSupport * macSupport) : macSupport(macSupport) { auto trayIconImageTemplate = juce::ImageCache::getFromMemory(BinaryData::DockTemplate32_png, BinaryData::DockTemplate32_pngSize); setIconImage(trayIconImageTemplate, trayIconImageTemplate); setIconTooltip("WavTool Bridge"); } void mouseDown(const MouseEvent & e) override { setHighlighted(true); macSupport->showStatusItemDropdownMenu(getNativeHandle()); } private: MacSupport * macSupport; }; #else class BridgePluginTrayIconComponent final : public SystemTrayIconComponent { public: BridgePluginTrayIconComponent(uWS::MoveOnlyFunction && clearTrustStore, uWS::MoveOnlyFunction && checkForUpdates, uWS::MoveOnlyFunction && editSearchPaths) : clearTrustStore(std::move(clearTrustStore)), checkForUpdates(std::move(checkForUpdates)), editSearchPaths(std::move(editSearchPaths)) { auto trayIconImage = juce::ImageCache::getFromMemory(BinaryData::AppIcon256_png, BinaryData::AppIcon256_pngSize); auto trayIconImageTemplate = juce::ImageCache::getFromMemory(BinaryData::DockTemplate32_png, BinaryData::DockTemplate32_pngSize); setIconImage(trayIconImage, trayIconImageTemplate); setIconTooltip("WavTool Bridge"); } void mouseDown(const MouseEvent& e) override { PopupMenu m; m.addItem(1, "Check for updates..."); m.addItem(2, "Clear trust store"); m.addItem(3, "Edit plugin search paths"); m.addItem(4, "Quit"); m.showMenuAsync(PopupMenu::Options(), [this](int result) { if (result == 1) { checkForUpdates(); } else if (result == 2) { clearTrustStore(); } else if (result == 3) { editSearchPaths(); } else if (result == 4) { JUCEApplicationBase::quit(); } }); } private: uWS::MoveOnlyFunction clearTrustStore; uWS::MoveOnlyFunction checkForUpdates; uWS::MoveOnlyFunction editSearchPaths; }; int wsc_can_shutdown(); void wsc_shutdown(); #endif #ifdef _MSC_VER class WavtoolWindowsKeyListener { public: WavtoolWindowsKeyListener(std::function&& keyHandler) : keyHandler(std::move(keyHandler)) { } void processEvent( uint8_t sc, bool e0, bool e1, bool keyup, uint8_t vk) { int combinedCode = sc; if (e0) { combinedCode |= 0xe000; } if (e1) { combinedCode |= 0xe100; } if (keyup) { auto it = heldKeys.find(combinedCode); if (it != heldKeys.end()) { if (it->second.has_value()) { auto desc = KeyDescription{ .keyDown = false, .keyDescription = it->second.value() }; keyHandler(desc); } heldKeys.erase(it); } } else { if (heldKeys.find(combinedCode) != heldKeys.end()) { return; } auto maybeDescStr = scanCodeToDescription(sc); if (maybeDescStr.has_value()) { auto desc = KeyDescription{ .keyDown = true, .keyDescription = maybeDescStr.value() }; keyHandler(desc); } heldKeys.insert(std::make_pair(combinedCode, maybeDescStr)); } } void reset() { for (auto& p : heldKeys) { if (!p.second.has_value()) { continue; } keyHandler(KeyDescription{ .keyDown = false, .keyDescription = p.second.value() }); } heldKeys.clear(); } private: bool isControlHeld() { return isAnyLowByteHeld(0x1d); } bool isAltHeld() { return isAnyLowByteHeld(0x38); } bool isShiftHeld() { return isAnyLowByteHeld(0x2a) || isAnyLowByteHeld(0x36); } bool isAnyLowByteHeld(uint8_t byte) { for (auto& p : heldKeys) { if ((p.first & 0xFF) == byte) { return true; } } return false; } std::optional scanCodeToDescriptionUnmodified(uint8_t sc) { if (sc >= 0x2 && sc <= 0xa) { return std::to_string(sc - 1); } switch (sc) { case 0x01: return "esc"; case 0x0b: return "0"; case 0x0c: case 0x4a: return "-"; case 0x0d: case 0x4e: return "+"; case 0x0e: return "backspace"; case 0x0f: return "tab"; case 0x10: return "q"; case 0x11: return "w"; case 0x12: return "e"; case 0x13: return "r"; case 0x14: return "t"; case 0x15: return "y"; case 0x16: return "u"; case 0x17: return "i"; case 0x18: return "o"; case 0x19: return "p"; case 0x1a: return "["; case 0x1b: return "]"; case 0x1c: return "enter"; case 0x1e: return "a"; case 0x1f: return "s"; case 0x20: return "d"; case 0x21: return "f"; case 0x22: return "g"; case 0x23: return "h"; case 0x24: return "j"; case 0x25: return "k"; case 0x26: return "l"; case 0x27: return ";"; case 0x28: return "'"; case 0x29: return "`"; case 0x2b: return "\\"; case 0x2c: return "z"; case 0x2d: return "x"; case 0x2e: return "c"; case 0x2f: return "v"; case 0x30: return "b"; case 0x31: return "n"; case 0x32: return "m"; case 0x33: return ","; case 0x34: return "."; case 0x35: return "/"; case 0x39: return "space"; case 0x47: return "home"; case 0x48: return "up"; case 0x4b: return "left"; case 0x4d: return "right"; case 0x50: return "down"; case 0x53: return "delete"; } return {}; } std::optional scanCodeToDescription(uint8_t sc) { auto unmod = scanCodeToDescriptionUnmodified(sc); if (!unmod.has_value()) { return {}; } std::string modifiers; if (isControlHeld()) { modifiers += "ctrl+"; } if (isAltHeld()) { modifiers += "alt+"; } if (isShiftHeld()) { modifiers += "shift+"; } return modifiers + unmod.value(); } std::map> heldKeys; std::function keyHandler; }; #endif class BridgePluginHost final : public JUCEApplication, AsyncUpdater, Timer { public: std::unique_ptr appProperties; BridgePluginHost() : websocketThread(this), #ifdef _MSC_VER rawKeyboardWnd(NULL), #endif alwaysOnTop(false), dAlwaysOnTop(false), trustQueryInProgress(false), isAnyProcessWindowForeground(false), noForegroundPollCount(0), dAlwaysOnTopPollCount(0), keyboardEnabled(true), dIsAnyForeground(false), dIsAnyForegroundPollCount(0) { supportedArchitectures.push_back("native"); #ifndef _MSC_VER if(File("/Library/Apple/usr/share/rosetta/rosetta").existsAsFile()) { supportedArchitectures.push_back(processIsTranslated() ? "arm64" : "x86_64"); } #endif } bool moreThanOneInstanceAllowed() override { auto params = getCommandLineParameters(); for (auto * uid : { scannerProcessUID, workerProcessUID }) { if (params.startsWith(String("--") + uid)) { return true; } } return false; } void initialise (const juce::String& commandLine) override { initializeAppProperties(); #ifndef _MSC_VER Process::setDockIconVisible(false); #endif auto scannerSubprocess = std::make_unique(); if (scannerSubprocess->initialiseFromCommandLine (commandLine, scannerProcessUID)) { subprocess = std::move (scannerSubprocess); return; } auto workerSubprocess = std::make_unique(); if (workerSubprocess->initialiseFromCommandLine (commandLine, workerProcessUID, 8000)) { subprocess = std::move (workerSubprocess); return; } enqueueThunk([this]{ #ifndef _MSC_VER macSupport = std::make_unique([this]{ clearTrustStore(); }, [this]{ openPluginSearchPathEditor(); }); auto bridgeKeySeedb64 = macSupport->generateOrLoadBridgeKeySeed([]{ return convertToBase64(StateIntegrity::generateSeed()); }); bridgeKeySeed = convertFromBase64(bridgeKeySeedb64); #else { PCREDENTIALW pcred; auto ok = ::CredReadW(L"WavTool/BridgeKeySeed", CRED_TYPE_GENERIC, 0, &pcred); if (ok) { bridgeKeySeed = convertFromBase64(std::string((char*)pcred->CredentialBlob, (size_t)pcred->CredentialBlobSize)); } else { bridgeKeySeed = StateIntegrity::generateSeed(); auto newSeedb64 = convertToBase64(bridgeKeySeed); CREDENTIALW cred = { 0 }; cred.Type = CRED_TYPE_GENERIC; cred.TargetName = (LPWSTR)L"WavTool/BridgeKeySeed"; cred.CredentialBlobSize = newSeedb64.size(); cred.CredentialBlob = (LPBYTE)newSeedb64.data(); cred.Persist = CRED_PERSIST_LOCAL_MACHINE; ok = ::CredWriteW(&cred, 0); if (!ok) { throw std::runtime_error("could not read or write credential"); } } } #endif stateIntegrity = std::make_unique(bridgeKeySeed); auto trustStore = appProperties->getUserSettings()->getXmlValue("trustStore"); if(trustStore) { try { stateIntegrity->loadTrustStore(*trustStore); } catch(const std::exception & e) { std::cerr << "Failed to load trust store from app settings: " << e.what() << "\n"; } } auto searchPathSettingsXml = appProperties->getUserSettings()->getXmlValue("searchPaths"); if(searchPathSettingsXml) { searchPathSettings.fromXml(searchPathSettingsXml.get()); } #ifndef _MSC_VER trayIconComponent = std::make_unique(macSupport.get()); #else trayIconComponent = std::make_unique([this] { clearTrustStore(); }, [this] { win_sparkle_check_update_with_ui(); }, [this] { openPluginSearchPathEditor(); }); win_sparkle_init(); win_sparkle_set_appcast_url("https://wavtool.com/bridge/win64/appcast.xml"); win_sparkle_set_dsa_pub_pem(WIN_SPARKLE_KEY); win_sparkle_set_can_shutdown_callback(wsc_can_shutdown); win_sparkle_set_shutdown_request_callback(wsc_shutdown); #endif loadPluginLists(pluginListByArchitecture); websocketThread.startThread(); #ifdef _MSC_VER attachKeyboardListener(); keyListener = std::make_unique([this](const KeyDescription& k) { { std::unique_lock lock(closeThunksMutex); for (auto& p : keyListenerSockets) { p.second->send(json_to_string(k), uWS::OpCode::TEXT); } } for (auto& sp : workerSuperprocs) { sp->sendKeyboardActivity(); } }); #endif startTimer(50); }); } void initializeAppProperties() { PropertiesFile::Options options; options.applicationName = "WavTool Bridge"; options.filenameSuffix = "settings"; options.osxLibrarySubFolder = "Preferences"; appProperties = std::make_unique(); appProperties->setStorageParameters (options); } void clearTrustStore() { if (!stateIntegrity) { return; } stateIntegrity->clearTrustStore(); auto trustStore = stateIntegrity->saveTrustStore(); appProperties->getUserSettings()->setValue("trustStore", trustStore.get()); appProperties->getUserSettings()->saveIfNeeded(); for (auto& superproc : workerSuperprocs) { superproc->sendTrustResolution(*trustStore); } } ~BridgePluginHost() { for(int i = 0; i < bridgeKeySeed.size(); i++) { bridgeKeySeed[i] = 0; } trayIconComponent.reset(); #ifdef _MSC_VER win_sparkle_cleanup(); #endif } void handleAsyncUpdate() override { for(;;) { uWS::MoveOnlyFunction thunk; { std::unique_lock lock(pendingThunksMutex); if(pendingThunks.empty()) { break; } thunk = std::move(pendingThunks.front()); pendingThunks.pop_front(); } thunk(); } } bool pollProcessWindowsForeground() { #ifndef _MSC_VER return false; #else HWND foregroundWindow = GetForegroundWindow(); if (foregroundWindow == NULL || !IsWindowVisible(foregroundWindow)) { return false; } TCHAR currentProcessPath[MAX_PATH]; GetModuleFileName(NULL, currentProcessPath, sizeof(currentProcessPath)); DWORD foregroundProcessId; if (GetWindowThreadProcessId(foregroundWindow, &foregroundProcessId) == 0) { return false; } HANDLE hProcess = OpenProcess(PROCESS_QUERY_INFORMATION, FALSE, foregroundProcessId); if (hProcess != NULL) { TCHAR foregroundProcessPath[MAX_PATH]; if (GetModuleFileNameEx(hProcess, NULL, foregroundProcessPath, sizeof(foregroundProcessPath))) { if (strcmp(foregroundProcessPath, currentProcessPath) == 0) { CloseHandle(hProcess); return true; } } CloseHandle(hProcess); } return false; #endif } void appDidEnterBackground() { #ifdef _MSC_VER if (keyListener) { keyListener->reset(); } #endif } template bool fallingEdgeDelay(T & current, bool next, int & poll, int threshold) { if (current && !next && poll++ >= threshold) { current = false; return true; } else if (next) { poll = 0; if (!current) { current = true; return true; } } return false; } void timerCallback() override { bool shouldUpdate = false; if(fallingEdgeDelay(isAnyProcessWindowForeground, pollProcessWindowsForeground(), noForegroundPollCount, 4)) { shouldUpdate = true; appDidEnterBackground(); } if(fallingEdgeDelay(dAlwaysOnTop, alwaysOnTop, dAlwaysOnTopPollCount, 6)) { shouldUpdate = true; } bool isAnyForeground = false; for(auto & superproc : workerSuperprocs) { if(superproc->getIsForeground()) { isAnyForeground = true; break; } } if(fallingEdgeDelay(dIsAnyForeground, isAnyForeground, dIsAnyForegroundPollCount, 4)) { shouldUpdate = true; } if (shouldUpdate) { updateProcessGroupForeground(); } } void updateProcessGroupForeground() { std::cout << "updateProcessGroupForeground: dAlwaysOnTop = " << dAlwaysOnTop << ", dIsAnyForeground = " << dIsAnyForeground << ", IsAnyProcessWindowForeground = " << isAnyProcessWindowForeground << ", numSuperprocs = " << workerSuperprocs.size() << "\n"; for(auto & superproc : workerSuperprocs) { superproc->sendShouldStayOnTop(dIsAnyForeground || isAnyProcessWindowForeground || dAlwaysOnTop); } } std::string generateBearerToken() { constexpr const char HEX_TABLE[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; constexpr const size_t TOKEN_BYTES = 32; unsigned char buf[TOKEN_BYTES]; if(random_bytes(buf, TOKEN_BYTES) != 0) { throw std::runtime_error("Failed to generate bearer token"); } std::string hexbuf(2 * TOKEN_BYTES, ' '); for(size_t i = 0; i < TOKEN_BYTES; i++) { hexbuf[2*i] = HEX_TABLE[buf[i] / 16]; hexbuf[2*i+1] = HEX_TABLE[buf[i] % 16]; } return hexbuf; } struct trustQueryInProgressRaii { trustQueryInProgressRaii(bool & valRef) : valRef(valRef) { valRef = true; } ~trustQueryInProgressRaii() { valRef = false; } private: bool & valRef; }; void trustQuery(const std::string_view & publicKey) { enqueueThunk([this,publicKey=std::string(publicKey)]() mutable { if (trustQueryInProgress) { pendingTrustQueries.emplace_back(std::move(publicKey)); return; } trustQueryOnce(std::move(publicKey)); }); } void trustQueryOnce(std::string && publicKey) { trustQueryInProgress = true; auto processNext = [this]() { enqueueThunk([this] { auto trustStore = stateIntegrity->saveTrustStore(); for (auto& superproc : workerSuperprocs) { superproc->sendTrustResolution(*trustStore); } if (pendingTrustQueries.empty()) { trustQueryInProgress = false; } else { auto nextPublicKey = std::move(pendingTrustQueries.front()); pendingTrustQueries.pop_front(); trustQueryOnce(std::move(nextPublicKey)); } }); }; if (stateIntegrity->getKeyTrust(publicKey).has_value()) { processNext(); } else { // first time this key has been seen - ask user auto continuation = [this, publicKey = std::move(publicKey), processNext=std::move(processNext)](int shouldTrust) mutable { enqueueThunk([this, publicKey = std::move(publicKey), processNext=std::move(processNext), shouldTrust] { std::cerr << "User set trust value of public key " << convertToBase64(publicKey) << " to " << shouldTrust << "\n"; stateIntegrity->updateKeyTrust(publicKey, shouldTrust); auto trustStore = stateIntegrity->saveTrustStore(); appProperties->getUserSettings()->setValue("trustStore", trustStore.get()); appProperties->getUserSettings()->saveIfNeeded(); processNext(); }); }; #ifndef _MSC_VER continuation(macSupport->showTrustQuery()); #else NativeMessageBox::showYesNoBox( MessageBoxIconType::WarningIcon, "WavTool Bridge", "WavTool is attempting to load VST settings from an unrecognized source.\n\n" "Loading untrusted VST settings may lead to compromise of your device. " "Be sure you trust the author of the project before continuing.\n\n" "Load untrusted settings?", nullptr, ModalCallbackFunction::create(std::move(continuation))); #endif } } struct ConnectionUserData { std::unique_ptr safeSend; std::unique_ptr pluginScannerThread; std::string peerAddress; int peerPort; }; template void handleControlMessage(const std::string_view & message, WebSocket * ws) { auto j = decode_json(message); auto userData = ws->getUserData(); auto safeSend = userData->safeSend->get(); switch(j.op) { case ClientOp::listPlugins: case ClientOp::scanPlugins: { enqueueThunk([=,this]{ if(j.op == ClientOp::scanPlugins) { clearSavedPluginLists(); } loadPluginLists(pluginListByArchitecture); std::map> spl; for(auto & p : pluginListByArchitecture) { spl.insert(std::make_pair(p.first, p.second->createXml())); } userData->pluginScannerThread = std::make_unique(spl, searchPathSettings, supportedArchitectures, [=,this](const std::string_view & currentlyScanning) { safeSend->send(json_to_string(ProgressResponse{.progress = std::string(currentlyScanning)}), uWS::OpCode::TEXT); }, [=,this](std::map> & newLists) mutable { enqueueThunk([=,this,newLists=std::remove_reference::type(std::make_move_iterator(newLists.begin()), std::make_move_iterator(newLists.end()))] { pluginListByArchitecture.clear(); for(auto & elem : newLists) { auto newList = std::make_unique(); newList->recreateFromXml(*elem.second); pluginListByArchitecture.insert(std::make_pair(elem.first, std::move(newList))); } savePluginLists(pluginListByArchitecture); safeSend->send(json_to_string(flattenPluginLists(pluginListByArchitecture)), uWS::OpCode::TEXT); }); }, [=,this](const std::string_view & errorView) { std::string error(errorView); enqueueThunk([=,this,error=std::move(error)]{ safeSend->send(json_to_string(ErrorResponse{.error = error}), uWS::OpCode::TEXT); }); }); }); break; } case ClientOp::resolvePlugin: { auto j2 = decode_json(message); ClientResolvePluginResponse exactMatch; exactMatch.found = false; ClientResolvePluginResponse nameManufacturerMatch; nameManufacturerMatch.found = false; for(auto & p : pluginListByArchitecture) { const auto & [architecture, pluginList] = p; for(auto & t : pluginList->getTypes()) { if(t.fileOrIdentifier.toStdString() == j2.identifier) { if(!exactMatch.found || architecture == "native") { // prefer native exactMatch.identifier = t.fileOrIdentifier.toStdString(); exactMatch.architecture = architecture; exactMatch.found = true; } } if(t.name.toStdString() == j2.name && t.manufacturerName.toStdString() == j2.manufacturer) { if(!nameManufacturerMatch.found || architecture == "native") { nameManufacturerMatch.identifier = t.fileOrIdentifier.toStdString(); nameManufacturerMatch.architecture = architecture; nameManufacturerMatch.found = true; } } } } if(exactMatch.found) { ws->send(json_to_string(exactMatch), uWS::OpCode::TEXT); } else if(nameManufacturerMatch.found) { ws->send(json_to_string(nameManufacturerMatch), uWS::OpCode::TEXT); } else { ws->send(json_to_string(ErrorResponse{.error = "Plugin could not be resolved"}), uWS::OpCode::TEXT); } break; } case ClientOp::startWorker: { auto j2 = decode_json(message); auto bearerToken = generateBearerToken(); auto superproc = std::make_unique(j2.bufferSize, j2.sampleRate, bearerToken, bridgeKeySeed, j2.architecture, [=](int port) { safeSend->send(json_to_string(WorkerStartedResponse{.port=port, .bearerToken=bearerToken}), uWS::OpCode::TEXT); }, [this]() { enqueueThunk([=]{ updateProcessGroupForeground(); }); }, [this](WorkerSuperprocess * which) { enqueueThunk([=]{ for(auto it = workerSuperprocs.begin(); it != workerSuperprocs.end(); it++) { if(it->get() == which) { workerSuperprocs.erase(it); break; } } updateProcessGroupForeground(); }); }, [this](const std::string_view & publicKey) { trustQuery(publicKey); }, [this](bool newKeyboardEnabled) { enqueueThunk([this,newKeyboardEnabled] { setKeyboardEnabled(newKeyboardEnabled); }); }); enqueueThunk([this,superproc=std::move(superproc)]() mutable { workerSuperprocs.emplace_back(std::move(superproc)); updateProcessGroupForeground(); sendKeyboardEnabled(); }); break; } case ClientOp::setAlwaysOnTop: { auto j2 = decode_json(message); alwaysOnTop = j2.alwaysOnTop; updateProcessGroupForeground(); break; } default: ws->send(json_to_string(ErrorResponse{.error = "Operation must be handled by a worker"}), uWS::OpCode::TEXT); break; } } class WebsocketThread : private Thread { public: WebsocketThread(BridgePluginHost * host) : Thread("Websocket thread"), host(host) { } BridgePluginHost * host; std::function closeAll; void run() override { auto app = uWS::App().ws("/*", { /* Settings */ .maxPayloadLength = 100 * 1024 * 1024, .idleTimeout = 16, .maxBackpressure = 100 * 1024 * 1024, .closeOnBackpressureLimit = false, .resetIdleTimeoutOnSend = false, .sendPingsAutomatically = true, /* Handlers */ .upgrade = [this](auto *res, auto *req, auto *context) { if(!validateOriginHeader(req)) { res->close(); return; } res->template upgrade({}, req->getHeader("sec-websocket-key"), req->getHeader("sec-websocket-protocol"), req->getHeader("sec-websocket-extensions"), context); }, .open = [this](auto * ws) { auto ctx = ws->getUserData(); auto [peerAddress, peerPort] = getPeerAddressAndPort(ws); ctx->peerAddress = peerAddress; ctx->peerPort = peerPort; if(peerAddress != "127.0.0.1") { ws->end(); return; } #ifdef _MSC_VER auto maybeMySsid = getStringSidForProcess(GetCurrentProcess()); auto maybeSsid = findLocalhostRemoteSocketOwner(peerPort); if(!maybeMySsid.has_value() || !maybeSsid.has_value() || maybeMySsid.value() != maybeSsid.value()) { ws->end(); return; } #else auto uid = findLocalhostRemoteSocketOwner(peerPort); if(!uid.has_value() || uid.value() != getuid()) { ws->end(); return; } #endif ctx->safeSend.reset(new SafeWebsocketDeferredSendT(ws, uWS::Loop::get())); std::unique_lock lock(host->closeThunksMutex); host->closeThunks.emplace(std::make_pair((uintptr_t)ws, [=]{ ws->end(); })); host->keyListenerSockets.emplace(std::make_pair((uintptr_t)ws, ctx->safeSend->get())); std::cerr << "New connection from " << peerAddress << ":" << peerPort << "\n"; ws->send("🫡 hello fellow nerds (protocol 2)", uWS::OpCode::TEXT); }, .message = [this](auto *ws, std::string_view message, uWS::OpCode opCode) { if(opCode == uWS::OpCode::TEXT) { // control message try { host->handleControlMessage(message, ws); } catch(std::exception & e) { ws->send(json_to_string(ErrorResponse{.error = e.what()}), uWS::OpCode::TEXT); } } else if(opCode == uWS::OpCode::BINARY) { ws->send(json_to_string(ErrorResponse{.error = "Binary frames can only be handled by a worker"}), uWS::OpCode::TEXT); } }, .dropped = [](auto */*ws*/, std::string_view /*message*/, uWS::OpCode /*opCode*/) { /* A message was dropped due to set maxBackpressure and closeOnBackpressureLimit limit */ }, .drain = [](auto */*ws*/) { /* Check ws->getBufferedAmount() here */ }, .ping = [](auto */*ws*/, std::string_view) { /* Not implemented yet */ }, .pong = [](auto */*ws*/, std::string_view) { /* Not implemented yet */ }, .close = [this](auto * ws, int /*code*/, std::string_view /*message*/) { std::unique_lock lock(host->closeThunksMutex); auto it = host->closeThunks.find((uintptr_t)ws); if(it != host->closeThunks.end()) { host->closeThunks.erase(it); } auto it2 = host->keyListenerSockets.find((uintptr_t)ws); if (it2 != host->keyListenerSockets.end()) { host->keyListenerSockets.erase(it2); } auto ctx = ws->getUserData(); std::cerr << "Connection closed: " << ctx->peerAddress << ":" << ctx->peerPort << "\n"; } }); app.any("/*", [](auto *res, auto *req) { res->close(); }); bool listening = false; for(int p = 9002; p < 9007 && !listening; p++) { app.listen("127.0.0.1", p, LIBUS_LISTEN_EXCLUSIVE_PORT, [&,this](auto *listen_socket) { if (listen_socket) { std::cerr << "listening on " << p << "\n"; listening = true; auto eventLoop = uWS::Loop::get(); closeAll = [=,this]{ eventLoop->defer([=,this]{ us_listen_socket_close(0, listen_socket); std::list> closeThunks; { std::unique_lock lock(host->closeThunksMutex); for(auto & pair : host->closeThunks) { closeThunks.emplace_back(std::move(pair.second)); } } for(auto & func : closeThunks) { func(); } }); }; } }); } if(!listening) { throw std::runtime_error("Failed to listen on any port in the range"); } app.run(); } friend class BridgePluginHost; }; friend class WebsocketThread; // we borrow this from uWS because JUCE doesn't support c++23 yet void enqueueThunk(uWS::MoveOnlyFunction && thunk) { { std::unique_lock lock(pendingThunksMutex); pendingThunks.emplace_back(std::move(thunk)); } triggerAsyncUpdate(); } void shutdown() override { #ifdef _MSC_VER detachKeyboardListener(); #endif if(websocketThread.isThreadRunning()) { websocketThread.closeAll(); websocketThread.waitForThreadToExit(5000); } if(websocketThread.isThreadRunning()) { // ugh... exitImmediately(); } } const juce::String getApplicationName() override { return "WavTool Bridge"; } const juce::String getApplicationVersion() override { #define WAVTOOL_STRINGIFY(x) #x #define WAVTOOL_TOSTRING(x) WAVTOOL_STRINGIFY(x) return WAVTOOL_TOSTRING(JUCE_APP_VERSION); #undef WAVTOOL_TOSTRING #undef WAVTOOL_STRINGIFY } void unhandledException(const std::exception * e, const String & file, int line) override { if(e) { std::cerr << "unhandled exception: " << e->what() << " raised at " << file << ":" << line << "\n"; } else { std::cerr << "unhandled exception raised at " << file << ":" << line << "\n"; } std::terminate(); } void setKeyboardEnabled(bool isEnabled) { std::cerr << "setKeyboardEnabled: " << isEnabled << "\n"; if (keyboardEnabled != isEnabled) { keyboardEnabled = isEnabled; sendKeyboardEnabled(); } #ifdef _MSC_VER if (!keyboardEnabled && keyListener) { keyListener->reset(); } #endif } void sendKeyboardEnabled() { for (auto& superproc : workerSuperprocs) { superproc->sendKeyboardEnabled(keyboardEnabled); } } #ifdef _MSC_VER void processKeyboardEvent( uint8_t sc, bool e0, bool e1, bool keyup, uint8_t vk ) { if (!isAnyProcessWindowForeground || !keyboardEnabled) { return; } if (keyListener) { keyListener->processEvent(sc, e0, e1, keyup, vk); } } void attachKeyboardListener() { //define a window class which is required to receive RAWINPUT events WNDCLASSEX wc; ZeroMemory(&wc, sizeof(WNDCLASSEX)); wc.cbSize = sizeof(WNDCLASSEX); wc.lpfnWndProc = KeyboardInputCallbackProc; wc.hInstance = GetModuleHandle(NULL); wc.lpszClassName = "wavtool_wndclass"; // register class if (!RegisterClassExA(&wc)) return; // create window rawKeyboardWnd = CreateWindowExA(0, wc.lpszClassName, NULL, 0, 0, 0, 0, 0, HWND_MESSAGE, NULL, GetModuleHandle(NULL), NULL); if (!rawKeyboardWnd) return; // setup raw input device sink RAWINPUTDEVICE devs = { 0x01 /* generic */, 0x06 /* keyboard */, RIDEV_INPUTSINK, rawKeyboardWnd }; if (RegisterRawInputDevices(&devs, 1, sizeof(RAWINPUTDEVICE)) == FALSE) return; } void detachKeyboardListener() { if (rawKeyboardWnd) { DestroyWindow(rawKeyboardWnd); } UnregisterClass("wavtool_wndclass", GetModuleHandle(NULL)); } #endif void openPluginSearchPathEditor() { if(!pluginSearchPathEditor) { pluginSearchPathEditor = std::make_unique(searchPathSettings, [this]{ pluginSearchPathEditor.reset(); }, [this] { auto searchPathSettingsXml = searchPathSettings.toXml(); appProperties->getUserSettings()->setValue("searchPaths", searchPathSettingsXml.get()); }); } pluginSearchPathEditor->toFront(true); } private: #ifndef _MSC_VER std::unique_ptr macSupport; #endif std::unique_ptr subprocess; std::map> pluginListByArchitecture; WebsocketThread websocketThread; #ifdef _MSC_VER HWND rawKeyboardWnd; std::unique_ptr keyListener; #endif std::list> pendingThunks; std::mutex pendingThunksMutex; std::list> workerSuperprocs; std::mutex closeThunksMutex; std::map> closeThunks; std::map < uintptr_t, std::shared_ptr> keyListenerSockets; std::unique_ptr trayIconComponent; std::atomic alwaysOnTop; std::atomic dAlwaysOnTop; std::string bridgeKeySeed; std::unique_ptr stateIntegrity; bool trustQueryInProgress; std::deque pendingTrustQueries; std::list supportedArchitectures; bool isAnyProcessWindowForeground; int noForegroundPollCount; int dAlwaysOnTopPollCount; bool keyboardEnabled; std::atomic dIsAnyForeground; int dIsAnyForegroundPollCount; SearchPathSettings searchPathSettings; std::unique_ptr pluginSearchPathEditor; }; ApplicationProperties& getAppProperties() { return *(dynamic_cast(JUCEApplication::getInstance())->appProperties); } #ifdef _MSC_VER int wsc_can_shutdown() { return 1; } void wsc_shutdown() { JUCEApplicationBase::quit(); } LRESULT CALLBACK KeyboardInputCallbackProc(HWND window, UINT message, WPARAM wparam, LPARAM lparam) { if (message != WM_INPUT) return DefWindowProc(window, message, wparam, lparam); char rid_buf[64]; UINT rid_size = sizeof(rid_buf); if (GetRawInputData((HRAWINPUT)lparam, RID_INPUT, rid_buf, &rid_size, sizeof(RAWINPUTHEADER))) { RAWINPUT* raw = (RAWINPUT*)rid_buf; if (raw->header.dwType == RIM_TYPEKEYBOARD) { RAWKEYBOARD* rk = &raw->data.keyboard; dynamic_cast(JUCEApplication::getInstance())->processKeyboardEvent( rk->MakeCode, rk->Flags & RI_KEY_E0, rk->Flags & RI_KEY_E1, rk->Flags & RI_KEY_BREAK, rk->VKey ); } } return DefWindowProc(window, message, wparam, lparam); } #endif START_JUCE_APPLICATION (BridgePluginHost) #if defined (_MSC_VER) && ! defined (NDEBUG) int main() { return WinMain(0, 0, 0, 0); } #endif