// Copyright (c) 2026 Ourpad Network // See LICENSE file in the project root for full license text. // // MixerIpcServer — Unix domain socket IPC server implementation. #include "MixerIpcServer.hpp" #include "MixerApi.hpp" #include "MixerEngine.hpp" #include #include #include #include #include #include #include #include #include #include // for htonl/ntohl // JSON writer/reader (inline minimal implementation to avoid heavy dependency) // In production, use a proper JSON library like nlohmann/json or boost.json. // Here we use the PiPedal project's own json_writer/json_reader if available, // but since this is a standalone daemon, we provide a minimal inline version. namespace { // Write a JSON string with proper escaping std::string jsonEscape(const std::string& s) { std::string out; out.reserve(s.size() + 2); for (char c : s) { switch (c) { case '"': out += "\\\""; break; case '\\': out += "\\\\"; break; case '\b': out += "\\b"; break; case '\f': out += "\\f"; break; case '\n': out += "\\n"; break; case '\r': out += "\\r"; break; case '\t': out += "\\t"; break; default: if (static_cast(c) < 0x20) { char buf[8]; snprintf(buf, sizeof(buf), "\\u%04x", c); out += buf; } else { out += c; } } } return out; } // Simple JSON object builder class JsonBuilder { public: JsonBuilder() : ss_() { ss_ << "{"; first_ = true; } void add(const std::string& key, const std::string& value) { if (!first_) ss_ << ","; first_ = false; ss_ << "\"" << jsonEscape(key) << "\":\"" << jsonEscape(value) << "\""; } void add(const std::string& key, const char* value) { add(key, std::string(value)); } void add(const std::string& key, int64_t value) { if (!first_) ss_ << ","; first_ = false; ss_ << "\"" << jsonEscape(key) << "\":" << value; } void add(const std::string& key, double value) { if (!first_) ss_ << ","; first_ = false; ss_ << "\"" << jsonEscape(key) << "\":" << value; } void add(const std::string& key, bool value) { if (!first_) ss_ << ","; first_ = false; ss_ << "\"" << jsonEscape(key) << "\":" << (value ? "true" : "false"); } void addRaw(const std::string& key, const std::string& rawJson) { if (!first_) ss_ << ","; first_ = false; ss_ << "\"" << jsonEscape(key) << "\":" << rawJson; } std::string build() { ss_ << "}"; return ss_.str(); } private: std::stringstream ss_; bool first_ = true; }; // Simple JSON key-value extractor (does not handle nested objects) // Just enough for our params extraction class SimpleJsonParser { public: SimpleJsonParser(const std::string& json) : data_(json), pos_(0) { skipWhitespace(); } bool enterObject() { skipWhitespace(); if (pos_ < data_.size() && data_[pos_] == '{') { pos_++; skipWhitespace(); return true; } return false; } bool enterArray() { skipWhitespace(); if (pos_ < data_.size() && data_[pos_] == '[') { pos_++; skipWhitespace(); return true; } return false; } void leaveObject() { skipWhitespace(); if (pos_ < data_.size() && data_[pos_] == '}') pos_++; } void leaveArray() { skipWhitespace(); if (pos_ < data_.size() && data_[pos_] == ']') pos_++; } std::string readKey() { skipWhitespace(); std::string key = readString(); skipWhitespace(); if (pos_ < data_.size() && data_[pos_] == ':') pos_++; return key; } std::string readString() { skipWhitespace(); if (pos_ >= data_.size() || data_[pos_] != '"') return ""; pos_++; // skip opening quote std::string result; while (pos_ < data_.size() && data_[pos_] != '"') { if (data_[pos_] == '\\' && pos_ + 1 < data_.size()) { pos_++; switch (data_[pos_]) { case '"': result += '"'; break; case '\\': result += '\\'; break; case '/': result += '/'; break; case 'n': result += '\n'; break; case 'r': result += '\r'; break; case 't': result += '\t'; break; default: result += data_[pos_]; break; } } else { result += data_[pos_]; } pos_++; } if (pos_ < data_.size() && data_[pos_] == '"') pos_++; // skip closing quote return result; } int64_t readInt() { skipWhitespace(); char* end = nullptr; int64_t val = strtoll(data_.c_str() + pos_, &end, 10); if (end) pos_ = end - data_.c_str(); return val; } double readDouble() { skipWhitespace(); char* end = nullptr; double val = strtod(data_.c_str() + pos_, &end); if (end) pos_ = end - data_.c_str(); return val; } bool readBool() { skipWhitespace(); if (data_.substr(pos_, 4) == "true") { pos_ += 4; return true; } if (data_.substr(pos_, 5) == "false") { pos_ += 5; return false; } return false; } std::string readRaw() { // Read a raw JSON value (for nested objects/arrays we want to preserve) skipWhitespace(); if (pos_ >= data_.size()) return ""; char start = data_[pos_]; if (start == '{' || start == '[') { // Find matching close brace/bracket char close = (start == '{') ? '}' : ']'; int depth = 0; size_t startPos = pos_; bool inString = false; while (pos_ < data_.size()) { char c = data_[pos_]; if (c == '"' && (pos_ == 0 || data_[pos_-1] != '\\')) inString = !inString; if (!inString) { if (c == start) depth++; if (c == close) { depth--; if (depth == 0) { pos_++; return data_.substr(startPos, pos_ - startPos); } } } pos_++; } pos_ = startPos; // reset on failure } return ""; } // Skip a single value (for keys we don't care about) void skipValue() { skipWhitespace(); if (pos_ >= data_.size()) return; char c = data_[pos_]; if (c == '"') { readString(); } else if (c == '{' || c == '[') { readRaw(); } else if (c == 't' || c == 'f') { readBool(); } else { readDouble(); } // number (also handles int) skipWhitespace(); if (pos_ < data_.size() && (data_[pos_] == ',' || data_[pos_] == '}')) { if (data_[pos_] == ',') pos_++; } } // Check if we've reached the end of the current object bool atEnd() { skipWhitespace(); return pos_ >= data_.size() || data_[pos_] == '}'; } // Parse a string value for a given key from a flat JSON object std::string getString(const std::string& key) { if (!enterObject()) return ""; while (!atEnd()) { std::string k = readKey(); if (k == key) { std::string v = readString(); leaveObject(); return v; } else { skipValue(); } } leaveObject(); return ""; } int64_t getInt(const std::string& key, int64_t def = 0) { if (!enterObject()) return def; while (!atEnd()) { std::string k = readKey(); if (k == key) { int64_t v = readInt(); leaveObject(); return v; } else { skipValue(); } } leaveObject(); return def; } double getDouble(const std::string& key, double def = 0.0) { if (!enterObject()) return def; while (!atEnd()) { std::string k = readKey(); if (k == key) { double v = readDouble(); leaveObject(); return v; } else { skipValue(); } } leaveObject(); return def; } bool getBool(const std::string& key, bool def = false) { if (!enterObject()) return def; while (!atEnd()) { std::string k = readKey(); if (k == key) { bool v = readBool(); leaveObject(); return v; } else { skipValue(); } } leaveObject(); return def; } std::string getRaw(const std::string& key) { if (!enterObject()) return ""; while (!atEnd()) { std::string k = readKey(); if (k == key) { std::string v = readRaw(); leaveObject(); return v; } else { skipValue(); } } leaveObject(); return ""; } private: const std::string& data_; size_t pos_; void skipWhitespace() { while (pos_ < data_.size() && (data_[pos_] == ' ' || data_[pos_] == '\t' || data_[pos_] == '\n' || data_[pos_] == '\r')) pos_++; } }; } // anonymous namespace using namespace pipedal; // --------------------------------------------------------------------------- // Construction / Destruction // --------------------------------------------------------------------------- MixerIpcServer::MixerIpcServer() {} MixerIpcServer::~MixerIpcServer() { stop(); } // --------------------------------------------------------------------------- // Start / Stop // --------------------------------------------------------------------------- bool MixerIpcServer::start(const std::string& socketPath) { if (running_.load()) { return true; // already running } socketPath_ = socketPath; // Remove existing socket file if present unlink(socketPath_.c_str()); // Create socket serverFd_ = socket(AF_UNIX, SOCK_STREAM, 0); if (serverFd_ < 0) { perror("socket"); return false; } // Bind 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(serverFd_, (struct sockaddr*)&addr, sizeof(addr)) < 0) { perror("bind"); close(serverFd_); serverFd_ = -1; return false; } // Set permissions so the FastAPI process (running as same user) can connect chmod(socketPath_.c_str(), 0666); // Listen if (listen(serverFd_, 5) < 0) { perror("listen"); close(serverFd_); serverFd_ = -1; unlink(socketPath_.c_str()); return false; } running_.store(true); // Start accept thread acceptThread_ = std::thread(&MixerIpcServer::acceptLoop, this); return true; } void MixerIpcServer::stop() { running_.store(false); // Close all client connections for (auto& [id, conn] : clients_) { if (conn->fd >= 0) { close(conn->fd); conn->fd = -1; } } clients_.clear(); // Close server socket if (serverFd_ >= 0) { close(serverFd_); serverFd_ = -1; } // Remove socket file if (!socketPath_.empty()) { unlink(socketPath_.c_str()); } // Join accept thread if (acceptThread_.joinable()) { acceptThread_.join(); } } // --------------------------------------------------------------------------- // Communication // --------------------------------------------------------------------------- bool MixerIpcServer::sendFrame(int fd, const std::string& jsonPayload) { if (fd < 0) return false; uint32_t len = static_cast(jsonPayload.size()); uint32_t lenBe = htonl(len); // Write length prefix ssize_t written = write(fd, &lenBe, sizeof(lenBe)); if (written != sizeof(lenBe)) return false; // Write payload written = write(fd, jsonPayload.data(), jsonPayload.size()); if (written != static_cast(jsonPayload.size())) return false; return true; } bool MixerIpcServer::sendResponse(int clientFd, const std::string& jsonResponse) { return sendFrame(clientFd, jsonResponse); } void MixerIpcServer::broadcast(const std::string& jsonMessage) { auto fds = connectedClientFds(); for (int fd : fds) { if (fd >= 0) { sendFrame(fd, jsonMessage); } } } std::vector MixerIpcServer::connectedClientFds() const { std::vector fds; for (const auto& [id, conn] : clients_) { if (conn->active && conn->fd >= 0) { fds.push_back(conn->fd); } } return fds; } // --------------------------------------------------------------------------- // Accept loop // --------------------------------------------------------------------------- void MixerIpcServer::acceptLoop() { while (running_.load()) { struct sockaddr_un clientAddr; socklen_t clientLen = sizeof(clientAddr); int clientFd = accept(serverFd_, (struct sockaddr*)&clientAddr, &clientLen); if (clientFd < 0) { if (errno == EINTR) continue; if (!running_.load()) break; perror("accept"); continue; } // Create client connection auto conn = std::make_unique(); conn->fd = clientFd; conn->active = true; int clientId = nextClientId_++; clients_[clientId] = std::move(conn); // Handle this client (blocking per-client) // In a real implementation, this would be in a separate thread or use event loop. // For this embedded version, we handle one client at a time per thread. handleClient(clientId); } } // --------------------------------------------------------------------------- // Client handler // --------------------------------------------------------------------------- void MixerIpcServer::handleClient(int clientId) { auto it = clients_.find(clientId); if (it == clients_.end()) return; auto& conn = it->second; while (running_.load() && conn->active) { if (!readFromClient(*conn)) { break; } } // Cleanup if (conn->fd >= 0) { close(conn->fd); conn->fd = -1; } conn->active = false; clients_.erase(clientId); } bool MixerIpcServer::readFromClient(ClientConnection& conn) { uint8_t buffer[4096]; ssize_t n = read(conn.fd, buffer, sizeof(buffer)); if (n <= 0) { if (n == 0) { // Connection closed return false; } if (errno == EAGAIN || errno == EWOULDBLOCK) { // No data available yet — yield and retry on next loop iteration // In a proper impl, use poll()/epoll() return true; } return false; } // Append to read buffer conn.readBuffer.insert(conn.readBuffer.end(), buffer, buffer + n); // Process complete frames while (true) { if (conn.readingHeader) { if (conn.readBuffer.size() < 4) break; // Read length prefix (big-endian uint32) uint32_t lenBe; memcpy(&lenBe, conn.readBuffer.data(), 4); conn.expectedLength = ntohl(lenBe); // Remove header from buffer conn.readBuffer.erase(conn.readBuffer.begin(), conn.readBuffer.begin() + 4); conn.readingHeader = false; } if (conn.readBuffer.size() < conn.expectedLength) break; // We have a complete message std::string jsonMessage(conn.readBuffer.begin(), conn.readBuffer.begin() + conn.expectedLength); conn.readBuffer.erase(conn.readBuffer.begin(), conn.readBuffer.begin() + conn.expectedLength); conn.readingHeader = true; conn.expectedLength = 0; // Process the message processMessage(conn, jsonMessage); } return true; } // --------------------------------------------------------------------------- // Message processing // --------------------------------------------------------------------------- void MixerIpcServer::processMessage(ClientConnection& conn, const std::string& jsonMessage) { if (messageCallback_) { messageCallback_(jsonMessage); } // Parse the JSON message SimpleJsonParser parser(jsonMessage); // Extract id, method, params int64_t msgId = 0; std::string method; std::string params; if (!parser.enterObject()) { // Invalid message JsonBuilder err; err.add("id", 0); err.add("error", std::string("Invalid JSON: expected object")); sendResponse(conn.fd, err.build()); return; } while (!parser.atEnd()) { std::string key = parser.readKey(); if (key == "id") { msgId = parser.readInt(); } else if (key == "method") { method = parser.readString(); } else if (key == "params") { // Read raw params value (could be object, array, etc.) params = parser.readRaw(); } else { parser.skipValue(); } } parser.leaveObject(); if (method.empty()) { JsonBuilder err; err.add("id", msgId); err.add("error", std::string("Missing 'method' field")); sendResponse(conn.fd, err.build()); return; } // Dispatch and get response std::string resultJson = dispatchMethod(method, params); // Wrap with response id std::stringstream ss; ss << "{\"id\":" << msgId << ","; // The resultJson starts with {"result": ...} or {"error": ...} // Strip the outer { and add our id if (!resultJson.empty() && resultJson[0] == '{') { ss << resultJson.substr(1); // skip leading { } else { ss << "\"result\":" << resultJson << ",\"error\":null"; } sendResponse(conn.fd, ss.str()); } // --------------------------------------------------------------------------- // Method dispatch // --------------------------------------------------------------------------- std::string MixerIpcServer::dispatchMethod(const std::string& method, const std::string& paramsJson) { if (!mixerApi_) { return "{\"error\":\"no mixer api configured\"}"; } try { if (method == "getState") { std::string state = mixerApi_->getStateJson(); return "{\"result\":" + state + ",\"error\":null}"; } else if (method == "setChannelVolume") { SimpleJsonParser p(paramsJson); int ci = (int)p.getInt("channelIndex", -1); double vol = p.getDouble("volumeDb", -96.0); if (ci < 0) return "{\"error\":\"missing channelIndex\"}"; mixerApi_->setChannelVolume(ci, (float)vol); return "{\"result\":{},\"error\":null}"; } else if (method == "setChannelPan") { SimpleJsonParser p(paramsJson); int ci = (int)p.getInt("channelIndex", -1); double pan = p.getDouble("pan", 0.0); if (ci < 0) return "{\"error\":\"missing channelIndex\"}"; mixerApi_->setChannelPan(ci, (float)pan); return "{\"result\":{},\"error\":null}"; } else if (method == "setChannelMute") { SimpleJsonParser p(paramsJson); int ci = (int)p.getInt("channelIndex", -1); bool mute = p.getBool("mute", false); if (ci < 0) return "{\"error\":\"missing channelIndex\"}"; mixerApi_->setChannelMute(ci, mute); return "{\"result\":{},\"error\":null}"; } else if (method == "setChannelSolo") { SimpleJsonParser p(paramsJson); int ci = (int)p.getInt("channelIndex", -1); bool solo = p.getBool("solo", false); if (ci < 0) return "{\"error\":\"missing channelIndex\"}"; mixerApi_->setChannelSolo(ci, solo); return "{\"result\":{},\"error\":null}"; } else if (method == "addChannel") { SimpleJsonParser p(paramsJson); int pi = (int)p.getInt("physicalInputIndex", 0); int idx = mixerApi_->addChannel(pi); JsonBuilder resp; resp.add("channelIndex", (int64_t)idx); return "{\"result\":" + resp.build() + ",\"error\":null}"; } else if (method == "removeChannel") { SimpleJsonParser p(paramsJson); int ci = (int)p.getInt("channelIndex", -1); if (ci < 0) return "{\"error\":\"missing channelIndex\"}"; mixerApi_->removeChannel(ci); return "{\"result\":{},\"error\":null}"; } else if (method == "getOutputRoutes") { std::string routes = mixerApi_->getOutputRoutesJson(); return "{\"result\":{\"routes\":" + routes + "},\"error\":null}"; } else if (method == "setOutputRoutes") { // params is the raw JSON for output routes array // We need the routes array from params SimpleJsonParser p(paramsJson); std::string routesArray = p.getRaw("routes"); if (!routesArray.empty()) { mixerApi_->setOutputRoutesFromJson(routesArray); } return "{\"result\":{},\"error\":null}"; } else if (method == "saveScene") { SimpleJsonParser p(paramsJson); std::string name = p.getString("name"); if (name.empty()) return "{\"error\":\"missing name\"}"; std::string result = mixerApi_->saveScene(name); // result from MixerApi is already JSON: {"id": N, "name": "..."} return "{\"result\":" + result + ",\"error\":null}"; } else if (method == "loadScene") { SimpleJsonParser p(paramsJson); std::string sceneId = p.getString("sceneId"); if (sceneId.empty()) { // Try as integer int64_t id = p.getInt("sceneId", -1); if (id < 0) return "{\"error\":\"missing sceneId\"}"; sceneId = std::to_string(id); } bool ok = mixerApi_->loadScene(sceneId); JsonBuilder resp; resp.add("ok", ok); return "{\"result\":" + resp.build() + ",\"error\":null}"; } else if (method == "listScenes") { std::string scenes = mixerApi_->listScenes(); if (scenes.empty()) scenes = "[]"; return "{\"result\":{\"scenes\":" + scenes + "},\"error\":null}"; } else if (method == "deleteScene") { SimpleJsonParser p(paramsJson); std::string sceneId = p.getString("sceneId"); if (sceneId.empty()) { int64_t id = p.getInt("sceneId", -1); if (id < 0) return "{\"error\":\"missing sceneId\"}"; sceneId = std::to_string(id); } bool ok = mixerApi_->deleteScene(sceneId); JsonBuilder resp; resp.add("ok", ok); return "{\"result\":" + resp.build() + ",\"error\":null}"; } else { return "{\"error\":\"unknown method: " + jsonEscape(method) + "\"}"; } } catch (const std::exception& e) { return "{\"error\":\"" + jsonEscape(e.what()) + "\"}"; } }