Files
OrcaSlicer/src/slic3r/Utils/PrintHost.cpp
T
HanifKoh 84657ff11e Add Missing Includes Across the Remaining Sources and Tests (#16071)
* Ignore Clipper, libpng, mcut and Boost.Polygon Internals in clang-tidy

Each only works through a wrapper or umbrella header: libslic3r/clipper.hpp or clipper_z.hpp configure Clipper before including it, png.h pulls in libpng's config headers, and Boost.Polygon's headers only compile through polygon.hpp or voronoi.hpp.

* Ignore minilzo's Config Headers in clang-tidy

lzoconf.h and lzodefs.h are internal to minilzo.h, which is what the code includes.

* Add Missing Includes Across the Remaining Sources and Tests

Covers src/slic3r/Utils, src/slic3r/plugin, src/slic3r/Config, src/libvgcode, src/dev-utils, src/OrcaSlicer.cpp and tests/, the directories left after src/slic3r/GUI and src/libslic3r. Generated with clang-tidy misc-include-cleaner. libvgcode's own headers are included by relative path as in the rest of that library, and Catch2 and pybind11 with angle brackets as elsewhere in the repo.

* Make the GUI and Test Headers Compile on Their Own

Each now includes, or forward-declares, what it uses instead of relying on what its includers happened to include first. Headers that only compile on one platform, or that nothing built includes, are left alone.

* Keep Windows and nanosvg Setup Ahead of the Added Includes

OrcaSlicer.cpp and several tests set _WIN32_WINNT, WIN32_LEAN_AND_MEAN or NOMINMAX before including Windows.h, and the profile validator defines NANOSVG_IMPLEMENTATION before any libslic3r header. The added includes had landed above those blocks, which broke the Windows build.

* Add the GUI Includes the First Pass Missed

Covers headers that only became editable once they compiled on their own, and wx symbols whose suggested header changed as the clang-tidy ignore list grew after the src/slic3r/GUI pass.

* Keep the Added Test Includes Below the NOMINMAX Guard

test_marchingsquares.cpp and test_texture_displacement.cpp had includes inside #ifndef NOMINMAX, which the tests inherit as defined on Windows from libslic3r, so those were skipped there. .clang-tidy also ignores the MSVC STL and UCRT internals, Boost.Multiprecision's fwd.hpp and CPython's Windows include directory, as in #16068.
2026-10-03 13:45:21 +08:00

513 lines
16 KiB
C++

#include "PrintHost.hpp"
#include <boost/optional/optional.hpp>
#include "libslic3r/Config.hpp"
#include <string>
#include <boost/algorithm/string/predicate.hpp>
#include "slic3r/GUI/Gizmos/GLGizmoBase.hpp"
#include <cstddef>
#include <boost/filesystem/path.hpp>
#include <utility>
#include <memory>
#include <boost/filesystem/operations.hpp>
#include "libslic3r/LifecycleEvents.hpp"
#include "slic3r/GUI/GUI_App.hpp"
#include <vector>
#include <thread>
#include <exception>
#include <sstream>
#include <boost/optional.hpp>
#include <boost/log/trivial.hpp>
#include <boost/filesystem.hpp>
#include <nlohmann/json.hpp>
#include <boost/property_tree/ptree.hpp>
#include <boost/property_tree/json_parser.hpp>
#include <wx/event.h>
#include <wx/string.h>
#include <wx/app.h>
#include <wx/arrstr.h>
#include "libslic3r/PrintConfig.hpp"
#include "libslic3r/Channel.hpp"
#include "OctoPrint.hpp"
#include "Duet.hpp"
#include "UltiMaker.hpp"
#include "FlashAir.hpp"
#include "AstroBox.hpp"
#include "Repetier.hpp"
#include "MKS.hpp"
#include "ESP3D.hpp"
#include "CrealityPrint.hpp"
#include "../GUI/PrintHostDialogs.hpp"
#include "../GUI/MainFrame.hpp"
#include "slic3r/plugin/PluginManager.hpp"
#include "Obico.hpp"
#include "Flashforge.hpp"
#include "SimplyPrint.hpp"
#include "ElegooLink.hpp"
#include "3DPrinterOS.hpp"
#include "Moonraker.hpp"
namespace fs = boost::filesystem;
using boost::optional;
using Slic3r::GUI::PrintHostQueueDialog;
namespace Slic3r {
PrintHost::~PrintHost() {}
PrintHost* PrintHost::get_print_host(DynamicPrintConfig *config)
{
PrinterTechnology tech = ptFFF;
{
const auto opt = config->option<ConfigOptionEnum<PrinterTechnology>>("printer_technology");
if (opt != nullptr) {
tech = opt->value;
}
}
if (tech == ptFFF) {
const auto opt = config->option<ConfigOptionEnum<PrintHostType>>("host_type");
const auto host_type = opt != nullptr ? opt->value : htOctoPrint;
switch (host_type) {
case htOctoPrint: return new OctoPrint(config);
case htDuet: return new Duet(config);
case htUltiMaker: return new UltiMaker(config);
case htFlashAir: return new FlashAir(config);
case htAstroBox: return new AstroBox(config);
case htRepetier: return new Repetier(config);
case htPrusaLink: return new PrusaLink(config);
case htPrusaConnect: return new PrusaConnect(config);
case htMKS: return new MKS(config);
case htESP3D: return new ESP3D(config);
case htCrealityPrint: return new CrealityPrint(config);
case htObico: return new Obico(config);
case htFlashforge: return new Flashforge(config);
case htSimplyPrint: return new SimplyPrint(config);
case htElegooLink: return new ElegooLink(config);
case ht3DPrinterOS: return new C3DPrinterOS(config);
case htMoonraker: return new Moonraker(config);
default: return nullptr;
}
} else {
return new SL1Host(config);
}
}
std::string PrintHost::get_print_host_webui(DynamicPrintConfig* config)
{
if (config == nullptr)
return {};
std::string webui_url;
const auto* host_type_opt = config->option<ConfigOptionEnum<PrintHostType>>("host_type");
const auto host_type = host_type_opt != nullptr ? host_type_opt->value : htOctoPrint;
switch (host_type) {
case htElegooLink: {
webui_url = ElegooLink::get_print_host_webui(config);
break;
}
default: break;
}
if (webui_url.empty()) {
webui_url = config->opt_string("print_host_webui");
if (webui_url.empty())
webui_url = config->opt_string("print_host");
if (webui_url.empty())
return webui_url;
}
const bool has_http_scheme = boost::algorithm::istarts_with(webui_url, "http");
const bool has_file_scheme = boost::algorithm::istarts_with(webui_url, "file:");
if (!has_http_scheme && !has_file_scheme)
webui_url = "http://" + webui_url;
return webui_url;
}
namespace {
// Moonraker (Klipper's API server) reports a raised exception as { "error": { "code", "message", "traceback" } }
// under every host type that connects to it, often with the cause only in the traceback. Returns the reason to show,
// or empty for any other body.
std::string moonraker_error_reason(const std::string &body)
{
const auto root = nlohmann::json::parse(body, nullptr, false);
const auto err = root.find("error");
if (err == root.end())
return {};
const auto message = err->find("message");
const auto traceback = err->find("traceback");
if (message == err->end() || traceback == err->end() || !message->is_string() || !traceback->is_string())
return {};
const auto &msg = message->get_ref<const std::string &>();
const auto &tb = traceback->get_ref<const std::string &>();
if (msg.empty())
return {};
const auto end = tb.find_last_not_of(" \t\r\n");
if (end == std::string::npos)
return msg;
// Chained exceptions each start a new traceback; the one that failed the request is the last.
const auto header = tb.rfind("Traceback (most recent call last):", end);
// Tornado renders a raised HTTPError as "HTTP <code>: <reason>[ (<detail>)]", and the detail may span lines.
const auto code = err->find("code");
if (code != err->end() && code->is_number_integer()) {
const std::string marker = "HTTP " + std::to_string(code->get<int>()) + ": ";
const auto pos = tb.rfind(marker, end);
if (pos != std::string::npos && (header == std::string::npos || pos > header) && pos + marker.size() <= end) {
const std::string reason = tb.substr(pos + marker.size(), end + 1 - pos - marker.size());
// An HTTPError whose detail equals its reason, like HTTPError(401, "Unauthorized"), renders the phrase twice.
return reason == msg + " (" + msg + ")" ? msg : reason;
}
}
// Any other exception's type and message are everything from the first unindented line after its frames.
auto begin = (header == std::string::npos) ? std::string::npos : tb.find('\n', header);
while (begin != std::string::npos && begin < end) {
++begin;
if (tb[begin] != ' ' && tb[begin] != '\r' && tb[begin] != '\n')
break;
begin = tb.find('\n', begin);
}
if (begin == std::string::npos || begin > end) {
const auto nl = tb.rfind('\n', end);
begin = (nl == std::string::npos) ? 0 : nl + 1;
}
return msg + " (" + tb.substr(begin, end + 1 - begin) + ")";
}
} // namespace
int PrintHost::get_err_code_from_body(const std::string &body)
{
boost::property_tree::ptree root;
std::istringstream iss(body);
try {
boost::property_tree::read_json(iss, root);
} catch (const std::exception &ex) {
BOOST_LOG_TRIVIAL(error) << "PrintHost: response is not valid JSON: " << ex.what();
return -1;
}
return root.get<int>("err", 0);
}
wxString PrintHost::format_error(const std::string &body, const std::string &error, unsigned status) const
{
if (status != 0) {
const std::string reason = moonraker_error_reason(body);
auto wxbody = wxString::FromUTF8(reason.empty() ? body : reason);
return wxString::Format("HTTP %u: %s", status, wxbody);
} else {
if (error.find("curl:Timeout was reached") != std::string::npos) {
return _L("Connection timed out. Please check if the printer and computer network are functioning properly, and confirm that they are on the same network.");
}else if(error.find("curl:Couldn't resolve host name")!= std::string::npos){
return _L("The Hostname/IP/URL could not be parsed, please check it and try again.");
} else if (error.find("Connection was reset") != std::string::npos){
return _L("File/data transfer interrupted. Please check the printer and network, then try it again.");
}
else {
return wxString::FromUTF8(error.data());
}
}
}
struct PrintHostJobQueue::priv
{
// XXX: comment on how bg thread works
PrintHostJobQueue *q;
Channel<PrintHostJob> channel_jobs;
Channel<size_t> channel_cancels;
size_t job_id = 0;
int prev_progress = -1;
fs::path source_to_remove;
std::thread bg_thread;
bool bg_exit = false;
PrintHostQueueDialog *queue_dialog;
priv(PrintHostJobQueue *q) : q(q) {}
void emit_progress(int progress);
void emit_error(wxString error);
void emit_cancel(size_t id);
void emit_info(wxString tag, wxString status);
void start_bg_thread();
void stop_bg_thread();
void bg_thread_main();
void progress_fn(Http::Progress progress, bool &cancel);
void error_fn(wxString error);
void info_fn(wxString tag, wxString status);
void remove_source(const fs::path &path);
void remove_source();
void perform_job(PrintHostJob the_job);
};
PrintHostJobQueue::PrintHostJobQueue(PrintHostQueueDialog *queue_dialog)
: p(new priv(this))
{
p->queue_dialog = queue_dialog;
}
PrintHostJobQueue::~PrintHostJobQueue()
{
if (p) { p->stop_bg_thread(); }
}
void PrintHostJobQueue::priv::emit_progress(int progress)
{
auto evt = new PrintHostQueueDialog::Event(GUI::EVT_PRINTHOST_PROGRESS, queue_dialog->GetId(), job_id, progress);
wxQueueEvent(queue_dialog, evt);
}
void PrintHostJobQueue::priv::emit_error(wxString error)
{
auto evt = new PrintHostQueueDialog::Event(GUI::EVT_PRINTHOST_ERROR, queue_dialog->GetId(), job_id, std::move(error));
wxQueueEvent(queue_dialog, evt);
}
void PrintHostJobQueue::priv::emit_info(wxString tag, wxString status)
{
auto evt = new PrintHostQueueDialog::Event(GUI::EVT_PRINTHOST_INFO, queue_dialog->GetId(), job_id, std::move(tag), std::move(status));
wxQueueEvent(queue_dialog, evt);
}
void PrintHostJobQueue::priv::emit_cancel(size_t id)
{
auto evt = new PrintHostQueueDialog::Event(GUI::EVT_PRINTHOST_CANCEL, queue_dialog->GetId(), id);
wxQueueEvent(queue_dialog, evt);
}
void PrintHostJobQueue::priv::start_bg_thread()
{
if (bg_thread.joinable()) { return; }
std::shared_ptr<priv> p2 = q->p;
bg_thread = std::thread([p2]() {
p2->bg_thread_main();
});
}
void PrintHostJobQueue::priv::stop_bg_thread()
{
if (bg_thread.joinable()) {
bg_exit = true;
channel_jobs.push(PrintHostJob()); // Push an empty job to wake up bg_thread in case it's sleeping
bg_thread.detach(); // Let the background thread go, it should exit on its own
}
}
void PrintHostJobQueue::priv::bg_thread_main()
{
// bg thread entry point
try {
// Pick up jobs from the job channel:
while (! bg_exit) {
auto job = channel_jobs.pop(); // Sleeps in a cond var if there are no jobs
if (job.empty()) {
// This happens when the thread is being stopped
break;
}
source_to_remove = job.upload_data.source_path;
BOOST_LOG_TRIVIAL(debug) << boost::format("PrintHostJobQueue/bg_thread: Received job: [%1%]: `%2%` -> `%3%`, cancelled: %4%")
% job_id
% job.upload_data.upload_path
% job.printhost->get_host()
% job.cancelled;
if (! job.cancelled) {
perform_job(std::move(job));
}
remove_source();
job_id++;
}
} catch (const std::exception &e) {
emit_error(e.what());
}
// Cleanup leftover files, if any
remove_source();
auto jobs = channel_jobs.lock_rw();
for (const PrintHostJob &job : *jobs) {
remove_source(job.upload_data.source_path);
}
}
void PrintHostJobQueue::priv::progress_fn(Http::Progress progress, bool &cancel)
{
if (cancel) {
// When cancel is true from the start, Http indicates request has been cancelled
emit_cancel(job_id);
return;
}
if (bg_exit) {
cancel = true;
return;
}
if (channel_cancels.size_hint() > 0) {
// Lock both queues
auto cancels = channel_cancels.lock_rw();
auto jobs = channel_jobs.lock_rw();
for (size_t cancel_id : *cancels) {
if (cancel_id == job_id) {
cancel = true;
} else if (cancel_id > job_id) {
const size_t idx = cancel_id - job_id - 1;
if (idx < jobs->size()) {
jobs->at(idx).cancelled = true;
BOOST_LOG_TRIVIAL(debug) << boost::format("PrintHostJobQueue: Job id %1% cancelled") % cancel_id;
emit_cancel(cancel_id);
}
}
}
cancels->clear();
}
if (! cancel) {
int gui_progress = progress.ultotal > 0 ? 100*progress.ulnow / progress.ultotal : 0;
if (gui_progress != prev_progress) {
emit_progress(gui_progress);
prev_progress = gui_progress;
}
}
}
void PrintHostJobQueue::priv::error_fn(wxString error)
{
// check if transfer was not canceled before error occured - than do not show the error
bool do_emit_err = true;
if (channel_cancels.size_hint() > 0) {
// Lock both queues
auto cancels = channel_cancels.lock_rw();
auto jobs = channel_jobs.lock_rw();
for (size_t cancel_id : *cancels) {
if (cancel_id == job_id) {
do_emit_err = false;
emit_cancel(job_id);
}
else if (cancel_id > job_id) {
const size_t idx = cancel_id - job_id - 1;
if (idx < jobs->size()) {
jobs->at(idx).cancelled = true;
BOOST_LOG_TRIVIAL(debug) << boost::format("PrintHostJobQueue: Job id %1% cancelled") % cancel_id;
emit_cancel(cancel_id);
}
}
}
cancels->clear();
}
if (do_emit_err)
emit_error(std::move(error));
}
void PrintHostJobQueue::priv::info_fn(wxString tag, wxString status)
{
emit_info(tag, status);
}
void PrintHostJobQueue::priv::remove_source(const fs::path &path)
{
if (! path.empty()) {
boost::system::error_code ec;
fs::remove(path, ec);
if (ec) {
BOOST_LOG_TRIVIAL(error) << boost::format("PrintHostJobQueue: Error removing file `%1%`: %2%") % path % ec;
}
}
}
void PrintHostJobQueue::priv::remove_source()
{
remove_source(source_to_remove);
source_to_remove.clear();
}
bool PrintHostJobQueue::upload_job(PrintHostJob &job, PrintHost::ProgressFn progress_fn, PrintHost::ErrorFn error_fn, PrintHost::InfoFn info_fn)
{
// Captured before upload_data is moved into upload() below.
const std::string upload_filename = job.upload_data.source_path.filename().string();
{
LifecycleEventContext ctx;
ctx.name = upload_filename;
ctx.code = LifecycleEvtCode::Ok;
fire_lifecycle_event(LifecycleEvent::UploadStarted, ctx);
}
bool success = false;
std::string error;
// A throwing upload must not stop the worker, or later jobs would stay queued forever.
try {
success = job.printhost->upload(std::move(job.upload_data), std::move(progress_fn), error_fn, std::move(info_fn));
} catch (const std::exception &e) {
error = e.what();
error_fn(error);
}
{
LifecycleEventContext ctx;
ctx.name = upload_filename;
ctx.code = success ? LifecycleEvtCode::Ok : LifecycleEvtCode::Error;
ctx.msg = error;
fire_lifecycle_event(LifecycleEvent::UploadFinished, ctx);
}
return success;
}
void PrintHostJobQueue::priv::perform_job(PrintHostJob the_job)
{
emit_progress(0); // Indicate the upload is starting
bool success = PrintHostJobQueue::upload_job(the_job,
[this](Http::Progress progress, bool &cancel) { this->progress_fn(std::move(progress), cancel); },
[this](wxString error) { this->error_fn(std::move(error)); },
[this](wxString tag, wxString host) { this->info_fn(std::move(tag), std::move(host)); }
);
if (success) {
emit_progress(100);
if (the_job.switch_to_device_tab) {
const auto mainframe = GUI::wxGetApp().mainframe;
mainframe->request_select_tab(TAB_ID_MONITOR);
}
}
}
void PrintHostJobQueue::enqueue(PrintHostJob job)
{
p->start_bg_thread();
p->queue_dialog->append_job(job);
p->channel_jobs.push(std::move(job));
}
void PrintHostJobQueue::cancel(size_t id)
{
p->channel_cancels.push(id);
}
}