feat(automation): localhost beast POST /jsonrpc server

This commit is contained in:
SoftFever
2026-06-03 02:32:51 +08:00
parent a2e8a90052
commit 487e1cb205
3 changed files with 151 additions and 0 deletions

View File

@@ -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

View File

@@ -0,0 +1,100 @@
#include "AutomationServer.hpp"
#include "libslic3r/Thread.hpp" // create_thread / set_current_thread_name
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/version.hpp>
#include <boost/log/trivial.hpp>
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<net::io_context>(1);
// Bind to loopback ONLY.
tcp::endpoint endpoint(net::ip::make_address("127.0.0.1"), m_port);
m_acceptor = std::make_unique<tcp::acceptor>(*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<http::string_body> req;
http::read(socket, buffer, req, ec);
if (ec) { socket.shutdown(tcp::socket::shutdown_send, ec); return; }
http::response<http::string_body> 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

View File

@@ -0,0 +1,42 @@
#pragma once
#include <boost/asio.hpp>
#include <boost/thread.hpp>
#include <atomic>
#include <functional>
#include <memory>
#include <string>
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<std::string(const std::string& body)>;
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<bool> m_started{false};
RequestHandler m_handler;
std::string m_health{"OrcaSlicer automation server"};
std::unique_ptr<boost::asio::io_context> m_ioc;
std::unique_ptr<boost::asio::ip::tcp::acceptor> m_acceptor;
boost::thread m_thread;
};
}}} // namespace