mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-28 11:21:30 +00:00
feat: add ssl compatibility with moonrakerprinteragent
This commit is contained in:
@@ -14,16 +14,22 @@
|
|||||||
#include <boost/algorithm/string.hpp>
|
#include <boost/algorithm/string.hpp>
|
||||||
#include <boost/asio/connect.hpp>
|
#include <boost/asio/connect.hpp>
|
||||||
#include <boost/asio/ip/tcp.hpp>
|
#include <boost/asio/ip/tcp.hpp>
|
||||||
|
#include <boost/asio/ssl.hpp>
|
||||||
#include <boost/beast/core.hpp>
|
#include <boost/beast/core.hpp>
|
||||||
|
#include <boost/beast/ssl.hpp>
|
||||||
#include <boost/beast/websocket.hpp>
|
#include <boost/beast/websocket.hpp>
|
||||||
#include <boost/filesystem.hpp>
|
#include <boost/filesystem.hpp>
|
||||||
#include <boost/log/trivial.hpp>
|
#include <boost/log/trivial.hpp>
|
||||||
|
#include <openssl/ssl.h>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
#include <map>
|
#include <map>
|
||||||
|
#include <memory>
|
||||||
|
#include <stdexcept>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
#include <variant>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -96,6 +102,130 @@ std::string map_moonraker_state(std::string state)
|
|||||||
|
|
||||||
namespace Slic3r {
|
namespace Slic3r {
|
||||||
|
|
||||||
|
struct MoonrakerWebsocket::Impl
|
||||||
|
{
|
||||||
|
using PlainWebsocket = websocket::stream<beast::tcp_stream>;
|
||||||
|
using SecureWebsocket = websocket::stream<beast::ssl_stream<beast::tcp_stream>>;
|
||||||
|
using PlainPtr = std::unique_ptr<PlainWebsocket>;
|
||||||
|
using SecurePtr = std::unique_ptr<SecureWebsocket>;
|
||||||
|
|
||||||
|
explicit Impl(bool secure, std::string api_key) : secure(secure), api_key(std::move(api_key)), ssl_context(net::ssl::context::tls_client)
|
||||||
|
{
|
||||||
|
if (this->secure) {
|
||||||
|
websocket = std::make_unique<SecureWebsocket>(ioc, ssl_context);
|
||||||
|
} else {
|
||||||
|
websocket = std::make_unique<PlainWebsocket>(beast::tcp_stream{ioc});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool secure;
|
||||||
|
std::string api_key;
|
||||||
|
net::io_context ioc;
|
||||||
|
net::ssl::context ssl_context;
|
||||||
|
std::variant<PlainPtr, SecurePtr> websocket;
|
||||||
|
};
|
||||||
|
|
||||||
|
MoonrakerWebsocket::MoonrakerWebsocket(bool secure, std::string api_key) : m_impl(std::make_unique<Impl>(secure, std::move(api_key))) {}
|
||||||
|
|
||||||
|
MoonrakerWebsocket::~MoonrakerWebsocket() = default;
|
||||||
|
|
||||||
|
void MoonrakerWebsocket::connect(const std::string& host, const std::string& port, std::chrono::seconds timeout)
|
||||||
|
{
|
||||||
|
tcp::resolver resolver(m_impl->ioc);
|
||||||
|
std::visit(
|
||||||
|
[&](auto& websocket_ptr) {
|
||||||
|
auto& stream = beast::get_lowest_layer(*websocket_ptr);
|
||||||
|
stream.expires_after(timeout);
|
||||||
|
stream.connect(resolver.resolve(host, port));
|
||||||
|
},
|
||||||
|
m_impl->websocket);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MoonrakerWebsocket::tls_handshake(const std::string& host)
|
||||||
|
{
|
||||||
|
if (!m_impl->secure) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto& websocket = *std::get<Impl::SecurePtr>(m_impl->websocket);
|
||||||
|
auto& tls_stream = websocket.next_layer();
|
||||||
|
if (!SSL_set_tlsext_host_name(tls_stream.native_handle(), host.c_str())) {
|
||||||
|
throw std::runtime_error("Moonraker WSS: failed to set TLS server name");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Match Http's existing printer-host behavior: encrypt the connection
|
||||||
|
// while accepting self-signed/local printer certificates.
|
||||||
|
tls_stream.set_verify_mode(net::ssl::verify_none);
|
||||||
|
tls_stream.handshake(net::ssl::stream_base::client);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MoonrakerWebsocket::handshake(const std::string& host, const std::string& target)
|
||||||
|
{
|
||||||
|
std::visit(
|
||||||
|
[&](auto& websocket_ptr) {
|
||||||
|
websocket_ptr->set_option(websocket::stream_base::decorator([api_key = m_impl->api_key](websocket::request_type& req) {
|
||||||
|
req.set(http::field::user_agent, "OrcaSlicer");
|
||||||
|
if (!api_key.empty()) {
|
||||||
|
req.set("X-Api-Key", api_key);
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
websocket_ptr->handshake(host, target);
|
||||||
|
},
|
||||||
|
m_impl->websocket);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MoonrakerWebsocket::text(bool enabled)
|
||||||
|
{
|
||||||
|
std::visit([&](auto& websocket_ptr) { websocket_ptr->text(enabled); }, m_impl->websocket);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MoonrakerWebsocket::write(const std::string& body)
|
||||||
|
{
|
||||||
|
std::visit([&](auto& websocket_ptr) { websocket_ptr->write(net::buffer(body)); }, m_impl->websocket);
|
||||||
|
}
|
||||||
|
|
||||||
|
MoonrakerWebsocket::ReadResult MoonrakerWebsocket::read(std::string& payload, std::string& error_message)
|
||||||
|
{
|
||||||
|
beast::flat_buffer buffer;
|
||||||
|
beast::error_code error;
|
||||||
|
std::visit([&](auto& websocket_ptr) { websocket_ptr->read(buffer, error); }, m_impl->websocket);
|
||||||
|
|
||||||
|
if (error == beast::error::timeout) {
|
||||||
|
return ReadResult::timeout;
|
||||||
|
}
|
||||||
|
if (error == websocket::error::closed) {
|
||||||
|
return ReadResult::closed;
|
||||||
|
}
|
||||||
|
if (error) {
|
||||||
|
error_message = error.message();
|
||||||
|
return ReadResult::error;
|
||||||
|
}
|
||||||
|
|
||||||
|
payload = beast::buffers_to_string(buffer.data());
|
||||||
|
return ReadResult::message;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MoonrakerWebsocket::close()
|
||||||
|
{
|
||||||
|
beast::error_code error;
|
||||||
|
std::visit([&](auto& websocket_ptr) { websocket_ptr->close(websocket::close_code::normal, error); }, m_impl->websocket);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MoonrakerWebsocket::expires_after(std::chrono::seconds timeout)
|
||||||
|
{
|
||||||
|
std::visit([&](auto& websocket_ptr) { beast::get_lowest_layer(*websocket_ptr).expires_after(timeout); }, m_impl->websocket);
|
||||||
|
}
|
||||||
|
|
||||||
|
void MoonrakerWebsocket::abort()
|
||||||
|
{
|
||||||
|
std::visit(
|
||||||
|
[](auto& websocket_ptr) {
|
||||||
|
beast::error_code error;
|
||||||
|
beast::get_lowest_layer(*websocket_ptr).socket().shutdown(tcp::socket::shutdown_both, error);
|
||||||
|
},
|
||||||
|
m_impl->websocket);
|
||||||
|
}
|
||||||
|
|
||||||
const std::string MoonrakerPrinterAgent_VERSION = "1.0.0";
|
const std::string MoonrakerPrinterAgent_VERSION = "1.0.0";
|
||||||
|
|
||||||
bool moonraker_is_light_name(const std::string& name)
|
bool moonraker_is_light_name(const std::string& name)
|
||||||
@@ -217,12 +347,6 @@ int MoonrakerPrinterAgent::connect_printer(std::string dev_id, std::string dev_i
|
|||||||
BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: connect_printer missing dev_id or dev_ip";
|
BOOST_LOG_TRIVIAL(error) << "MoonrakerPrinterAgent: connect_printer missing dev_id or dev_ip";
|
||||||
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
|
return BAMBU_NETWORK_ERR_INVALID_HANDLE;
|
||||||
}
|
}
|
||||||
|
|
||||||
// why: Moonraker/print-host serves plain HTTP (nginx :80 or Moonraker :7125), never
|
|
||||||
// https:443; MachineObject::connect defaults use_ssl=true -> forced https -> refused.
|
|
||||||
// Pin http. (matches feature/printer-agent-port-pristine)
|
|
||||||
use_ssl = false;
|
|
||||||
|
|
||||||
std::string base_url;
|
std::string base_url;
|
||||||
std::string api_key;
|
std::string api_key;
|
||||||
uint64_t gen;
|
uint64_t gen;
|
||||||
@@ -1567,7 +1691,7 @@ bool MoonrakerPrinterAgent::send_ws_rpc(const std::string& method, const nlohman
|
|||||||
}
|
}
|
||||||
|
|
||||||
WsEndpoint endpoint;
|
WsEndpoint endpoint;
|
||||||
if (!parse_ws_endpoint(base_url, endpoint) || endpoint.secure) {
|
if (!parse_ws_endpoint(base_url, endpoint)) {
|
||||||
BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: send_ws_rpc has no usable websocket for base_url="
|
BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: send_ws_rpc has no usable websocket for base_url="
|
||||||
<< base_url;
|
<< base_url;
|
||||||
return false;
|
return false;
|
||||||
@@ -1589,36 +1713,26 @@ bool MoonrakerPrinterAgent::send_ws_rpc(const std::string& method, const nlohman
|
|||||||
|
|
||||||
for (const auto& port : ports) {
|
for (const auto& port : ports) {
|
||||||
try {
|
try {
|
||||||
net::io_context ioc;
|
MoonrakerWebsocket ws{endpoint.secure, api_key};
|
||||||
tcp::resolver resolver{ioc};
|
ws.connect(endpoint.host, port, std::chrono::seconds(5));
|
||||||
beast::tcp_stream stream{ioc};
|
ws.tls_handshake(endpoint.host);
|
||||||
stream.expires_after(std::chrono::seconds(5));
|
|
||||||
stream.connect(resolver.resolve(endpoint.host, port));
|
|
||||||
|
|
||||||
websocket::stream<beast::tcp_stream> ws{std::move(stream)};
|
|
||||||
ws.set_option(websocket::stream_base::decorator([&](websocket::request_type& req) {
|
|
||||||
req.set(http::field::user_agent, "OrcaSlicer");
|
|
||||||
if (!api_key.empty()) {
|
|
||||||
req.set("X-Api-Key", api_key);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
std::string host_header = endpoint.host;
|
std::string host_header = endpoint.host;
|
||||||
if (!port.empty() && port != "80") {
|
if (!port.empty() && port != (endpoint.secure ? "443" : "80")) {
|
||||||
host_header += ":" + port;
|
host_header += ":" + port;
|
||||||
}
|
}
|
||||||
ws.handshake(host_header, endpoint.target);
|
ws.handshake(host_header, endpoint.target);
|
||||||
ws.text(true);
|
ws.text(true);
|
||||||
ws.write(net::buffer(body));
|
ws.write(body);
|
||||||
|
|
||||||
ws.next_layer().expires_after(std::chrono::seconds(2));
|
ws.expires_after(std::chrono::seconds(2));
|
||||||
beast::flat_buffer buffer;
|
std::string response;
|
||||||
beast::error_code read_ec;
|
std::string read_error;
|
||||||
ws.read(buffer, read_ec);
|
ws.read(response, read_error);
|
||||||
|
|
||||||
beast::error_code close_ec;
|
ws.close();
|
||||||
ws.close(websocket::close_code::normal, close_ec);
|
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: sent " << method << " over "
|
||||||
BOOST_LOG_TRIVIAL(info) << "MoonrakerPrinterAgent: sent " << method << " over ws to "
|
<< (endpoint.secure ? "wss" : "ws") << " to "
|
||||||
<< endpoint.host << ":" << port;
|
<< endpoint.host << ":" << port;
|
||||||
return true;
|
return true;
|
||||||
} catch (const std::exception& e) {
|
} catch (const std::exception& e) {
|
||||||
@@ -2070,11 +2184,6 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba
|
|||||||
BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: websocket endpoint invalid for base_url=" << base_url;
|
BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: websocket endpoint invalid for base_url=" << base_url;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (endpoint.secure) {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: websocket wss not supported for base_url=" << base_url;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reconnection logic
|
// Reconnection logic
|
||||||
ws_reconnect_requested.store(false); // Reset reconnect flag
|
ws_reconnect_requested.store(false); // Reset reconnect flag
|
||||||
int retry_count = 0;
|
int retry_count = 0;
|
||||||
@@ -2085,15 +2194,9 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba
|
|||||||
bool connection_lost = false; // Flag to distinguish clean shutdown from unexpected disconnect
|
bool connection_lost = false; // Flag to distinguish clean shutdown from unexpected disconnect
|
||||||
|
|
||||||
try {
|
try {
|
||||||
net::io_context ioc;
|
MoonrakerWebsocket ws{endpoint.secure, api_key};
|
||||||
tcp::resolver resolver{ioc};
|
ws.connect(endpoint.host, endpoint.port, std::chrono::seconds(10));
|
||||||
beast::tcp_stream stream{ioc};
|
ws.tls_handshake(endpoint.host);
|
||||||
|
|
||||||
stream.expires_after(std::chrono::seconds(10));
|
|
||||||
auto const results = resolver.resolve(endpoint.host, endpoint.port);
|
|
||||||
stream.connect(results);
|
|
||||||
|
|
||||||
websocket::stream<beast::tcp_stream> ws{std::move(stream)};
|
|
||||||
|
|
||||||
// Allow stop_status_stream() to force this socket shut so a blocked
|
// Allow stop_status_stream() to force this socket shut so a blocked
|
||||||
// synchronous ws.read()/ws.write() returns with an error (Beast's
|
// synchronous ws.read()/ws.write() returns with an error (Beast's
|
||||||
@@ -2108,20 +2211,12 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba
|
|||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(ws_abort_mutex);
|
std::lock_guard<std::mutex> lock(ws_abort_mutex);
|
||||||
ws_abort_io = [&ws] {
|
ws_abort_io = [&ws] {
|
||||||
beast::error_code ec;
|
ws.abort();
|
||||||
ws.next_layer().socket().shutdown(tcp::socket::shutdown_both, ec);
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ws.set_option(websocket::stream_base::decorator([&](websocket::request_type& req) {
|
|
||||||
req.set(http::field::user_agent, "OrcaSlicer");
|
|
||||||
if (!api_key.empty()) {
|
|
||||||
req.set("X-Api-Key", api_key);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
std::string host_header = endpoint.host;
|
std::string host_header = endpoint.host;
|
||||||
if (!endpoint.port.empty() && endpoint.port != "80") {
|
if (!endpoint.port.empty() && endpoint.port != (endpoint.secure ? "443" : "80")) {
|
||||||
host_header += ":" + endpoint.port;
|
host_header += ":" + endpoint.port;
|
||||||
}
|
}
|
||||||
ws.handshake(host_header, endpoint.target);
|
ws.handshake(host_header, endpoint.target);
|
||||||
@@ -2136,7 +2231,7 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba
|
|||||||
identify["params"]["type"] = "agent";
|
identify["params"]["type"] = "agent";
|
||||||
identify["params"]["url"] = "https://github.com/SoftFever/OrcaSlicer";
|
identify["params"]["url"] = "https://github.com/SoftFever/OrcaSlicer";
|
||||||
identify["id"] = 0;
|
identify["id"] = 0;
|
||||||
ws.write(net::buffer(identify.dump()));
|
ws.write(identify.dump());
|
||||||
|
|
||||||
std::set<std::string> subscribe_objects = {"print_stats", "virtual_sdcard"};
|
std::set<std::string> subscribe_objects = {"print_stats", "virtual_sdcard"};
|
||||||
std::set<std::string> available_objects;
|
std::set<std::string> available_objects;
|
||||||
@@ -2188,7 +2283,7 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba
|
|||||||
}
|
}
|
||||||
subscribe["params"]["objects"] = std::move(objects);
|
subscribe["params"]["objects"] = std::move(objects);
|
||||||
subscribe["id"] = 1;
|
subscribe["id"] = 1;
|
||||||
ws.write(net::buffer(subscribe.dump()));
|
ws.write(subscribe.dump());
|
||||||
|
|
||||||
// Eager fetch so AMS data is available immediately after connecting,
|
// Eager fetch so AMS data is available immediately after connecting,
|
||||||
// without waiting on the loop's own refresh clock below.
|
// without waiting on the loop's own refresh clock below.
|
||||||
@@ -2200,11 +2295,11 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba
|
|||||||
while (!ws_stop.load()) {
|
while (!ws_stop.load()) {
|
||||||
on_status_loop_tick(dev_id);
|
on_status_loop_tick(dev_id);
|
||||||
|
|
||||||
ws.next_layer().expires_after(std::chrono::seconds(2));
|
ws.expires_after(std::chrono::seconds(2));
|
||||||
beast::flat_buffer buffer;
|
std::string payload;
|
||||||
beast::error_code ec;
|
std::string read_error;
|
||||||
ws.read(buffer, ec);
|
const auto read_result = ws.read(payload, read_error);
|
||||||
if (ec == beast::error::timeout) {
|
if (read_result == MoonrakerWebsocket::ReadResult::timeout) {
|
||||||
const auto now_ms = static_cast<uint64_t>(
|
const auto now_ms = static_cast<uint64_t>(
|
||||||
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count());
|
std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now().time_since_epoch()).count());
|
||||||
const auto last_ms = ws_last_emit_ms.load();
|
const auto last_ms = ws_last_emit_ms.load();
|
||||||
@@ -2219,12 +2314,12 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba
|
|||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (ec == websocket::error::closed) {
|
if (read_result == MoonrakerWebsocket::ReadResult::closed) {
|
||||||
connection_lost = true;
|
connection_lost = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if (ec) {
|
if (read_result == MoonrakerWebsocket::ReadResult::error) {
|
||||||
BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: websocket read error: " << ec.message();
|
BOOST_LOG_TRIVIAL(warning) << "MoonrakerPrinterAgent: websocket read error: " << read_error;
|
||||||
connection_lost = true;
|
connection_lost = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -2239,7 +2334,7 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba
|
|||||||
ams_last_fetch_ms.store(now_ms);
|
ams_last_fetch_ms.store(now_ms);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
handle_ws_message(dev_id, beast::buffers_to_string(buffer.data()), base_url, api_key);
|
handle_ws_message(dev_id, std::move(payload), base_url, api_key);
|
||||||
// Check if handle_ws_message triggered reconnection request`
|
// Check if handle_ws_message triggered reconnection request`
|
||||||
if (ws_reconnect_requested.exchange(false)) {
|
if (ws_reconnect_requested.exchange(false)) {
|
||||||
connection_lost = true;
|
connection_lost = true;
|
||||||
@@ -2247,8 +2342,7 @@ void MoonrakerPrinterAgent::run_status_stream(std::string dev_id, std::string ba
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
beast::error_code ec;
|
ws.close();
|
||||||
ws.close(websocket::close_code::normal, ec);
|
|
||||||
|
|
||||||
// Only reset retry count on clean shutdown (not connection_lost)
|
// Only reset retry count on clean shutdown (not connection_lost)
|
||||||
if (!connection_lost && !ws_stop.load()) {
|
if (!connection_lost && !ws_stop.load()) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
#include <set>
|
#include <set>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
|
#include <chrono>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
#include <deque>
|
#include <deque>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
@@ -19,6 +20,35 @@ namespace Slic3r {
|
|||||||
|
|
||||||
bool moonraker_is_light_name(const std::string& name);
|
bool moonraker_is_light_name(const std::string& name);
|
||||||
|
|
||||||
|
class MoonrakerWebsocket
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
enum class ReadResult
|
||||||
|
{
|
||||||
|
message,
|
||||||
|
timeout,
|
||||||
|
closed,
|
||||||
|
error,
|
||||||
|
};
|
||||||
|
|
||||||
|
MoonrakerWebsocket(bool secure, std::string api_key);
|
||||||
|
~MoonrakerWebsocket();
|
||||||
|
|
||||||
|
void connect(const std::string& host, const std::string& port, std::chrono::seconds timeout);
|
||||||
|
void tls_handshake(const std::string& host);
|
||||||
|
void handshake(const std::string& host, const std::string& target);
|
||||||
|
void text(bool enabled);
|
||||||
|
void write(const std::string& body);
|
||||||
|
ReadResult read(std::string& payload, std::string& error_message);
|
||||||
|
void close();
|
||||||
|
void expires_after(std::chrono::seconds timeout);
|
||||||
|
void abort();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Impl;
|
||||||
|
std::unique_ptr<Impl> m_impl;
|
||||||
|
};
|
||||||
|
|
||||||
class MoonrakerPrinterAgent : public IPrinterAgent
|
class MoonrakerPrinterAgent : public IPrinterAgent
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
|
|||||||
Reference in New Issue
Block a user