#pragma once #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "FlowGraph.hpp" #include "Signal.hpp" namespace fs = std::filesystem; namespace dmf { enum class NodeState { Running, Stopped, Crashed }; struct NodeProcess { std::string id; std::string type; pid_t pid{-1}; NodeState state{NodeState::Stopped}; }; class StudioManager { public: StudioManager(fs::path bin_dir, std::string domain) : bin_dir_(std::move(bin_dir)), domain_(std::move(domain)) {} ~StudioManager() { shutdown(); } // Replace the running pipeline with a new graph. // Stops all current nodes, then starts the new ones. nlohmann::json load_graph(const nlohmann::json& graph_json) { nlohmann::json s; std::string err; { std::lock_guard lk(mutex_); stop_all_locked(); garbage_collect_locked(); try { graph_ = parse_graph(graph_json); start_all_locked(); } catch (const std::exception& e) { err = e.what(); } s = status_locked(); } notify(s); if (!err.empty()) return {{"type", "error"}, {"message", err}}; return s; } nlohmann::json load_graph_file(const std::string& path) { std::ifstream f(path); if (!f) return {{"type", "error"}, {"message", "cannot open: " + path}}; std::ostringstream ss; ss << f.rdbuf(); auto j = nlohmann::json::parse(ss.str(), nullptr, false); if (j.is_discarded()) return {{"type", "error"}, {"message", "invalid JSON in: " + path}}; return load_graph(j); } nlohmann::json stop_node(const std::string& id) { nlohmann::json s; { std::lock_guard lk(mutex_); for (auto& p : processes_) { if (p.id == id && p.pid > 0) { kill(p.pid, SIGTERM); waitpid(p.pid, nullptr, 0); p.pid = -1; p.state = NodeState::Stopped; fprintf(stderr, "[studio-manager] stopped node %s\n", id.c_str()); break; } } s = status_locked(); } notify(s); return s; } nlohmann::json start_node(const std::string& id) { nlohmann::json s; { std::lock_guard lk(mutex_); for (auto& p : processes_) { if (p.id == id && p.pid <= 0) { launch_locked(p); break; } } s = status_locked(); } notify(s); return s; } nlohmann::json get_status() { std::lock_guard lk(mutex_); return status_locked(); } // Register a callback invoked (without mutex held) whenever node state changes. // Safe to call send_text() from inside the callback. void on_status_change(std::function cb) { std::lock_guard lk(mutex_); status_cb_ = std::move(cb); } void shutdown() { std::lock_guard lk(mutex_); stop_all_locked(); } // Blocks until g_running is false. Call from a dedicated thread. void run_monitor() { while (dmf::g_running.load(std::memory_order_relaxed)) { mxlSleepForNs(500'000'000); check_children(); } } private: fs::path bin_dir_; std::string domain_; FlowGraph graph_; std::vector processes_; std::mutex mutex_; std::function status_cb_; // ── locked helpers — call only while holding mutex_ ────────────────────── void stop_all_locked() { for (auto& p : processes_) if (p.pid > 0) kill(p.pid, SIGTERM); for (auto& p : processes_) { if (p.pid > 0) { waitpid(p.pid, nullptr, 0); fprintf(stderr, "[studio-manager] stopped node %s\n", p.id.c_str()); p.pid = -1; p.state = NodeState::Stopped; } } processes_.clear(); } void garbage_collect_locked() { mxlInstance gc = mxlCreateInstance(domain_.c_str(), ""); if (gc) { mxlGarbageCollectFlows(gc); mxlDestroyInstance(gc); } } void start_all_locked() { processes_.clear(); for (const auto& node : graph_.nodes) { processes_.push_back({node.id, node.type, -1, NodeState::Stopped}); launch_locked(processes_.back()); } } void launch_locked(NodeProcess& p) { const std::string binary = (bin_dir_ / ("dmf-node-" + p.type)).string(); const nlohmann::json cfg = graph_.node_config(p.id); pid_t pid = fork(); if (pid < 0) { perror("fork"); return; } if (pid == 0) { setenv("MXL_DOMAIN", domain_.c_str(), 1); setenv("NODE_CONFIG", cfg.dump().c_str(), 1); execl(binary.c_str(), binary.c_str(), nullptr); perror(("execl " + binary).c_str()); _exit(1); } p.pid = pid; p.state = NodeState::Running; fprintf(stderr, "[studio-manager] launched %s pid=%d\n", p.id.c_str(), pid); } nlohmann::json status_locked() const { auto nodes = nlohmann::json::array(); for (const auto& p : processes_) { const char* state = p.state == NodeState::Running ? "running" : p.state == NodeState::Crashed ? "crashed" : "stopped"; nodes.push_back({{"id", p.id}, {"type", p.type}, {"pid", p.pid}, {"state", state}}); } return {{"type", "status"}, {"nodes", nodes}}; } // ── unlocked helpers ───────────────────────────────────────────────────── void notify(const nlohmann::json& s) { // Called WITHOUT mutex_ held so the callback can safely call back into us. if (status_cb_) status_cb_(s); } void check_children() { bool changed = false; nlohmann::json s; { std::lock_guard lk(mutex_); for (auto& p : processes_) { if (p.pid <= 0) continue; int ws = 0; if (waitpid(p.pid, &ws, WNOHANG) == p.pid) { fprintf(stderr, "[studio-manager] node %s (pid=%d) exited (status=%d)\n", p.id.c_str(), p.pid, WEXITSTATUS(ws)); p.pid = -1; p.state = NodeState::Crashed; changed = true; } } if (changed) s = status_locked(); } if (changed) notify(s); } // ── static helpers ─────────────────────────────────────────────────────── static FlowGraph parse_graph(const nlohmann::json& j) { FlowGraph g; for (const auto& n : j.at("nodes")) g.nodes.push_back({ n.at("id").get(), n.at("type").get(), n.value("params", nlohmann::json::object()) }); std::map, std::string> flow_ids; for (const auto& e : j.at("edges")) { auto from = e.at("from").get(); auto port = e.at("from_port").get(); auto key = std::make_pair(from, port); if (!flow_ids.count(key)) flow_ids[key] = gen_uuid(); g.edges.push_back({ flow_ids.at(key), from, port, e.value("to", std::string{}), e.value("to_port", std::string{}), e.at("format") }); } return g; } static std::string gen_uuid() { uint8_t b[16]; int fd = open("/dev/urandom", O_RDONLY); if (fd < 0) { perror("open /dev/urandom"); exit(1); } if (read(fd, b, sizeof(b)) != (ssize_t)sizeof(b)) { perror("read"); exit(1); } close(fd); b[6] = (b[6] & 0x0Fu) | 0x40u; b[8] = (b[8] & 0x3Fu) | 0x80u; char s[37]; snprintf(s, sizeof(s), "%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", b[0],b[1],b[2],b[3], b[4],b[5], b[6],b[7], b[8],b[9], b[10],b[11],b[12],b[13],b[14],b[15]); return s; } }; } // namespace dmf