diff --git a/src/slic3r/CMakeLists.txt b/src/slic3r/CMakeLists.txt index c4d6369b72..3d4ce6d7a4 100644 --- a/src/slic3r/CMakeLists.txt +++ b/src/slic3r/CMakeLists.txt @@ -230,6 +230,15 @@ set(SLIC3R_GUI_SOURCES GUI/HMSPanel.hpp GUI/HttpServer.cpp GUI/HttpServer.hpp + GUI/Automation/IUiBackend.hpp + GUI/Automation/WidgetSerializer.cpp + GUI/Automation/WidgetSerializer.hpp + GUI/Automation/Locator.cpp + GUI/Automation/Locator.hpp + GUI/Automation/JsonRpcDispatcher.cpp + GUI/Automation/JsonRpcDispatcher.hpp + GUI/Automation/AutomationServer.cpp + GUI/Automation/AutomationServer.hpp GUI/I18N.cpp GUI/I18N.hpp GUI/DragDropPanel.cpp diff --git a/src/slic3r/GUI/Automation/AutomationServer.cpp b/src/slic3r/GUI/Automation/AutomationServer.cpp new file mode 100644 index 0000000000..c3be4eac13 --- /dev/null +++ b/src/slic3r/GUI/Automation/AutomationServer.cpp @@ -0,0 +1,100 @@ +#include "AutomationServer.hpp" +#include "libslic3r/Thread.hpp" // create_thread / set_current_thread_name + +#include +#include +#include +#include + +namespace beast = boost::beast; +namespace http = beast::http; +namespace net = boost::asio; +using tcp = net::ip::tcp; + +namespace Slic3r { namespace GUI { namespace Automation { + +AutomationServer::AutomationServer(unsigned short port) : m_port(port) {} + +AutomationServer::~AutomationServer() { stop(); } + +void AutomationServer::start() { + if (m_started) return; + m_ioc = std::make_unique(1); + // Bind to loopback ONLY. + tcp::endpoint endpoint(net::ip::make_address("127.0.0.1"), m_port); + m_acceptor = std::make_unique(*m_ioc); + m_acceptor->open(endpoint.protocol()); + m_acceptor->set_option(net::socket_base::reuse_address(true)); + m_acceptor->bind(endpoint); + m_acceptor->listen(net::socket_base::max_listen_connections); + m_started = true; + + do_accept(); + + net::io_context* ioc = m_ioc.get(); + m_thread = create_thread([ioc] { + set_current_thread_name("orca_automation"); + ioc->run(); + }); + BOOST_LOG_TRIVIAL(info) << "AutomationServer listening on 127.0.0.1:" << m_port; +} + +void AutomationServer::stop() { + if (!m_started) return; + m_started = false; + if (m_ioc) m_ioc->stop(); + if (m_thread.joinable()) m_thread.join(); + m_acceptor.reset(); + m_ioc.reset(); +} + +void AutomationServer::do_accept() { + m_acceptor->async_accept([this](beast::error_code ec, tcp::socket socket) { + if (!ec) { + // v1: single-client, serialized — handle synchronously on the io thread. + handle_session(std::move(socket)); + } + if (m_started && m_acceptor && m_acceptor->is_open()) + do_accept(); + }); +} + +void AutomationServer::handle_session(tcp::socket socket) { + beast::error_code ec; + beast::flat_buffer buffer; + http::request req; + http::read(socket, buffer, req, ec); + if (ec) { socket.shutdown(tcp::socket::shutdown_send, ec); return; } + + http::response res; + res.version(req.version()); + res.keep_alive(false); + + if (req.method() == http::verb::post && req.target() == "/jsonrpc") { + std::string body_out; + try { + body_out = m_handler ? m_handler(req.body()) + : R"({"jsonrpc":"2.0","id":null,"error":{"code":-32603,"message":"no handler"}})"; + } catch (const std::exception& e) { + body_out = std::string(R"({"jsonrpc":"2.0","id":null,"error":{"code":-32603,"message":")") + + e.what() + R"("}})"; + } + res.result(http::status::ok); + res.set(http::field::content_type, "application/json"); + res.body() = std::move(body_out); + } else if (req.method() == http::verb::get && req.target() == "/") { + res.result(http::status::ok); + res.set(http::field::content_type, "text/plain"); + res.body() = m_health; + } else { + res.result(http::status::not_found); + res.set(http::field::content_type, "text/plain"); + res.body() = "not found"; + } + res.set(http::field::server, "OrcaSlicer/automation"); + res.prepare_payload(); + http::write(socket, res, ec); + socket.shutdown(tcp::socket::shutdown_send, ec); +} + +}}} // namespace diff --git a/src/slic3r/GUI/Automation/AutomationServer.hpp b/src/slic3r/GUI/Automation/AutomationServer.hpp new file mode 100644 index 0000000000..c4020daf52 --- /dev/null +++ b/src/slic3r/GUI/Automation/AutomationServer.hpp @@ -0,0 +1,42 @@ +#pragma once +#include +#include +#include +#include +#include +#include + +namespace Slic3r { namespace GUI { namespace Automation { + +// Localhost-only HTTP/1.1 server. POST /jsonrpc -> handler(body) -> response body. +// GET / -> a tiny health/version page. The handler runs on the server's own +// io thread; it is responsible for any further thread marshaling. +class AutomationServer { +public: + using RequestHandler = std::function; + + explicit AutomationServer(unsigned short port); + ~AutomationServer(); + + void set_handler(RequestHandler handler) { m_handler = std::move(handler); } + void set_health_text(std::string text) { m_health = std::move(text); } + + void start(); // binds to 127.0.0.1:port, starts the io thread + void stop(); // stops the io thread, joins + bool is_started() const { return m_started; } + unsigned short port() const { return m_port; } + +private: + void do_accept(); + void handle_session(boost::asio::ip::tcp::socket socket); + + unsigned short m_port; + std::atomic m_started{false}; + RequestHandler m_handler; + std::string m_health{"OrcaSlicer automation server"}; + std::unique_ptr m_ioc; + std::unique_ptr m_acceptor; + boost::thread m_thread; +}; + +}}} // namespace