#include "WorkerSubprocess.hpp" #include "ConnectionWorker.hpp" #include "SafeWebsocketDeferredSend.h" #include "common.h" #include "json.h" #include #include #include #include "SocketOwnership.hpp" #include "RTLogger.hpp" #include "Base64.h" #include "DisableCrashReporter.hpp" using namespace juce; using namespace jsoncons; // PROCESS_BLOCK wire format: // FROM client: // frameId: uint32 // midi: // repeated: // samplePosition: int32 where -1 breaks repetition // eventLength: byte // eventBytes: byte[eventLength] // automationCount: uint32 // parameterSampleStride: uint32 // repeated automationCount times: // parameterIndex: uint32 // automation: // runarray(samplePosition, 1): indexed by time of change in input samples for this block // value: float32 // inputCount: byte number of stereo inputs // outputCount: byte number of stereo outputs expected // sampleCount: uint32 number of samples per input channel // samples: float32[inputCount * sampleCount * 2] planar input-major // TO client: // frameId: uint32 // midi: // repeated: // samplePosition: int32 where -1 breaks repetition // eventLength: byte // eventBytes: byte[eventLength] // samples: float32[outputCount * sampleCount * 2] planar output-major // // runarray(index, increment): enclosed: // repeated: // startIndex: int32 indexing index where -1 breaks repetition // numItems: uint32 // repeated numItems times: // enclosed @ startIndex + repetitionCount * increment struct WInputStream { WInputStream(uint8_t * data, size_t len) : data(data), len(len), pos(0) { } void boundsCheck(bool expr) { if(!expr) { throw std::runtime_error("message too short"); } } template T readT() { boundsCheck(pos + sizeof(T) <= len); T res = 0; memcpy(&res, data + pos, sizeof(T)); pos += sizeof(T); return res; } template<> uint8_t readT() { boundsCheck(pos < len); return data[pos++]; } void readBlock(uint8_t *dest, size_t count) { boundsCheck(pos + count <= len); memcpy(dest, data + pos, count); pos += count; } // F(int32 index) -> void template void readRunarray(uint32_t increment, F && callback) { while(pos < len) { auto startIndex = readT(); if(startIndex == -1) { break; } auto numItems = readT(); for(uint32_t i = 0; i < numItems; i++) { callback(startIndex + i * increment); } } } size_t remain() { if(pos > len) { throw std::runtime_error("pos exceeds len"); } return len - pos; } uint8_t * data; size_t len; size_t pos; }; void processblock_wire_to_shm(ShmData & res, const std::string_view & data) { // Be careful with memory access here! `data' should be treated as malicious WInputStream stream((uint8_t*)data.data(), data.size()); res.messageType = ShmMessageType::PROCESS_BLOCK; auto & pb = res.processBlock; pb.frameId = stream.readT(); // MIDI { pb.numMidiEvents = 0; for(;;) { auto samplePos = stream.readT(); if(samplePos == -1) { break; } if(pb.numMidiEvents >= MAX_MIDI_EVENTS) { throw std::runtime_error("too many midi events"); } auto & evt = pb.midiEvents[pb.numMidiEvents++]; evt.sampleIndex = (uint32_t)samplePos; evt.length = stream.readT(); if(evt.length > MAX_MIDI_EVENT_LENGTH) { throw std::runtime_error("midi event too long"); } stream.readBlock(evt.data, evt.length); } } // Automations { pb.numParameterAutomations = stream.readT(); if(pb.numParameterAutomations > MAX_PARAMETER_AUTOMATIONS) { throw std::runtime_error("too many parameter automations"); } pb.parameterSampleStride = stream.readT(); for(size_t i = 0; i < pb.numParameterAutomations; i++) { auto & aut = pb.automations[i]; aut.parameterIndex = stream.readT(); size_t lastIdx = 0; float lastVal = 0.f; stream.readRunarray(1, [&](uint32_t sampleIndex){ if(sampleIndex >= MAX_BUFFER_SIZE) { throw std::runtime_error("parameter automation too long"); } for(size_t j = lastIdx; j < sampleIndex; j++) { aut.values[j] = lastVal; } lastIdx = sampleIndex; lastVal = stream.readT(); aut.values[sampleIndex] = lastVal; return true; }); for(size_t j = lastIdx + 1; j < MAX_BUFFER_SIZE; j++) { aut.values[j] = lastVal; } } } // Audio pb.numInputs = stream.readT(); pb.numOutputs = stream.readT(); if(pb.numInputs > MAX_CHANNEL_PAIRS || pb.numOutputs > MAX_CHANNEL_PAIRS) { throw std::runtime_error("too many ios"); } pb.numSamplesPerChannel = stream.readT(); if(pb.numSamplesPerChannel > MAX_BUFFER_SIZE) { throw std::runtime_error("too many samples per channel"); } for(size_t i = 0; i < pb.numInputs; i++) { auto & inp = pb.channelss[i]; for(size_t j = 0; j < 2; j++) { stream.readBlock((uint8_t*)inp.samples[j], pb.numSamplesPerChannel * sizeof(float)); } } } std::string_view shm_to_wire(ShmData & data) { static std::string res; if(data.messageType == ShmMessageType::PROCESS_BLOCK) { auto & pb = data.processBlock; const auto channelBytes = pb.numSamplesPerChannel * sizeof(float); size_t midiBytes = sizeof(int32_t); for(size_t i = 0; i < pb.numMidiEvents; i++) { midiBytes += sizeof(int32_t) + 1 + pb.midiEvents[i].length; } const uint32_t frameId = pb.frameId; const size_t requiredSize = channelBytes * pb.numOutputs * 2 + sizeof(frameId) + midiBytes; if(res.size() < requiredSize) { res.resize(requiredSize, 0); } uint8_t * data = (uint8_t*)res.data(); memcpy(res.data(), &frameId, sizeof(frameId)); data += sizeof(frameId); for(size_t i = 0; i < pb.numMidiEvents; i++) { const auto & evt = pb.midiEvents[i]; int32_t samplePos = (int32_t)evt.sampleIndex; memcpy(data, &samplePos, sizeof(samplePos)); data += sizeof(samplePos); *(data++) = evt.length; memcpy(data, evt.data, evt.length); data += evt.length; } const int32_t endMarker = -1; memcpy(data, &endMarker, sizeof(endMarker)); data += sizeof(endMarker); for(size_t i = 0; i < pb.numOutputs; i++) { for(size_t j = 0; j < 2; j++) { memcpy(data, pb.channelss[i].samples[j], channelBytes); data += channelBytes; } } return std::string_view(res.begin(), res.begin() + requiredSize); } else if(data.messageType == ShmMessageType::SET_TRANSPORT) { // TODO avoid allocation auto s = json_to_string(PluginLatencyResponse{.samplesLatency = data.setTransport.samplesLatency}); if(res.size() < s.size()) { res.resize(s.size(), 0); } std::copy(s.begin(), s.end(), res.begin()); return std::string_view(res.begin(), res.begin() + s.size()); } else { throw std::runtime_error("shm_to_wire: unknown messageType"); } } class WebsocketThread : private Thread { public: WebsocketThread(int bufferSize, int sampleRate, const std::string_view & bearerToken, const std::string_view & bridgeKeySeed, const std::string_view & architecture, std::function && portCallback, std::function && disarmTimeoutCallback, std::function && doneCallback, std::function && trustHandler, std::function && launchingChangedHandler, std::function && keyboardEnabledHandler) : Thread("Websocket thread"), bufferSize(bufferSize), sampleRate(sampleRate), bearerToken(bearerToken), shouldShutdown(false), listenSocket(nullptr), clientSocket(nullptr), eventLoop(nullptr), portCallback(std::move(portCallback)), disarmTimeoutCallback(std::move(disarmTimeoutCallback)), doneCallback(std::move(doneCallback)), trustHandler(std::move(trustHandler)), launchingChangedHandler(std::move(launchingChangedHandler)), keyboardEnabledHandler(std::move(keyboardEnabledHandler)), initialShouldStayOnTop(false), initialKeyboardEnabled(false), bridgeKeySeed(bridgeKeySeed), architecture(architecture) { shm.shouldReset = false; } void shutdown() { std::unique_lock lock(connectionStateMutex); shouldShutdown = true; if(!eventLoop) { return; } eventLoop->defer([this]{ std::unique_lock lock(connectionStateMutex); if(listenSocket) { us_listen_socket_close(0, listenSocket); listenSocket = nullptr; } if(clientSocket) { us_socket_close(0, clientSocket, 0, nullptr); clientSocket = nullptr; } }); } void setShouldStayOnTop(bool value) { std::unique_lock lock(initialShouldStayOnTopMutex); if(!worker) { initialShouldStayOnTop = value; return; } worker->setPluginWindowAlwaysOnTop(value); } void setKeyboardEnabled(bool value) { std::unique_lock lock(initialShouldStayOnTopMutex); if (!worker) { initialKeyboardEnabled = value; return; } worker->setKeyboardEnabled(value); } void keyboardActivity() { worker->keyboardActivity(); } void trustResolution(const XmlElement & elem) { if(!worker) { return; } worker->trustResolution(elem); } private: std::string id; struct ConnectionUserData { std::unique_ptr safeSend; }; using clock_t = std::chrono::high_resolution_clock; void run() override { try { uint64_t tid; #ifdef _MSC_VER tid = GetCurrentThreadId(); #else pthread_threadid_np(NULL, &tid); #endif std::stringstream ss; ss << tid; id = ss.str(); realRun(); } catch(const std::exception & e) { RTLogger::log("Exception in realtime thread %s: %s", id.c_str(), e.what()); } catch(...) { RTLogger::log("Exception in realtime thread %s (non-std)", id.c_str()); } { std::unique_lock lock(connectionStateMutex); eventLoop = nullptr; } doneCallback(); doneCallback = {}; } void realRun() { RTLogger::log("Realtime thread %s staring: bufferSize=%d, sampleRate=%d", id.c_str(), bufferSize, sampleRate); uWS::Loop::get()->setLockFree(true); loadPluginLists(pluginListByArchitecture); 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; } std::string accessTokenProtocol = "access_token"; std::set protocols; { auto wsProtocol = std::string(req->getHeader("sec-websocket-protocol")); std::regex protoSplitRegex(",\\s*"); std::sregex_token_iterator iter(wsProtocol.begin(), wsProtocol.end(), protoSplitRegex, -1); std::sregex_token_iterator end; for (; iter != end; ++iter) { protocols.insert(*iter); } } if(protocols.find(accessTokenProtocol) == protocols.end() || protocols.find(bearerToken) == protocols.end()) { res->writeStatus("403 Forbidden")->end(); return; } res->template upgrade({}, req->getHeader("sec-websocket-key"), accessTokenProtocol, req->getHeader("sec-websocket-extensions"), context); }, .open = [=,this](auto * ws) { std::unique_lock lock(connectionStateMutex); if(shouldShutdown) { ws->end(); return; } auto ctx = ws->getUserData(); auto [peerAddress, peerPort] = getPeerAddressAndPort(ws); this->peerAddress = peerAddress; this->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())); disarmTimeoutCallback(); disarmTimeoutCallback = {}; if(listenSocket) { us_listen_socket_close(0, listenSocket); listenSocket = nullptr; } clientSocket = (us_socket_t*)ws; RTLogger::log("Realtime thread %s new connection from %s:%d", id.c_str(), peerAddress.c_str(), peerPort); }, .message = [this](auto *ws, std::string_view message, uWS::OpCode opCode) { auto startTime = clock_t::now(); if(opCode == uWS::OpCode::TEXT) { // control message try { 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) { // midi + audio frames if(!worker) { return; } processblock_wire_to_shm(shm, message); worker->handleBlock(shm); ws->send(shm_to_wire(shm), uWS::OpCode::BINARY); } observeMessageHandlerDuration(clock_t::now() - startTime); }, .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*/) { // TODO RTLogger::log("Realtime thread %s connection closed: %s:%d", id.c_str(), peerAddress.c_str(), peerPort); } }); app.any("/*", [](auto *res, auto *req) { res->close(); }); { std::unique_lock lock(connectionStateMutex); if(!shouldShutdown) { eventLoop = uWS::Loop::get(); listenSocket = nullptr; for(int p = 9020; p < 9050 && !listenSocket; p++) { app.listen("127.0.0.1", p, LIBUS_LISTEN_EXCLUSIVE_PORT, [&,this](auto *listen_socket) { if (listen_socket) { RTLogger::log("Realtime thread %s listening on %d", id.c_str(), p); listenSocket = listen_socket; portCallback(p); portCallback = {}; } }); } } else { return; } } if(!listenSocket) { throw std::runtime_error("Failed to listen on any port in range"); } app.run(); } void observeMessageHandlerDuration(const clock_t::duration & dt) { static clock_t::time_point last_report_time = clock_t::now(); static float tavg = 0.f, tmin = std::numeric_limits::infinity(), tmax = 0.f; auto dtMs = (float)std::chrono::duration_cast(dt).count() / 1000.f; tavg += dtMs; tmin = std::min(tmin, dtMs); tmax = std::max(tmax, dtMs); if(clock_t::now() - last_report_time > std::chrono::seconds(5)) { RTLogger::log("RT %s: avg %.2fms, min %.2fms, max %.2fms", id.c_str(), tavg / 1000.0, (double)tmin, (double)tmax); tavg = 0.f; tmin = std::numeric_limits::infinity(); tmax = 0.f; last_report_time = clock_t::now(); } } template void handleControlMessage(const std::string_view & message, WebSocket * ws) { auto j = decode_json(message); auto safeSend = ws->getUserData()->safeSend->get(); switch(j.op) { case ClientOp::listInstanceParameters: { // ... break; } case ClientOp::setTransportState: { if(!worker) { ws->send(json_to_string(ErrorResponse{.error = "No plugin instance"}), uWS::OpCode::TEXT); return; } shm.messageType = ShmMessageType::SET_TRANSPORT; shm.setTransport = decode_json(message); worker->handleBlock(shm); ws->send(shm_to_wire(shm), uWS::OpCode::TEXT); break; } case ClientOp::resetPlugin: { if(!worker) { ws->send(json_to_string(ErrorResponse{.error = "No plugin instance"}), uWS::OpCode::TEXT); return; } shm.shouldReset = true; break; } case ClientOp::instantiatePlugin: { if(worker) { ws->send(json_to_string(ErrorResponse{.error = "Plugin already instantiated"}), uWS::OpCode::TEXT); return; } auto pluginListIt = pluginListByArchitecture.find(architecture); if(pluginListIt == pluginListByArchitecture.end()) { ws->send(json_to_string(ErrorResponse{.error = "Architecture not found"}), uWS::OpCode::TEXT); return; } auto & pluginList = pluginListIt->second; auto j2 = decode_json(message); std::unique_ptr desc; for(auto & type : pluginList->getTypes()) { if(type.fileOrIdentifier == j2.pluginIdentifier) { desc = std::make_unique(type); break; } } if(!desc) { ws->send(json_to_string(ErrorResponse{.error = "Plugin not found"}), uWS::OpCode::TEXT); return; } auto loadedHandler = [=] { safeSend->send(json_to_string(EventResponse{.event = "loaded"}), uWS::OpCode::TEXT); }; auto errorHandler = [=](const std::string & message) { RTLogger::log("errorHandler called: %s", message.c_str()); safeSend->send(json_to_string(ErrorResponse{.error = message}), uWS::OpCode::TEXT); }; auto saveHandler = [=](const std::string_view & data, bool periodic) { EncodedStateMessage msg; msg.encodedState = convertToBase64(data); msg.periodic = periodic; safeSend->send(json_to_string(msg), uWS::OpCode::TEXT); }; auto parameterGestureHandler = [=](const ParameterDescription & desc) { safeSend->send(json_to_string(desc), uWS::OpCode::TEXT); }; auto keypressHandler = [=](const KeyDescription & desc) { safeSend->send(json_to_string(desc), uWS::OpCode::TEXT); }; auto editorClosedHandler = [=]() { safeSend->send(json_to_string(EditorClosedResponse()), uWS::OpCode::TEXT); }; auto editorMovedHandler = [=](std::pair pos) { safeSend->send(json_to_string(EditorMovedResponse(pos.first, pos.second)), uWS::OpCode::TEXT); }; auto latencyChangedHandler = [=](int samplesLatency) { safeSend->send(json_to_string(PluginLatencyResponse{.samplesLatency = samplesLatency}), uWS::OpCode::TEXT); }; std::unique_lock lock(initialShouldStayOnTopMutex); worker = std::make_unique(*desc, bufferSize, sampleRate, std::move(bridgeKeySeed), std::move(loadedHandler), std::move(errorHandler), std::move(saveHandler), std::move(parameterGestureHandler), std::move(keypressHandler), std::move(editorClosedHandler), std::move(editorMovedHandler), std::move(trustHandler), std::move(latencyChangedHandler), std::move(launchingChangedHandler), std::move(keyboardEnabledHandler)); worker->setPluginWindowAlwaysOnTop(initialShouldStayOnTop); worker->setKeyboardEnabled(initialKeyboardEnabled); trustHandler = {}; break; } case ClientOp::loadState: { if(!worker) { ws->send(json_to_string(ErrorResponse{.error = "No plugin instance"}), uWS::OpCode::TEXT); return; } auto j2 = decode_json(message); worker->loadState(convertFromBase64(j2.encodedState)); break; } case ClientOp::saveState: { if(!worker) { ws->send(json_to_string(ErrorResponse{.error = "No plugin instance"}), uWS::OpCode::TEXT); return; } worker->requestSaveState(false); break; } case ClientOp::openEditor: { if(!worker) { ws->send(json_to_string(ErrorResponse{.error = "No plugin instance"}), uWS::OpCode::TEXT); return; } auto j2 = decode_json(message); std::optional> pos; if(j2.x.has_value() && j2.y.has_value()) { pos = std::make_pair(j2.x.value(), j2.y.value()); } worker->openPluginWindow(pos); break; } case ClientOp::closeEditor: if(!worker) { ws->send(json_to_string(ErrorResponse{.error = "No plugin instance"}), uWS::OpCode::TEXT); return; } worker->closePluginWindow(); break; default: ws->send(json_to_string(ErrorResponse{.error = "Operation must be handled by host"}), uWS::OpCode::TEXT); break; } } friend class WorkerSubprocess; const int bufferSize; const int sampleRate; const std::string bearerToken; ShmData shm; std::unique_ptr worker; std::map> pluginListByArchitecture; std::string peerAddress; int peerPort; std::mutex connectionStateMutex; bool shouldShutdown; us_listen_socket_t * listenSocket; us_socket_t * clientSocket; uWS::Loop * eventLoop; std::function portCallback; std::function disarmTimeoutCallback; std::function doneCallback; std::function trustHandler; std::function launchingChangedHandler; std::function keyboardEnabledHandler; std::mutex initialShouldStayOnTopMutex; bool initialShouldStayOnTop; bool initialKeyboardEnabled; std::string bridgeKeySeed; std::string architecture; }; WorkerSubprocess::WorkerSubprocess() : lastForegroundMessage(false), shuttingDown(false) { } WorkerSubprocess::~WorkerSubprocess() { } bool WorkerSubprocess::initialiseFromCommandLine (const String& commandLine, const String& commandLineUniqueID, int timeoutMs) { auto res = ChildProcessWorker::initialiseFromCommandLine(commandLine, commandLineUniqueID, timeoutMs); if(res) { disableCrashReporter(); } return res; } void WorkerSubprocess::handleMessageFromCoordinator (const MemoryBlock& mb) { if (mb.isEmpty()) { return; } auto elem = parseXML(mb.toString()); if (!elem) { throw std::runtime_error("Failed to parse message to worker subprocess"); } if(elem->getTagName() == "WorkerSubprocess") { startup(std::move(elem)); } else if(elem->getTagName() == "ShouldStayOnTop") { if(!workerThread) { return; } workerThread->setShouldStayOnTop(elem->getBoolAttribute("value")); } else if(elem->getTagName() == "TrustResolution") { auto trustStoreElem = elem->getFirstChildElement(); if(!workerThread || !trustStoreElem) { return; } workerThread->trustResolution(*trustStoreElem); } else if (elem->getTagName() == "KeyboardEnabled") { if (!workerThread) { return; } workerThread->setKeyboardEnabled(elem->getBoolAttribute("value")); } else if (elem->getTagName() == "KeyboardActivity") { if (!workerThread) { return; } workerThread->keyboardActivity(); } } void WorkerSubprocess::startup(std::unique_ptr startupArgs) { if (workerThread) { throw std::runtime_error("Attempted reinitialization of WorkerSubprocess"); } #ifdef _MSC_VER // assume active while launching otherwise all editors close updateLaunching(true); #endif if (!startupArgs) { throw std::runtime_error("Failed to parse startup args"); } int bufferSize = startupArgs->getIntAttribute("bufferSize"); int sampleRate = startupArgs->getIntAttribute("sampleRate"); auto bearerToken = startupArgs->getStringAttribute("bearerToken").toStdString(); auto bridgeKeySeed = convertFromBase64(startupArgs->getStringAttribute("bridgeKeySeed")); auto architecture = startupArgs->getStringAttribute("architecture").toStdString(); if(bufferSize == 0 || sampleRate == 0 || bearerToken.empty()) { throw std::runtime_error("Invalid startup args"); } workerThread = std::make_unique(bufferSize, sampleRate, bearerToken, bridgeKeySeed, architecture, [this](int port) { auto message = std::make_unique("WorkerSubprocessStarted"); message->setAttribute("port", port); auto xmlString = message->toString(); sendMessageToCoordinator({ xmlString.toRawUTF8(), xmlString.getNumBytesAsUTF8() }); }, [this] { timeoutArmed = false; }, [this] { triggerAsyncUpdate(); }, [this](const std::string_view & publicKey) { auto message = std::make_unique("WorkerSubprocessTrustQuery"); message->setAttribute("publicKey", convertToBase64(publicKey)); auto xmlString = message->toString(); sendMessageToCoordinator({ xmlString.toRawUTF8(), xmlString.getNumBytesAsUTF8() }); }, [this](bool isLaunching) { updateLaunching(isLaunching); }, [this](bool keyboardEnabled) { auto message = std::make_unique("WorkerKeyboardEnabled"); message->setAttribute("value", keyboardEnabled); auto xmlString = message->toString(); sendMessageToCoordinator({ xmlString.toRawUTF8(), xmlString.getNumBytesAsUTF8() }); }); // allow 50% of the frame budget in this VST, to a maximum of 30ms auto perFrameTimeMs = std::fmin(1000.0 * 0.5 * (double)bufferSize / (double)sampleRate, 30); auto periodMs = 1000.0 * (double)bufferSize / (double)sampleRate; workerThread->startRealtimeThread(Thread::RealtimeOptions{}.withMaximumProcessingTimeMs(perFrameTimeMs).withPeriodMs(periodMs)); timeoutArmed = true; startTime = std::chrono::steady_clock::now(); startTimer(50); } void WorkerSubprocess::handleConnectionLost() { triggerAsyncUpdate(); } void WorkerSubprocess::handleAsyncUpdate() { if(shuttingDown) { return; } shuttingDown = true; stopTimer(); if(workerThread) { workerThread->shutdown(); workerThread->waitForThreadToExit(5000); workerThread.reset(); } JUCEApplicationBase::quit(); } void WorkerSubprocess::updateLaunching(bool foregroundMessage) { bool expected = lastForegroundMessage; if (foregroundMessage == expected) { return; } if (!lastForegroundMessage.compare_exchange_strong(expected, foregroundMessage)) { return; } auto message = std::make_unique("WorkerSubprocessForegroundChanged"); message->setAttribute("foreground", foregroundMessage); auto xmlString = message->toString(); sendMessageToCoordinator({ xmlString.toRawUTF8(), xmlString.getNumBytesAsUTF8() }); } void WorkerSubprocess::timerCallback() { if(timeoutArmed && std::chrono::steady_clock::now() - startTime > std::chrono::seconds(10)) { triggerAsyncUpdate(); } #ifndef _MSC_VER updateLaunching(Process::isForegroundProcess()); #endif }