mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-29 11:51:24 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8176013383 | ||
|
|
dd593317fd | ||
|
|
ec0d8c225f | ||
|
|
3040ebaac1 | ||
|
|
5298e49dd2 | ||
|
|
d347a80ef8 | ||
|
|
e340a13c18 | ||
|
|
9e16cdb23b | ||
|
|
58842bab05 | ||
|
|
04204ec0d3 | ||
|
|
9ed459e132 |
+10
-5
@@ -2504,7 +2504,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
|||||||
|
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(print->model().id().id);
|
ctx.id = std::to_string(print->model().id().id);
|
||||||
|
ctx.name = print->get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Ok;
|
ctx.code = LifecycleEvtCode::Ok;
|
||||||
ctx.msg = path;
|
ctx.msg = path;
|
||||||
ctx.cancellation_check = [print]() { return print->canceled(); };
|
ctx.cancellation_check = [print]() { return print->canceled(); };
|
||||||
@@ -2556,7 +2557,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
|||||||
}
|
}
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(print->model().id().id);
|
ctx.id = std::to_string(print->model().id().id);
|
||||||
|
ctx.name = print->get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Error;
|
ctx.code = LifecycleEvtCode::Error;
|
||||||
ctx.msg = std::string(path) + "\n" + err_msg;
|
ctx.msg = std::string(path) + "\n" + err_msg;
|
||||||
ctx.cancellation_check = [print]() { return print->canceled(); };
|
ctx.cancellation_check = [print]() { return print->canceled(); };
|
||||||
@@ -2580,7 +2582,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
|||||||
boost::nowide::remove(path_tmp.c_str());
|
boost::nowide::remove(path_tmp.c_str());
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(print->model().id().id);
|
ctx.id = std::to_string(print->model().id().id);
|
||||||
|
ctx.name = print->get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Error;
|
ctx.code = LifecycleEvtCode::Error;
|
||||||
ctx.msg = std::string(path) + "\n" + ex.what();
|
ctx.msg = std::string(path) + "\n" + ex.what();
|
||||||
ctx.cancellation_check = [print]() { return print->canceled(); };
|
ctx.cancellation_check = [print]() { return print->canceled(); };
|
||||||
@@ -2693,7 +2696,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
|||||||
if (ret) {
|
if (ret) {
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(print->model().id().id);
|
ctx.id = std::to_string(print->model().id().id);
|
||||||
|
ctx.name = print->get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Error;
|
ctx.code = LifecycleEvtCode::Error;
|
||||||
ctx.msg = std::string(path) + "\nFailed to rename the output G-code file: " + ret.message();
|
ctx.msg = std::string(path) + "\nFailed to rename the output G-code file: " + ret.message();
|
||||||
ctx.cancellation_check = [print]() { return print->canceled(); };
|
ctx.cancellation_check = [print]() { return print->canceled(); };
|
||||||
@@ -2712,7 +2716,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
|||||||
|
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(print->model().id().id);
|
ctx.id = std::to_string(print->model().id().id);
|
||||||
|
ctx.name = print->get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Ok;
|
ctx.code = LifecycleEvtCode::Ok;
|
||||||
ctx.msg = path;
|
ctx.msg = path;
|
||||||
ctx.cancellation_check = [print]() { return print->canceled(); };
|
ctx.cancellation_check = [print]() { return print->canceled(); };
|
||||||
|
|||||||
+10
-5
@@ -2717,7 +2717,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
|||||||
|
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(m_model.id().id);
|
ctx.id = std::to_string(m_model.id().id);
|
||||||
|
ctx.name = get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Ok;
|
ctx.code = LifecycleEvtCode::Ok;
|
||||||
ctx.cancellation_check = [this]() { return canceled(); };
|
ctx.cancellation_check = [this]() { return canceled(); };
|
||||||
fire_lifecycle_event(LifecycleEvent::SliceStarted, ctx);
|
fire_lifecycle_event(LifecycleEvent::SliceStarted, ctx);
|
||||||
@@ -3344,7 +3345,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
|||||||
|
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(m_model.id().id);
|
ctx.id = std::to_string(m_model.id().id);
|
||||||
|
ctx.name = get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Ok;
|
ctx.code = LifecycleEvtCode::Ok;
|
||||||
ctx.cancellation_check = [this]() { return canceled(); };
|
ctx.cancellation_check = [this]() { return canceled(); };
|
||||||
fire_lifecycle_event(LifecycleEvent::SliceGeometryFinished, ctx);
|
fire_lifecycle_event(LifecycleEvent::SliceGeometryFinished, ctx);
|
||||||
@@ -4950,7 +4952,8 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
|
|||||||
{
|
{
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(m_model.id().id);
|
ctx.id = std::to_string(m_model.id().id);
|
||||||
|
ctx.name = get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Ok;
|
ctx.code = LifecycleEvtCode::Ok;
|
||||||
ctx.msg = file;
|
ctx.msg = file;
|
||||||
ctx.cancellation_check = [this]() { return canceled(); };
|
ctx.cancellation_check = [this]() { return canceled(); };
|
||||||
@@ -4980,7 +4983,8 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
|
|||||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": found errors when process gcode file %1%") %file.c_str();
|
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": found errors when process gcode file %1%") %file.c_str();
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(m_model.id().id);
|
ctx.id = std::to_string(m_model.id().id);
|
||||||
|
ctx.name = get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Error;
|
ctx.code = LifecycleEvtCode::Error;
|
||||||
ctx.msg = file + "\n" + ex.what();
|
ctx.msg = file + "\n" + ex.what();
|
||||||
ctx.cancellation_check = [this]() { return canceled(); };
|
ctx.cancellation_check = [this]() { return canceled(); };
|
||||||
@@ -4994,7 +4998,8 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
|
|||||||
|
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = std::to_string(m_model.id().id);
|
ctx.id = std::to_string(m_model.id().id);
|
||||||
|
ctx.name = get_model_name();
|
||||||
ctx.code = LifecycleEvtCode::Ok;
|
ctx.code = LifecycleEvtCode::Ok;
|
||||||
ctx.msg = file;
|
ctx.msg = file;
|
||||||
ctx.cancellation_check = [this]() { return canceled(); };
|
ctx.cancellation_check = [this]() { return canceled(); };
|
||||||
|
|||||||
@@ -256,13 +256,10 @@ extern bool is_gallery_file(const std::string& path, char const* type);
|
|||||||
extern bool is_shapes_dir(const std::string& dir);
|
extern bool is_shapes_dir(const std::string& dir);
|
||||||
//BBS: add json support
|
//BBS: add json support
|
||||||
extern bool is_json_file(const std::string& path);
|
extern bool is_json_file(const std::string& path);
|
||||||
// True if rel_path is relative, has no ".." component or embedded NUL and, joined to root, still resolves inside it.
|
// True if rel_path is relative, has no ".." component and, joined to root, still resolves inside it.
|
||||||
// Both '/' and '\\' are treated as separators on every platform, so an archive rejected on one OS
|
// Both '/' and '\\' are treated as separators on every platform, so an archive rejected on one OS
|
||||||
// is rejected on all of them.
|
// is rejected on all of them.
|
||||||
extern bool is_path_within_root(const std::string &rel_path, const boost::filesystem::path &root);
|
extern bool is_path_within_root(const std::string &rel_path, const boost::filesystem::path &root);
|
||||||
// True if a symlink stored at link_rel_path (relative to root) with this target stays inside root: the target
|
|
||||||
// must be relative, and joined to the link's directory it must pass is_path_within_root.
|
|
||||||
extern bool is_symlink_target_within_root(const std::string &link_rel_path, const std::string &target, const boost::filesystem::path &root);
|
|
||||||
|
|
||||||
// Orca: custom protocal support utils
|
// Orca: custom protocal support utils
|
||||||
inline bool is_orca_open(const std::string& url) { return boost::starts_with(url, "orcaslicer://open"); }
|
inline bool is_orca_open(const std::string& url) { return boost::starts_with(url, "orcaslicer://open"); }
|
||||||
|
|||||||
@@ -4,9 +4,6 @@
|
|||||||
#include "miniz_extension.hpp"
|
#include "miniz_extension.hpp"
|
||||||
#include "Utils.hpp"
|
#include "Utils.hpp"
|
||||||
|
|
||||||
#include <boost/filesystem.hpp>
|
|
||||||
#include <boost/log/trivial.hpp>
|
|
||||||
|
|
||||||
#if defined(_MSC_VER) || defined(__MINGW64__)
|
#if defined(_MSC_VER) || defined(__MINGW64__)
|
||||||
#include "boost/nowide/cstdio.hpp"
|
#include "boost/nowide/cstdio.hpp"
|
||||||
#endif
|
#endif
|
||||||
@@ -118,66 +115,6 @@ std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_
|
|||||||
return decode_zip_unicode_path_extra_field(extra.substr(0, extra_size > 0 ? extra_size - 1 : 0), stat.m_filename);
|
return decode_zip_unicode_path_extra_field(extra.substr(0, extra_size > 0 ? extra_size - 1 : 0), stat.m_filename);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool extract_archive_confined(const std::string &zip_path_utf8, const std::string &dest_dir)
|
|
||||||
{
|
|
||||||
mz_zip_archive archive;
|
|
||||||
mz_zip_zero_struct(&archive);
|
|
||||||
|
|
||||||
if (!open_zip_reader(&archive, zip_path_utf8)) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "Unable to open zip reader for " << zip_path_utf8;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mz_uint num_entries = mz_zip_reader_get_num_files(&archive);
|
|
||||||
mz_zip_archive_file_stat stat;
|
|
||||||
|
|
||||||
// Validate every entry first so an archive with a single escaping entry leaves no partial output behind.
|
|
||||||
const boost::filesystem::path root(dest_dir);
|
|
||||||
for (mz_uint i = 0; i < num_entries; ++i) {
|
|
||||||
if (mz_zip_reader_file_stat(&archive, i, &stat) && !is_path_within_root(stat.m_filename, root)) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "Unzip: rejecting " << zip_path_utf8 << ", entry " << stat.m_filename << " resolves outside " << dest_dir;
|
|
||||||
close_zip_reader(&archive);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (mz_uint i = 0; i < num_entries; ++i) {
|
|
||||||
if (!mz_zip_reader_file_stat(&archive, i, &stat)) {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << "Unzip: read file stat failed";
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const std::string dest_file = dest_dir + "/" + stat.m_filename;
|
|
||||||
try {
|
|
||||||
if (stat.m_is_directory) {
|
|
||||||
const boost::filesystem::path dest_path(dest_file);
|
|
||||||
if (!boost::filesystem::exists(dest_path))
|
|
||||||
boost::filesystem::create_directories(dest_path);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (stat.m_uncomp_size == 0) {
|
|
||||||
BOOST_LOG_TRIVIAL(warning) << "Unzip: invalid size for file " << stat.m_filename;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Replace a symlink at the destination rather than writing through it.
|
|
||||||
const boost::filesystem::path dest_path(dest_file);
|
|
||||||
if (boost::filesystem::is_symlink(boost::filesystem::symlink_status(dest_path)))
|
|
||||||
boost::filesystem::remove(dest_path);
|
|
||||||
if (!mz_zip_reader_extract_to_file(&archive, stat.m_file_index, dest_file.c_str(), 0)) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "Unzip: extract file " << stat.m_filename << " to dest " << dest_file << " failed";
|
|
||||||
close_zip_reader(&archive);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
BOOST_LOG_TRIVIAL(info) << "Unzip: successfully extract file " << stat.m_file_index << " to " << dest_file;
|
|
||||||
} catch (const std::exception &e) {
|
|
||||||
close_zip_reader(&archive);
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "Unzip: archive read exception: " << e.what();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
close_zip_reader(&archive);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
MZ_Archive::MZ_Archive()
|
MZ_Archive::MZ_Archive()
|
||||||
{
|
{
|
||||||
mz_zip_zero_struct(&arch);
|
mz_zip_zero_struct(&arch);
|
||||||
|
|||||||
@@ -11,8 +11,6 @@ bool open_zip_writer(mz_zip_archive *zip, const std::string &fname_utf8);
|
|||||||
bool close_zip_reader(mz_zip_archive *zip);
|
bool close_zip_reader(mz_zip_archive *zip);
|
||||||
bool close_zip_writer(mz_zip_archive *zip);
|
bool close_zip_writer(mz_zip_archive *zip);
|
||||||
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat);
|
std::string decode_archive_entry_path(mz_zip_archive *zip, const mz_zip_archive_file_stat &stat);
|
||||||
// Extracts every entry of the archive under dest_dir. Nothing is written if any entry would resolve outside dest_dir.
|
|
||||||
bool extract_archive_confined(const std::string &zip_path_utf8, const std::string &dest_dir);
|
|
||||||
|
|
||||||
class MZ_Archive {
|
class MZ_Archive {
|
||||||
public:
|
public:
|
||||||
|
|||||||
+1
-16
@@ -1093,9 +1093,6 @@ bool is_path_within_root(const std::string &rel_path, const boost::filesystem::p
|
|||||||
auto is_separator = [](char c) { return c == '/' || c == '\\'; };
|
auto is_separator = [](char c) { return c == '/' || c == '\\'; };
|
||||||
if (rel_path.empty() || is_separator(rel_path.front()) || (rel_path.size() > 1 && rel_path[1] == ':'))
|
if (rel_path.empty() || is_separator(rel_path.front()) || (rel_path.size() > 1 && rel_path[1] == ':'))
|
||||||
return false;
|
return false;
|
||||||
// The filesystem calls stop at a NUL, so they would act on a shorter path than the one checked here.
|
|
||||||
if (rel_path.find('\0') != std::string::npos)
|
|
||||||
return false;
|
|
||||||
for (size_t start = 0; start <= rel_path.size();) {
|
for (size_t start = 0; start <= rel_path.size();) {
|
||||||
size_t end = start;
|
size_t end = start;
|
||||||
while (end < rel_path.size() && !is_separator(rel_path[end]))
|
while (end < rel_path.size() && !is_separator(rel_path[end]))
|
||||||
@@ -1106,10 +1103,7 @@ bool is_path_within_root(const std::string &rel_path, const boost::filesystem::p
|
|||||||
}
|
}
|
||||||
// Resolve against the canonical root so a symlink inside it cannot lead back out.
|
// Resolve against the canonical root so a symlink inside it cannot lead back out.
|
||||||
try {
|
try {
|
||||||
std::string root_str = boost::filesystem::weakly_canonical(root).string();
|
const std::string root_str = boost::filesystem::weakly_canonical(root).string();
|
||||||
// A trailing separator on root would otherwise fail the prefix match below for every path.
|
|
||||||
while (!root_str.empty() && (root_str.back() == '/' || root_str.back() == boost::filesystem::path::preferred_separator))
|
|
||||||
root_str.pop_back();
|
|
||||||
const std::string full_str = boost::filesystem::weakly_canonical(root / rel_path).string();
|
const std::string full_str = boost::filesystem::weakly_canonical(root / rel_path).string();
|
||||||
return full_str.compare(0, root_str.size(), root_str) == 0 &&
|
return full_str.compare(0, root_str.size(), root_str) == 0 &&
|
||||||
(full_str.size() == root_str.size() || full_str[root_str.size()] == boost::filesystem::path::preferred_separator);
|
(full_str.size() == root_str.size() || full_str[root_str.size()] == boost::filesystem::path::preferred_separator);
|
||||||
@@ -1118,15 +1112,6 @@ bool is_path_within_root(const std::string &rel_path, const boost::filesystem::p
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bool is_symlink_target_within_root(const std::string &link_rel_path, const std::string &target, const boost::filesystem::path &root)
|
|
||||||
{
|
|
||||||
if (target.empty() || target.front() == '/' || target.front() == '\\' || (target.size() > 1 && target[1] == ':'))
|
|
||||||
return false;
|
|
||||||
// A relative target without ".." only descends from the link's directory, so no chain of such links can leave root.
|
|
||||||
const size_t sep = link_rel_path.find_last_of("/\\");
|
|
||||||
return is_path_within_root((sep == std::string::npos ? std::string() : link_rel_path.substr(0, sep + 1)) + target, root);
|
|
||||||
}
|
|
||||||
|
|
||||||
bool is_img_file(const std::string &path)
|
bool is_img_file(const std::string &path)
|
||||||
{
|
{
|
||||||
return boost::iends_with(path, ".png") || boost::iends_with(path, ".svg");
|
return boost::iends_with(path, ".png") || boost::iends_with(path, ".svg");
|
||||||
|
|||||||
@@ -1512,33 +1512,11 @@ int GUI_App::install_plugin(std::string name, std::string package_name, InstallP
|
|||||||
size_t n = mz_zip_reader_get_extra(&archive, stat.m_file_index, extra.data(), extra.size());
|
size_t n = mz_zip_reader_get_extra(&archive, stat.m_file_index, extra.data(), extra.size());
|
||||||
dest_file = decode(extra.substr(0, n), stat.m_filename);
|
dest_file = decode(extra.substr(0, n), stat.m_filename);
|
||||||
}
|
}
|
||||||
if (!is_path_within_root(dest_file, plugin_folder)) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "[install_plugin] entry " << dest_file << " resolves outside " << plugin_folder.string();
|
|
||||||
close_zip_reader(&archive);
|
|
||||||
if (pro_fn) { pro_fn(InstallStatusUnzipFailed, 0, cancel); }
|
|
||||||
return InstallStatusUnzipFailed;
|
|
||||||
}
|
|
||||||
auto dest_path = plugin_folder / dest_file;
|
auto dest_path = plugin_folder / dest_file;
|
||||||
std::string dest_zip_file = encode_path(dest_path.string().c_str());
|
|
||||||
#ifndef WIN32
|
|
||||||
// Validate a symlink's target before anything at the destination is replaced.
|
|
||||||
const bool is_link = S_ISLNK(stat.m_external_attr >> 16);
|
|
||||||
std::string link;
|
|
||||||
if (is_link) {
|
|
||||||
link.assign(stat.m_uncomp_size, 0);
|
|
||||||
if (!mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, link.data(), stat.m_uncomp_size, 0) ||
|
|
||||||
!is_symlink_target_within_root(dest_file, link, plugin_folder)) {
|
|
||||||
BOOST_LOG_TRIVIAL(error) << "[install_plugin] link " << dest_file << " -> " << link << " is unreadable or resolves outside " << plugin_folder.string();
|
|
||||||
close_zip_reader(&archive);
|
|
||||||
if (pro_fn) { pro_fn(InstallStatusUnzipFailed, 0, cancel); }
|
|
||||||
return InstallStatusUnzipFailed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
try {
|
|
||||||
boost::filesystem::create_directories(dest_path.parent_path());
|
boost::filesystem::create_directories(dest_path.parent_path());
|
||||||
// symlink_status so that an existing symlink, dangling or not, is replaced rather than written through.
|
std::string dest_zip_file = encode_path(dest_path.string().c_str());
|
||||||
if (fs::exists(fs::symlink_status(dest_path))) {
|
try {
|
||||||
|
if (fs::exists(dest_path)) {
|
||||||
boost::system::error_code ec;
|
boost::system::error_code ec;
|
||||||
fs::remove(dest_path, ec);
|
fs::remove(dest_path, ec);
|
||||||
if (ec) {
|
if (ec) {
|
||||||
@@ -1566,8 +1544,9 @@ int GUI_App::install_plugin(std::string name, std::string package_name, InstallP
|
|||||||
}
|
}
|
||||||
mz_bool res = 0;
|
mz_bool res = 0;
|
||||||
#ifndef WIN32
|
#ifndef WIN32
|
||||||
if (is_link) {
|
if (S_ISLNK(stat.m_external_attr >> 16)) {
|
||||||
res = 1;
|
std::string link(stat.m_uncomp_size + 1, 0);
|
||||||
|
res = mz_zip_reader_extract_to_mem(&archive, stat.m_file_index, link.data(), stat.m_uncomp_size, 0);
|
||||||
try {
|
try {
|
||||||
boost::filesystem::create_symlink(link, dest_path);
|
boost::filesystem::create_symlink(link, dest_path);
|
||||||
} catch (const std::exception &e) {
|
} catch (const std::exception &e) {
|
||||||
|
|||||||
@@ -12800,8 +12800,6 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
|
|||||||
notification_manager->set_slicing_progress_export_possible();
|
notification_manager->set_slicing_progress_export_possible();
|
||||||
|
|
||||||
// Reset the "export G-code path" name, so that the automatic background processing will be enabled again.
|
// Reset the "export G-code path" name, so that the automatic background processing will be enabled again.
|
||||||
const std::string lifecycle_job_name = this->background_process.fff_print() ?
|
|
||||||
this->background_process.fff_print()->output_filename() : std::string();
|
|
||||||
this->background_process.reset_export();
|
this->background_process.reset_export();
|
||||||
// This bool stops showing export finished notification even when process_completed_with_error is false
|
// This bool stops showing export finished notification even when process_completed_with_error is false
|
||||||
bool has_error = false;
|
bool has_error = false;
|
||||||
@@ -12848,8 +12846,18 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
|
|||||||
|
|
||||||
{
|
{
|
||||||
Slic3r::LifecycleEventContext ctx;
|
Slic3r::LifecycleEventContext ctx;
|
||||||
ctx.name = lifecycle_job_name;
|
if (const PrintBase* print = this->background_process.current_print()) {
|
||||||
ctx.code = evt.cancelled() ? Slic3r::LifecycleEvtCode::Warn : (has_error ? Slic3r::LifecycleEvtCode::Error : Slic3r::LifecycleEvtCode::Ok);
|
const Model& model = print->model();
|
||||||
|
ctx.id = std::to_string(model.id().id);
|
||||||
|
if (model.model_info)
|
||||||
|
ctx.name = model.model_info->model_name;
|
||||||
|
} else {
|
||||||
|
// Realistically Printbase* print will never be null because select_technology already asserts an active print
|
||||||
|
// and the worker thread asserts it before processing.
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << __FUNCTION__ << ": slicing completed without an active print; lifecycle event has no model ID";
|
||||||
|
}
|
||||||
|
ctx.code = evt.cancelled() ? Slic3r::LifecycleEvtCode::Warn :
|
||||||
|
(has_error ? Slic3r::LifecycleEvtCode::Error : Slic3r::LifecycleEvtCode::Ok);
|
||||||
ctx.msg = evt.cancelled() ? "cancelled" : (has_error ? lifecycle_error_msg : std::string());
|
ctx.msg = evt.cancelled() ? "cancelled" : (has_error ? lifecycle_error_msg : std::string());
|
||||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::SlicingJobComplete, ctx);
|
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::SlicingJobComplete, ctx);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,14 +31,15 @@ int UserManager::parse_json(std::string payload)
|
|||||||
{
|
{
|
||||||
bool restored_json = false;
|
bool restored_json = false;
|
||||||
json j;
|
json j;
|
||||||
|
|
||||||
|
//bind/unbind
|
||||||
|
|
||||||
|
try {
|
||||||
json j_pre = json::parse(payload);
|
json j_pre = json::parse(payload);
|
||||||
if (j_pre.empty()) {
|
if (j_pre.empty()) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
//bind/unbind
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (j_pre.contains("bind")) {
|
if (j_pre.contains("bind")) {
|
||||||
if (j_pre["bind"].contains("command")) {
|
if (j_pre["bind"].contains("command")) {
|
||||||
|
|
||||||
|
|||||||
@@ -274,13 +274,4 @@ bool Duet::start_print(wxString &msg, const std::string &filename, ConnectionTyp
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
int Duet::get_err_code_from_body(const std::string &body) const
|
|
||||||
{
|
|
||||||
pt::ptree root;
|
|
||||||
std::istringstream iss (body); // wrap returned json to istringstream
|
|
||||||
pt::read_json(iss, root);
|
|
||||||
|
|
||||||
return root.get<int>("err", 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ private:
|
|||||||
ConnectionType connect(wxString &msg) const;
|
ConnectionType connect(wxString &msg) const;
|
||||||
void disconnect(ConnectionType connectionType) const;
|
void disconnect(ConnectionType connectionType) const;
|
||||||
bool start_print(wxString &msg, const std::string &filename, ConnectionType connectionType, bool simulationMode) const;
|
bool start_print(wxString &msg, const std::string &filename, ConnectionType connectionType, bool simulationMode) const;
|
||||||
int get_err_code_from_body(const std::string &body) const;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -146,15 +146,6 @@ bool ESP3D::start_print(wxString& msg, const std::string& filename) const
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
int ESP3D::get_err_code_from_body(const std::string& body) const
|
|
||||||
{
|
|
||||||
pt::ptree root;
|
|
||||||
std::istringstream iss(body); // wrap returned json to istringstream
|
|
||||||
pt::read_json(iss, root);
|
|
||||||
|
|
||||||
return root.get<int>("err", 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ESP3D only accepts 8.3 filenames else it crashes marlin and other undefined behaviour
|
// ESP3D only accepts 8.3 filenames else it crashes marlin and other undefined behaviour
|
||||||
std::string ESP3D::get_short_name(const std::string& filename) const
|
std::string ESP3D::get_short_name(const std::string& filename) const
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ private:
|
|||||||
std::string m_console_port;
|
std::string m_console_port;
|
||||||
|
|
||||||
bool start_print(wxString& msg, const std::string& filename) const;
|
bool start_print(wxString& msg, const std::string& filename) const;
|
||||||
int get_err_code_from_body(const std::string& body) const;
|
|
||||||
std::string get_short_name(const std::string& filename) const;
|
std::string get_short_name(const std::string& filename) const;
|
||||||
std::string format_command(const std::string& path, const std::string& arg, const std::string& val) const;
|
std::string format_command(const std::string& path, const std::string& arg, const std::string& val) const;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -510,12 +510,22 @@ bool Flashforge::fetch_material_slots(std::vector<FlashforgeMaterialSlot>& slots
|
|||||||
if (!request_local_api_json("detail", json{{"serialNumber", m_serial_number}, {"checkCode", m_check_code}}.dump(), body, msg))
|
if (!request_local_api_json("detail", json{{"serialNumber", m_serial_number}, {"checkCode", m_check_code}}.dump(), body, msg))
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
const auto parsed = json::parse(body, nullptr, false, true);
|
if (!parse_material_slots(body, slots, supports_material_station)) {
|
||||||
if (parsed.is_discarded()) {
|
|
||||||
msg = _(L("Flashforge returned an invalid JSON response."));
|
msg = _(L("Flashforge returned an invalid JSON response."));
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool Flashforge::parse_material_slots(const std::string& body, std::vector<FlashforgeMaterialSlot>& slots, bool* supports_material_station)
|
||||||
|
{
|
||||||
|
slots.clear();
|
||||||
|
|
||||||
|
const auto parsed = json::parse(body, nullptr, false, true);
|
||||||
|
if (parsed.is_discarded())
|
||||||
|
return false;
|
||||||
|
|
||||||
const auto& detail = parsed.contains("detail") ? parsed["detail"] : parsed;
|
const auto& detail = parsed.contains("detail") ? parsed["detail"] : parsed;
|
||||||
const auto& station = detail.contains("matlStationInfo") ? detail["matlStationInfo"] :
|
const auto& station = detail.contains("matlStationInfo") ? detail["matlStationInfo"] :
|
||||||
detail.contains("MatlStationInfo") ? detail["MatlStationInfo"] : json();
|
detail.contains("MatlStationInfo") ? detail["MatlStationInfo"] : json();
|
||||||
@@ -542,12 +552,21 @@ bool Flashforge::fetch_material_slots(std::vector<FlashforgeMaterialSlot>& slots
|
|||||||
if (supports_material_station != nullptr)
|
if (supports_material_station != nullptr)
|
||||||
*supports_material_station = reports_material_station;
|
*supports_material_station = reports_material_station;
|
||||||
|
|
||||||
|
// Fields are read leniently: firmware may send numbers as strings or flags as numbers.
|
||||||
for (const auto& slot : slot_infos) {
|
for (const auto& slot : slot_infos) {
|
||||||
|
if (!slot.is_object())
|
||||||
|
continue;
|
||||||
FlashforgeMaterialSlot info;
|
FlashforgeMaterialSlot info;
|
||||||
info.slot_id = slot.value("slotId", static_cast<int>(slots.size()) + 1);
|
info.slot_id = static_cast<int>(slots.size()) + 1;
|
||||||
info.has_filament = slot.value("hasFilament", false);
|
if (const auto it = slot.find("slotId"); it != slot.end())
|
||||||
info.material_name = slot.value("materialName", std::string());
|
try_parse_json_int(*it, info.slot_id);
|
||||||
info.material_color = slot.value("materialColor", std::string());
|
int has_filament = 0;
|
||||||
|
if (const auto it = slot.find("hasFilament"); it != slot.end() && try_parse_json_int(*it, has_filament))
|
||||||
|
info.has_filament = has_filament != 0;
|
||||||
|
if (const auto it = slot.find("materialName"); it != slot.end() && it->is_string())
|
||||||
|
info.material_name = it->get<std::string>();
|
||||||
|
if (const auto it = slot.find("materialColor"); it != slot.end() && it->is_string())
|
||||||
|
info.material_color = it->get<std::string>();
|
||||||
slots.emplace_back(std::move(info));
|
slots.emplace_back(std::move(info));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -670,13 +689,4 @@ std::string Flashforge::extract_host_name() const
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
int Flashforge::get_err_code_from_body(const std::string& body) const
|
|
||||||
{
|
|
||||||
pt::ptree root;
|
|
||||||
std::istringstream iss(body); // wrap returned json to istringstream
|
|
||||||
pt::read_json(iss, root);
|
|
||||||
|
|
||||||
return root.get<int>("err", 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace Slic3r
|
} // namespace Slic3r
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ public:
|
|||||||
PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; }
|
PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; }
|
||||||
std::string get_host() const override { return m_host; }
|
std::string get_host() const override { return m_host; }
|
||||||
bool fetch_material_slots(std::vector<FlashforgeMaterialSlot>& slots, bool* supports_material_station, wxString& msg) const;
|
bool fetch_material_slots(std::vector<FlashforgeMaterialSlot>& slots, bool* supports_material_station, wxString& msg) const;
|
||||||
|
// Parses a local API "detail" reply. Returns false when the body is not valid JSON.
|
||||||
|
static bool parse_material_slots(const std::string& body, std::vector<FlashforgeMaterialSlot>& slots, bool* supports_material_station);
|
||||||
static bool discover_printers(std::vector<FlashforgeDiscoveredPrinter>& printers, wxString& msg, int timeout_ms = 10000, int idle_timeout_ms = 1500, int max_retries = 3);
|
static bool discover_printers(std::vector<FlashforgeDiscoveredPrinter>& printers, wxString& msg, int timeout_ms = 10000, int idle_timeout_ms = 1500, int max_retries = 3);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -68,7 +70,6 @@ private:
|
|||||||
bool request_local_api_json(const std::string& path, const std::string& body, std::string& response_body, wxString& error_msg) const;
|
bool request_local_api_json(const std::string& path, const std::string& body, std::string& response_body, wxString& error_msg) const;
|
||||||
std::string make_http_url(const std::string& path) const;
|
std::string make_http_url(const std::string& path) const;
|
||||||
std::string extract_host_name() const;
|
std::string extract_host_name() const;
|
||||||
int get_err_code_from_body(const std::string &body) const;
|
|
||||||
bool connect(wxString& msg) const;
|
bool connect(wxString& msg) const;
|
||||||
bool start_print(wxString& msg, const std::string& filename) const;
|
bool start_print(wxString& msg, const std::string& filename) const;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -141,13 +141,4 @@ bool MKS::start_print(wxString& msg, const std::string& filename) const
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
int MKS::get_err_code_from_body(const std::string& body) const
|
|
||||||
{
|
|
||||||
pt::ptree root;
|
|
||||||
std::istringstream iss(body); // wrap returned json to istringstream
|
|
||||||
pt::read_json(iss, root);
|
|
||||||
|
|
||||||
return root.get<int>("err", 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
} // Slic3r
|
} // Slic3r
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ private:
|
|||||||
|
|
||||||
std::string get_upload_url(const std::string& filename) const;
|
std::string get_upload_url(const std::string& filename) const;
|
||||||
bool start_print(wxString& msg, const std::string& filename) const;
|
bool start_print(wxString& msg, const std::string& filename) const;
|
||||||
int get_err_code_from_body(const std::string& body) const;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -339,8 +339,62 @@ bool PresetUpdater::priv::get_file(const std::string &url, const fs::path &targe
|
|||||||
//BBS: refine preset update logic
|
//BBS: refine preset update logic
|
||||||
bool PresetUpdater::priv::extract_file(const fs::path &source_path, const fs::path &dest_path)
|
bool PresetUpdater::priv::extract_file(const fs::path &source_path, const fs::path &dest_path)
|
||||||
{
|
{
|
||||||
const std::string parent_path = (!dest_path.empty() ? dest_path : source_path.parent_path()).string();
|
bool res = true;
|
||||||
return extract_archive_confined(source_path.string(), parent_path);
|
std::string file_path = source_path.string();
|
||||||
|
std::string parent_path = (!dest_path.empty() ? dest_path : source_path.parent_path()).string();
|
||||||
|
mz_zip_archive archive;
|
||||||
|
mz_zip_zero_struct(&archive);
|
||||||
|
|
||||||
|
if (!open_zip_reader(&archive, file_path))
|
||||||
|
{
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "Unable to open zip reader for "<<file_path;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mz_uint num_entries = mz_zip_reader_get_num_files(&archive);
|
||||||
|
|
||||||
|
mz_zip_archive_file_stat stat;
|
||||||
|
// we first loop the entries to read from the archive the .amf file only, in order to extract the version from it
|
||||||
|
for (mz_uint i = 0; i < num_entries; ++i)
|
||||||
|
{
|
||||||
|
if (mz_zip_reader_file_stat(&archive, i, &stat))
|
||||||
|
{
|
||||||
|
std::string dest_file = parent_path+"/"+stat.m_filename;
|
||||||
|
if (stat.m_is_directory) {
|
||||||
|
fs::path dest_path(dest_file);
|
||||||
|
if (!fs::exists(dest_path))
|
||||||
|
fs::create_directories(dest_path);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else if (stat.m_uncomp_size == 0) {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]Unzip: invalid size for file "<<stat.m_filename;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
res = mz_zip_reader_extract_to_file(&archive, stat.m_file_index, dest_file.c_str(), 0);
|
||||||
|
if (!res) {
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "[Orca Updater]extract file "<<stat.m_filename<<" to dest "<<dest_file<<" failed";
|
||||||
|
close_zip_reader(&archive);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]successfully extract file " << stat.m_file_index << " to "<<dest_file;
|
||||||
|
}
|
||||||
|
catch (const std::exception& e)
|
||||||
|
{
|
||||||
|
// ensure the zip archive is closed and rethrow the exception
|
||||||
|
close_zip_reader(&archive);
|
||||||
|
BOOST_LOG_TRIVIAL(error) << "[Orca Updater]Archive read exception:"<<e.what();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]Unzip: read file stat failed";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close_zip_reader(&archive);
|
||||||
|
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove a leftover partial archive for the vendor about to be synchronized.
|
// Remove a leftover partial archive for the vendor about to be synchronized.
|
||||||
|
|||||||
+101
-10
@@ -3,9 +3,13 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
|
#include <sstream>
|
||||||
#include <boost/optional.hpp>
|
#include <boost/optional.hpp>
|
||||||
#include <boost/log/trivial.hpp>
|
#include <boost/log/trivial.hpp>
|
||||||
#include <boost/filesystem.hpp>
|
#include <boost/filesystem.hpp>
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
#include <boost/property_tree/ptree.hpp>
|
||||||
|
#include <boost/property_tree/json_parser.hpp>
|
||||||
|
|
||||||
#include <wx/string.h>
|
#include <wx/string.h>
|
||||||
#include <wx/app.h>
|
#include <wx/app.h>
|
||||||
@@ -115,10 +119,81 @@ std::string PrintHost::get_print_host_webui(DynamicPrintConfig* config)
|
|||||||
return 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
|
wxString PrintHost::format_error(const std::string &body, const std::string &error, unsigned status) const
|
||||||
{
|
{
|
||||||
if (status != 0) {
|
if (status != 0) {
|
||||||
auto wxbody = wxString::FromUTF8(body.data());
|
const std::string reason = moonraker_error_reason(body);
|
||||||
|
auto wxbody = wxString::FromUTF8(reason.empty() ? body : reason);
|
||||||
return wxString::Format("HTTP %u: %s", status, wxbody);
|
return wxString::Format("HTTP %u: %s", status, wxbody);
|
||||||
} else {
|
} else {
|
||||||
if (error.find("curl:Timeout was reached") != std::string::npos) {
|
if (error.find("curl:Timeout was reached") != std::string::npos) {
|
||||||
@@ -357,12 +432,10 @@ void PrintHostJobQueue::priv::remove_source()
|
|||||||
source_to_remove.clear();
|
source_to_remove.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void PrintHostJobQueue::priv::perform_job(PrintHostJob the_job)
|
bool PrintHostJobQueue::upload_job(PrintHostJob &job, PrintHost::ProgressFn progress_fn, PrintHost::ErrorFn error_fn, PrintHost::InfoFn info_fn)
|
||||||
{
|
{
|
||||||
emit_progress(0); // Indicate the upload is starting
|
|
||||||
|
|
||||||
// Captured before upload_data is moved into upload() below.
|
// Captured before upload_data is moved into upload() below.
|
||||||
const std::string upload_filename = the_job.upload_data.source_path.filename().string();
|
const std::string upload_filename = job.upload_data.source_path.filename().string();
|
||||||
|
|
||||||
{
|
{
|
||||||
LifecycleEventContext ctx;
|
LifecycleEventContext ctx;
|
||||||
@@ -371,19 +444,37 @@ void PrintHostJobQueue::priv::perform_job(PrintHostJob the_job)
|
|||||||
fire_lifecycle_event(LifecycleEvent::UploadStarted, ctx);
|
fire_lifecycle_event(LifecycleEvent::UploadStarted, ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool success = the_job.printhost->upload(std::move(the_job.upload_data),
|
bool success = false;
|
||||||
[this](Http::Progress progress, bool &cancel) { this->progress_fn(std::move(progress), cancel); },
|
std::string error;
|
||||||
[this](wxString error) { this->error_fn(std::move(error)); },
|
// A throwing upload must not stop the worker, or later jobs would stay queued forever.
|
||||||
[this](wxString tag, wxString host) { this->info_fn(std::move(tag), std::move(host)); }
|
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;
|
LifecycleEventContext ctx;
|
||||||
ctx.name = upload_filename;
|
ctx.name = upload_filename;
|
||||||
ctx.code = success ? LifecycleEvtCode::Ok : LifecycleEvtCode::Error;
|
ctx.code = success ? LifecycleEvtCode::Ok : LifecycleEvtCode::Error;
|
||||||
|
ctx.msg = error;
|
||||||
fire_lifecycle_event(LifecycleEvent::UploadFinished, ctx);
|
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) {
|
if (success) {
|
||||||
emit_progress(100);
|
emit_progress(100);
|
||||||
if (the_job.switch_to_device_tab) {
|
if (the_job.switch_to_device_tab) {
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ public:
|
|||||||
|
|
||||||
static PrintHost* get_print_host(DynamicPrintConfig *config);
|
static PrintHost* get_print_host(DynamicPrintConfig *config);
|
||||||
static std::string get_print_host_webui(DynamicPrintConfig *config);
|
static std::string get_print_host_webui(DynamicPrintConfig *config);
|
||||||
|
// Reads the "err" field of a JSON reply, 0 when absent. Returns -1 when the body is not valid JSON.
|
||||||
|
static int get_err_code_from_body(const std::string &body);
|
||||||
|
|
||||||
//Support for cloud webui login
|
//Support for cloud webui login
|
||||||
virtual bool is_cloud() const { return false; }
|
virtual bool is_cloud() const { return false; }
|
||||||
@@ -150,6 +152,10 @@ public:
|
|||||||
void enqueue(PrintHostJob job);
|
void enqueue(PrintHostJob job);
|
||||||
void cancel(size_t id);
|
void cancel(size_t id);
|
||||||
|
|
||||||
|
// Uploads the job, firing UploadStarted and a matching UploadFinished. An exception thrown by
|
||||||
|
// the upload is reported through error_fn and makes the upload fail.
|
||||||
|
static bool upload_job(PrintHostJob &job, PrintHost::ProgressFn progress_fn, PrintHost::ErrorFn error_fn, PrintHost::InfoFn info_fn);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
struct priv;
|
struct priv;
|
||||||
std::shared_ptr<priv> p;
|
std::shared_ptr<priv> p;
|
||||||
|
|||||||
@@ -654,13 +654,4 @@ bool UltiMaker::start_print(wxString &msg, const std::string &filename, Connecti
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
int UltiMaker::get_err_code_from_body(const std::string &body) const
|
|
||||||
{
|
|
||||||
pt::ptree root;
|
|
||||||
std::istringstream iss (body); // wrap returned json to istringstream
|
|
||||||
pt::read_json(iss, root);
|
|
||||||
|
|
||||||
return root.get<int>("err", 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -64,7 +64,6 @@ private:
|
|||||||
void set_auth(Http& http) const;
|
void set_auth(Http& http) const;
|
||||||
void disconnect(ConnectionType connectionType) const;
|
void disconnect(ConnectionType connectionType) const;
|
||||||
bool start_print(wxString &msg, const std::string &filename, ConnectionType connectionType) const;
|
bool start_print(wxString &msg, const std::string &filename, ConnectionType connectionType) const;
|
||||||
int get_err_code_from_body(const std::string &body) const;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
#include "libslic3r/Layer.hpp"
|
#include "libslic3r/Layer.hpp"
|
||||||
#include "libslic3r/Model.hpp"
|
#include "libslic3r/Model.hpp"
|
||||||
#include "libslic3r/GCodeReader.hpp"
|
#include "libslic3r/GCodeReader.hpp"
|
||||||
|
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||||
|
#include "libslic3r/Exception.hpp"
|
||||||
|
#include "libslic3r/LifecycleEvents.hpp"
|
||||||
|
|
||||||
#include "test_helpers.hpp"
|
#include "test_helpers.hpp"
|
||||||
#include "test_utils.hpp"
|
#include "test_utils.hpp"
|
||||||
@@ -22,7 +25,10 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
#include <iterator>
|
#include <iterator>
|
||||||
|
#include <memory>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
using namespace Slic3r;
|
using namespace Slic3r;
|
||||||
using namespace Slic3r::Test;
|
using namespace Slic3r::Test;
|
||||||
@@ -224,8 +230,88 @@ std::string resolved_output_name(Model& model, const std::string& format, const
|
|||||||
return print.output_filename(filename_base);
|
return print.output_filename(filename_base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
struct ScopedLifecycleHook
|
||||||
|
{
|
||||||
|
explicit ScopedLifecycleHook(LifecycleHookFn hook) { set_lifecycle_hook_fn(std::move(hook)); }
|
||||||
|
~ScopedLifecycleHook() { set_lifecycle_hook_fn(nullptr); }
|
||||||
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("Slicing lifecycle events identify the model", "[Print][LifecycleEvents]")
|
||||||
|
{
|
||||||
|
struct ObservedEvent {
|
||||||
|
LifecycleEvent event;
|
||||||
|
std::string id;
|
||||||
|
std::string name;
|
||||||
|
};
|
||||||
|
std::vector<ObservedEvent> events;
|
||||||
|
ScopedLifecycleHook hook([&](LifecycleEvent event, const LifecycleEventContext& ctx) {
|
||||||
|
events.push_back({ event, ctx.id, ctx.name });
|
||||||
|
});
|
||||||
|
|
||||||
|
Print print;
|
||||||
|
Model model;
|
||||||
|
ModelInfo info;
|
||||||
|
info.model_name = "Lifecycle test model";
|
||||||
|
model.model_info = std::make_shared<ModelInfo>(std::move(info));
|
||||||
|
init_print({cube(20)}, print, model);
|
||||||
|
|
||||||
|
print.process();
|
||||||
|
ScopedTemporaryFile temp(".gcode");
|
||||||
|
print.export_gcode(temp.string(), nullptr, nullptr);
|
||||||
|
GCodeProcessorResult result;
|
||||||
|
print.export_gcode_from_previous_file(temp.string(), &result);
|
||||||
|
|
||||||
|
const std::string expected_id = std::to_string(print.model().id().id);
|
||||||
|
const std::vector<LifecycleEvent> expected_events = {
|
||||||
|
LifecycleEvent::SliceStarted,
|
||||||
|
LifecycleEvent::SliceGeometryFinished,
|
||||||
|
LifecycleEvent::GCodeExportStarted,
|
||||||
|
LifecycleEvent::GCodeExportFinished,
|
||||||
|
LifecycleEvent::GCodeExportStarted,
|
||||||
|
LifecycleEvent::GCodeExportFinished,
|
||||||
|
};
|
||||||
|
REQUIRE(events.size() == expected_events.size());
|
||||||
|
for (size_t i = 0; i < expected_events.size(); ++i) {
|
||||||
|
CHECK(events[i].event == expected_events[i]);
|
||||||
|
CHECK(events[i].id == expected_id);
|
||||||
|
CHECK(events[i].name == "Lifecycle test model");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Slicing lifecycle event name is empty without model metadata", "[Print][LifecycleEvents]")
|
||||||
|
{
|
||||||
|
std::string event_id;
|
||||||
|
std::string event_name = "unset";
|
||||||
|
ScopedLifecycleHook hook([&](LifecycleEvent event, const LifecycleEventContext& ctx) {
|
||||||
|
if (event == LifecycleEvent::SliceStarted) {
|
||||||
|
event_id = ctx.id;
|
||||||
|
event_name = ctx.name;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Print print;
|
||||||
|
Model model;
|
||||||
|
init_print({cube(20)}, print, model);
|
||||||
|
print.process();
|
||||||
|
|
||||||
|
CHECK(event_id == std::to_string(print.model().id().id));
|
||||||
|
CHECK(event_name.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Output filenames with numeric statistics fail before slicing finishes", "[Print][Regression]")
|
||||||
|
{
|
||||||
|
DynamicPrintConfig config = DynamicPrintConfig::full_print_config();
|
||||||
|
config.set_key_value("filename_format", new ConfigOptionString("{int(total_weight*10) / 10.0}"));
|
||||||
|
|
||||||
|
Print print;
|
||||||
|
Model model;
|
||||||
|
init_print({cube(20)}, print, model, config);
|
||||||
|
|
||||||
|
CHECK_THROWS_AS(print.output_filename(), PlaceholderParserError);
|
||||||
|
}
|
||||||
|
|
||||||
TEST_CASE("Print: {first_object_name} names the first printable object on the plate", "[Print]")
|
TEST_CASE("Print: {first_object_name} names the first printable object on the plate", "[Print]")
|
||||||
{
|
{
|
||||||
Model model;
|
Model model;
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ add_executable(${_TEST_NAME}_tests
|
|||||||
test_lay_on_face.cpp
|
test_lay_on_face.cpp
|
||||||
test_model.cpp
|
test_model.cpp
|
||||||
test_utils.cpp
|
test_utils.cpp
|
||||||
test_miniz_extension.cpp
|
|
||||||
test_timeutils.cpp
|
test_timeutils.cpp
|
||||||
test_voronoi.cpp
|
test_voronoi.cpp
|
||||||
test_wipe_tower_estimate.cpp
|
test_wipe_tower_estimate.cpp
|
||||||
|
|||||||
@@ -1,194 +0,0 @@
|
|||||||
#include <catch2/catch_all.hpp>
|
|
||||||
|
|
||||||
#include "libslic3r/miniz_extension.hpp"
|
|
||||||
|
|
||||||
#include "test_utils.hpp"
|
|
||||||
|
|
||||||
#include <boost/filesystem.hpp>
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <fstream>
|
|
||||||
#include <iterator>
|
|
||||||
#include <string>
|
|
||||||
#include <utility>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
using namespace Slic3r;
|
|
||||||
namespace fs = boost::filesystem;
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
void write_zip(const fs::path &zip_file, const std::vector<std::pair<std::string, std::string>> &entries)
|
|
||||||
{
|
|
||||||
mz_zip_archive zip;
|
|
||||||
mz_zip_zero_struct(&zip);
|
|
||||||
REQUIRE(open_zip_writer(&zip, zip_file.string()));
|
|
||||||
for (const auto &[name, content] : entries)
|
|
||||||
REQUIRE(mz_zip_writer_add_mem(&zip, name.c_str(), content.data(), content.size(), MZ_DEFAULT_COMPRESSION));
|
|
||||||
REQUIRE(mz_zip_writer_finalize_archive(&zip));
|
|
||||||
REQUIRE(close_zip_writer(&zip));
|
|
||||||
}
|
|
||||||
|
|
||||||
// miniz refuses to write a name starting with '/', so write a placeholder of the same length and patch it in place.
|
|
||||||
void rename_entry(const fs::path &zip_file, const std::string &from, const std::string &to)
|
|
||||||
{
|
|
||||||
REQUIRE(from.size() == to.size());
|
|
||||||
std::string bytes;
|
|
||||||
{
|
|
||||||
std::ifstream in(zip_file.string(), std::ios::binary);
|
|
||||||
bytes.assign(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
|
|
||||||
}
|
|
||||||
size_t count = 0;
|
|
||||||
for (size_t pos = bytes.find(from); pos != std::string::npos; pos = bytes.find(from, pos + to.size()), ++count)
|
|
||||||
bytes.replace(pos, from.size(), to);
|
|
||||||
// Once in the local header and once in the central directory.
|
|
||||||
REQUIRE(count == 2);
|
|
||||||
std::ofstream out(zip_file.string(), std::ios::binary | std::ios::trunc);
|
|
||||||
out << bytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::string> list_dir(const fs::path &dir)
|
|
||||||
{
|
|
||||||
std::vector<std::string> names;
|
|
||||||
for (const fs::directory_entry &entry : fs::directory_iterator(dir))
|
|
||||||
names.push_back(entry.path().filename().string());
|
|
||||||
std::sort(names.begin(), names.end());
|
|
||||||
return names;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string read_file(const fs::path &file)
|
|
||||||
{
|
|
||||||
std::ifstream in(file.string(), std::ios::binary);
|
|
||||||
return std::string(std::istreambuf_iterator<char>(in), std::istreambuf_iterator<char>());
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
TEST_CASE("Confined extraction writes a well-formed archive under the target directory", "[MinizExtension]")
|
|
||||||
{
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
const fs::path zip_file = tmp.path() / "bundle.zip";
|
|
||||||
const fs::path target = tmp.path() / "cache";
|
|
||||||
fs::create_directories(target);
|
|
||||||
write_zip(zip_file, {{"vendor/", ""}, {"vendor/machine/", ""}, {"vendor.json", "{\"a\":1}"}, {"vendor/machine/printer.json", "{\"b\":2}"}});
|
|
||||||
|
|
||||||
REQUIRE(extract_archive_confined(zip_file.string(), target.string()));
|
|
||||||
CHECK(fs::is_directory(target / "vendor"));
|
|
||||||
CHECK(read_file(target / "vendor.json") == "{\"a\":1}");
|
|
||||||
CHECK(read_file(target / "vendor" / "machine" / "printer.json") == "{\"b\":2}");
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("Confined extraction rejects an archive with an entry outside the target directory", "[MinizExtension]")
|
|
||||||
{
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
const fs::path zip_file = tmp.path() / "bundle.zip";
|
|
||||||
const fs::path target = tmp.path() / "cache";
|
|
||||||
fs::create_directories(target);
|
|
||||||
|
|
||||||
const std::string escaping_entry = GENERATE(std::string("../escape.txt"), std::string("..\\escape.txt"),
|
|
||||||
std::string("sub/../../escape.txt"), std::string("C:/escape.txt"),
|
|
||||||
std::string("C:escape.txt"), std::string("\\escape.txt"));
|
|
||||||
// The normal entry comes first so a per-entry check would already have written it.
|
|
||||||
write_zip(zip_file, {{"normal.json", "{}"}, {escaping_entry, "escaped"}});
|
|
||||||
|
|
||||||
CAPTURE(escaping_entry);
|
|
||||||
CHECK_FALSE(extract_archive_confined(zip_file.string(), target.string()));
|
|
||||||
CHECK_FALSE(fs::exists(tmp.path() / "escape.txt"));
|
|
||||||
CHECK(fs::is_empty(target));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("Confined extraction rejects an archive with an absolute entry name", "[MinizExtension]")
|
|
||||||
{
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
const fs::path zip_file = tmp.path() / "bundle.zip";
|
|
||||||
const fs::path target = tmp.path() / "cache";
|
|
||||||
fs::create_directories(target);
|
|
||||||
|
|
||||||
const std::string absolute = (tmp.path() / "escape.txt").generic_string();
|
|
||||||
const std::string placeholder = "#" + absolute.substr(1);
|
|
||||||
write_zip(zip_file, {{"normal.json", "{}"}, {placeholder, "escaped"}});
|
|
||||||
rename_entry(zip_file, placeholder, absolute);
|
|
||||||
|
|
||||||
CHECK_FALSE(extract_archive_confined(zip_file.string(), target.string()));
|
|
||||||
CHECK_FALSE(fs::exists(tmp.path() / "escape.txt"));
|
|
||||||
CHECK(fs::is_empty(target));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("Confined extraction rejects a directory entry outside the target directory", "[MinizExtension]")
|
|
||||||
{
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
const fs::path zip_file = tmp.path() / "bundle.zip";
|
|
||||||
const fs::path target = tmp.path() / "cache";
|
|
||||||
fs::create_directories(target);
|
|
||||||
write_zip(zip_file, {{"vendor/", ""}, {"../outside/", ""}});
|
|
||||||
|
|
||||||
CHECK_FALSE(extract_archive_confined(zip_file.string(), target.string()));
|
|
||||||
CHECK_FALSE(fs::exists(tmp.path() / "outside"));
|
|
||||||
CHECK(fs::is_empty(target));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("Confined extraction validates zero-size entries like any other", "[MinizExtension]")
|
|
||||||
{
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
const fs::path zip_file = tmp.path() / "bundle.zip";
|
|
||||||
const fs::path target = tmp.path() / "cache";
|
|
||||||
fs::create_directories(target);
|
|
||||||
|
|
||||||
SECTION("an empty file inside the target does not fail the archive") {
|
|
||||||
write_zip(zip_file, {{"empty.json", ""}, {"vendor.json", "{}"}});
|
|
||||||
CHECK(extract_archive_confined(zip_file.string(), target.string()));
|
|
||||||
CHECK(read_file(target / "vendor.json") == "{}");
|
|
||||||
}
|
|
||||||
SECTION("an empty file outside the target rejects the archive") {
|
|
||||||
write_zip(zip_file, {{"vendor.json", "{}"}, {"../escape.txt", ""}});
|
|
||||||
CHECK_FALSE(extract_archive_confined(zip_file.string(), target.string()));
|
|
||||||
CHECK_FALSE(fs::exists(tmp.path() / "escape.txt"));
|
|
||||||
CHECK(fs::is_empty(target));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("Confined extraction writes nothing outside the target for Windows-specific name forms", "[MinizExtension]")
|
|
||||||
{
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
const fs::path zip_file = tmp.path() / "bundle.zip";
|
|
||||||
const fs::path target = tmp.path() / "cache";
|
|
||||||
fs::create_directories(target);
|
|
||||||
|
|
||||||
// Windows strips trailing dots and spaces and maps device names; whether these extract depends on the
|
|
||||||
// platform, but none of them may land beside the target.
|
|
||||||
const std::string name = GENERATE(std::string("name."), std::string("name "), std::string("..."), std::string(".. "),
|
|
||||||
std::string(".. /escape.txt"), std::string(".../escape.txt"), std::string("CON"),
|
|
||||||
std::string("sub/NUL.txt"), std::string("C:escape.txt"));
|
|
||||||
write_zip(zip_file, {{name, "payload"}});
|
|
||||||
|
|
||||||
CAPTURE(name);
|
|
||||||
extract_archive_confined(zip_file.string(), target.string());
|
|
||||||
CHECK(list_dir(tmp.path()) == std::vector<std::string>{"bundle.zip", "cache"});
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifndef _WIN32
|
|
||||||
TEST_CASE("Confined extraction replaces a symlink at the destination instead of writing through it", "[MinizExtension]")
|
|
||||||
{
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
const fs::path zip_file = tmp.path() / "bundle.zip";
|
|
||||||
const fs::path target = tmp.path() / "cache";
|
|
||||||
const fs::path outside = tmp.path() / "outside";
|
|
||||||
fs::create_directories(target);
|
|
||||||
fs::create_directories(outside);
|
|
||||||
write_zip(zip_file, {{"vendor.json", "{\"a\":1}"}});
|
|
||||||
|
|
||||||
SECTION("a dangling symlink") {
|
|
||||||
fs::create_symlink(outside / "vendor.json", target / "vendor.json");
|
|
||||||
CHECK(extract_archive_confined(zip_file.string(), target.string()));
|
|
||||||
CHECK_FALSE(fs::exists(outside / "vendor.json"));
|
|
||||||
CHECK_FALSE(fs::is_symlink(fs::symlink_status(target / "vendor.json")));
|
|
||||||
CHECK(read_file(target / "vendor.json") == "{\"a\":1}");
|
|
||||||
}
|
|
||||||
SECTION("a symlink to an existing file") {
|
|
||||||
{ std::ofstream((outside / "vendor.json").string()) << "original"; }
|
|
||||||
fs::create_symlink(outside / "vendor.json", target / "vendor.json");
|
|
||||||
extract_archive_confined(zip_file.string(), target.string());
|
|
||||||
CHECK(read_file(outside / "vendor.json") == "original");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
@@ -152,82 +152,3 @@ TEST_CASE("resolve_cli_input_path leaves inputs that must not be completed uncha
|
|||||||
REQUIRE(resolve_cli_input_path("").empty());
|
REQUIRE(resolve_cli_input_path("").empty());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST_CASE("is_path_within_root accepts a root given with a trailing separator", "[utils]") {
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
const std::string root = tmp.path().string();
|
|
||||||
const std::string with_separator = GENERATE_COPY(root + "/", root + std::string(1, static_cast<char>(boost::filesystem::path::preferred_separator)));
|
|
||||||
|
|
||||||
CAPTURE(with_separator);
|
|
||||||
CHECK(is_path_within_root("vendor.json", with_separator));
|
|
||||||
CHECK(is_path_within_root("vendor/machine/printer.json", with_separator));
|
|
||||||
CHECK_FALSE(is_path_within_root("../vendor.json", with_separator));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("is_path_within_root treats Windows-specific name forms the same on every platform", "[utils]") {
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
|
|
||||||
SECTION("names ending in dots or spaces stay inside the root") {
|
|
||||||
const std::string name = GENERATE(std::string("name."), std::string("name "), std::string("dir./file.json"), std::string("dir /file.json"));
|
|
||||||
CAPTURE(name);
|
|
||||||
CHECK(is_path_within_root(name, tmp.path()));
|
|
||||||
}
|
|
||||||
SECTION("drive-relative names are rejected") {
|
|
||||||
const std::string name = GENERATE(std::string("C:x"), std::string("c:x/y.json"), std::string("C:"));
|
|
||||||
CAPTURE(name);
|
|
||||||
CHECK_FALSE(is_path_within_root(name, tmp.path()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("is_path_within_root rejects a name with an embedded NUL", "[utils]") {
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
// The filesystem calls stop at the NUL, so they would act on a different path than the one checked.
|
|
||||||
const std::string name = GENERATE(std::string("..\0", 3), std::string("..\0x/file.json", 14), std::string("sub/..\0x", 8),
|
|
||||||
std::string("file.json\0", 10), std::string("\0file.json", 10));
|
|
||||||
CAPTURE(name.size());
|
|
||||||
CHECK_FALSE(is_path_within_root(name, tmp.path()));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("is_symlink_target_within_root accepts relative targets that stay inside the root", "[utils]") {
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
const auto [link, target] = GENERATE(std::make_pair(std::string("Versions/Current"), std::string("A")),
|
|
||||||
std::make_pair(std::string("Foo.framework/Foo"), std::string("Versions/Current/Foo")),
|
|
||||||
std::make_pair(std::string("libfoo.so"), std::string("libfoo.so.1")),
|
|
||||||
std::make_pair(std::string("a/b/link"), std::string("c/d")));
|
|
||||||
CAPTURE(link, target);
|
|
||||||
CHECK(is_symlink_target_within_root(link, target, tmp.path()));
|
|
||||||
}
|
|
||||||
|
|
||||||
TEST_CASE("is_symlink_target_within_root rejects absolute targets and targets that climb out", "[utils]") {
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
const std::string outside = (tmp.path().parent_path() / "outside").generic_string();
|
|
||||||
const auto [link, target] = GENERATE_COPY(std::make_pair(std::string("sub/link"), outside),
|
|
||||||
std::make_pair(std::string("sub/link"), std::string("/etc/passwd")),
|
|
||||||
std::make_pair(std::string("sub/link"), std::string("\\outside")),
|
|
||||||
std::make_pair(std::string("sub/link"), std::string("C:/outside")),
|
|
||||||
std::make_pair(std::string("sub/link"), std::string("C:outside")),
|
|
||||||
std::make_pair(std::string("sub/link"), std::string("")),
|
|
||||||
std::make_pair(std::string("link"), std::string("..")),
|
|
||||||
std::make_pair(std::string("link"), std::string("../outside")),
|
|
||||||
std::make_pair(std::string("sub/link"), std::string("../../outside")),
|
|
||||||
std::make_pair(std::string("sub/link"), std::string("x/../../../outside")),
|
|
||||||
std::make_pair(std::string("sub/link"), std::string("..\\..\\outside")),
|
|
||||||
// symlink() stops at the NUL, so this target would be created as "..".
|
|
||||||
std::make_pair(std::string("link"), std::string("..\0", 3)));
|
|
||||||
CAPTURE(link, target);
|
|
||||||
CHECK_FALSE(is_symlink_target_within_root(link, target, tmp.path()));
|
|
||||||
}
|
|
||||||
|
|
||||||
#ifndef _WIN32
|
|
||||||
TEST_CASE("is_symlink_target_within_root rejects a target that passes through a symlink leading out", "[utils]") {
|
|
||||||
ScopedTemporaryDir tmp;
|
|
||||||
const boost::filesystem::path root = tmp.path() / "root";
|
|
||||||
const boost::filesystem::path outside = tmp.path() / "outside";
|
|
||||||
boost::filesystem::create_directories(root);
|
|
||||||
boost::filesystem::create_directories(outside);
|
|
||||||
boost::filesystem::create_symlink(outside, root / "out");
|
|
||||||
|
|
||||||
CHECK_FALSE(is_symlink_target_within_root("link", "out/lib.so", root));
|
|
||||||
CHECK(is_symlink_target_within_root("link", "in/lib.so", root));
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|||||||
@@ -18,12 +18,14 @@ add_executable(${_TEST_NAME}_tests
|
|||||||
test_plugin_install.cpp
|
test_plugin_install.cpp
|
||||||
test_plugin_lifecycle.cpp
|
test_plugin_lifecycle.cpp
|
||||||
test_plugin_printer_agent.cpp
|
test_plugin_printer_agent.cpp
|
||||||
|
test_printhost.cpp
|
||||||
test_slicing_pipeline_bindings.cpp
|
test_slicing_pipeline_bindings.cpp
|
||||||
test_slicing_pipeline_config.cpp
|
test_slicing_pipeline_config.cpp
|
||||||
test_plugin_sort.cpp
|
test_plugin_sort.cpp
|
||||||
test_plugin_cloud_metadata.cpp
|
test_plugin_cloud_metadata.cpp
|
||||||
test_plugin_audit.cpp
|
test_plugin_audit.cpp
|
||||||
test_shortcuts.cpp
|
test_shortcuts.cpp
|
||||||
|
test_user_manager.cpp
|
||||||
../fff_print/test_helpers.cpp
|
../fff_print/test_helpers.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,374 @@
|
|||||||
|
#include <catch2/catch_all.hpp>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <nlohmann/json.hpp>
|
||||||
|
|
||||||
|
#include "libslic3r/LifecycleEvents.hpp"
|
||||||
|
#include "slic3r/Utils/PrintHost.hpp"
|
||||||
|
#include "slic3r/Utils/Flashforge.hpp"
|
||||||
|
|
||||||
|
using namespace Slic3r;
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class TestPrintHost : public PrintHost
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using PrintHost::format_error;
|
||||||
|
|
||||||
|
const char* get_name() const override { return "Test"; }
|
||||||
|
bool test(wxString&) const override { return true; }
|
||||||
|
wxString get_test_ok_msg() const override { return {}; }
|
||||||
|
wxString get_test_failed_msg(wxString&) const override { return {}; }
|
||||||
|
bool upload(PrintHostUpload, ProgressFn, ErrorFn, InfoFn) const override { return true; }
|
||||||
|
bool has_auto_discovery() const override { return false; }
|
||||||
|
bool can_test() const override { return false; }
|
||||||
|
PrintHostPostUploadActions get_post_upload_actions() const override { return {}; }
|
||||||
|
std::string get_host() const override { return {}; }
|
||||||
|
};
|
||||||
|
|
||||||
|
class ThrowingPrintHost : public TestPrintHost
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
bool upload(PrintHostUpload, ProgressFn, ErrorFn, InfoFn) const override { throw std::runtime_error("reply could not be read"); }
|
||||||
|
};
|
||||||
|
|
||||||
|
struct UploadEvents
|
||||||
|
{
|
||||||
|
std::vector<LifecycleEvent> events;
|
||||||
|
std::vector<LifecycleEvtCode> codes;
|
||||||
|
std::vector<std::string> errors;
|
||||||
|
bool uploaded{false};
|
||||||
|
|
||||||
|
explicit UploadEvents(std::unique_ptr<PrintHost> host)
|
||||||
|
{
|
||||||
|
set_lifecycle_hook_fn([this](LifecycleEvent event, const LifecycleEventContext& ctx) {
|
||||||
|
events.push_back(event);
|
||||||
|
codes.push_back(ctx.code);
|
||||||
|
});
|
||||||
|
|
||||||
|
PrintHostJob job;
|
||||||
|
job.printhost = std::move(host);
|
||||||
|
job.upload_data.source_path = "plate.gcode";
|
||||||
|
try {
|
||||||
|
uploaded = PrintHostJobQueue::upload_job(job, [](Http::Progress, bool&) {},
|
||||||
|
[this](wxString error) { errors.push_back(error.ToStdString()); },
|
||||||
|
[](wxString, wxString) {});
|
||||||
|
} catch (...) {
|
||||||
|
set_lifecycle_hook_fn(nullptr);
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
set_lifecycle_hook_fn(nullptr);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string format_error(const std::string& body, const std::string& error, unsigned status)
|
||||||
|
{
|
||||||
|
return TestPrintHost().format_error(body, error, status).ToStdString();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string envelope(int code, const std::string& message, const std::string& traceback)
|
||||||
|
{
|
||||||
|
return nlohmann::json{{"error", {{"code", code}, {"message", message}, {"traceback", traceback}}}}.dump();
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string moonraker_error(int code, const std::string& message, const std::string& detail = {})
|
||||||
|
{
|
||||||
|
std::string line = "tornado.web.HTTPError: HTTP " + std::to_string(code) + ": " + message;
|
||||||
|
if (!detail.empty())
|
||||||
|
line += " (" + detail + ")";
|
||||||
|
return envelope(code, message, "Traceback (most recent call last):\n ...\n" + line + "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
// A real Moonraker body for uploading a file that is being printed.
|
||||||
|
constexpr const char* k_busy_file_403 =
|
||||||
|
R"JSON({"error": {"code": 403, "message": "Forbidden", "traceback": "Traceback (most recent call last):\n\n File \"/home/lava/moonraker/moonraker/components/file_manager/file_manager.py\", line 1017, in _finish_gcode_upload\n can_start = self._handle_operation_check(check_path)\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\nmoonraker.utils.exceptions.ServerError: File currently in use\n\nDuring handling of the above exception, another exception occurred:\n\nTraceback (most recent call last):\n\n File \"/home/lava/moonraker/moonraker/components/application.py\", line 1069, in post\n raise tornado.web.HTTPError(\ntornado.web.HTTPError: HTTP 403: Forbidden (File is loaded, upload not permitted)\n"}})JSON";
|
||||||
|
|
||||||
|
// Replies a print host can send instead of JSON: a proxy or login page, nothing, a cut-off body.
|
||||||
|
const std::vector<std::string> non_json_replies = {
|
||||||
|
"<html><body>proxy login required</body></html>",
|
||||||
|
"",
|
||||||
|
"{\"err\":",
|
||||||
|
"{\"detail\":{\"matlStationInfo\":{\"slotInfos\":[{\"slotId\":1,",
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
TEST_CASE("A Klipper upload error shows its reason instead of a Python traceback", "[PrintHost][Regression]")
|
||||||
|
{
|
||||||
|
const std::string msg = format_error(k_busy_file_403, "", 403);
|
||||||
|
INFO("actual: " << msg);
|
||||||
|
|
||||||
|
CHECK(msg == "HTTP 403: Forbidden (File is loaded, upload not permitted)");
|
||||||
|
CHECK_THAT(msg, !Catch::Matchers::ContainsSubstring("Traceback"));
|
||||||
|
CHECK_THAT(msg, !Catch::Matchers::ContainsSubstring("file_manager.py"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("The specific cause is recovered from a file endpoint's traceback", "[PrintHost]")
|
||||||
|
{
|
||||||
|
SECTION("a plain detail")
|
||||||
|
{
|
||||||
|
const std::string body = moonraker_error(403, "Forbidden", "File is loaded, upload not permitted");
|
||||||
|
CHECK(format_error(body, "", 403) == "HTTP 403: Forbidden (File is loaded, upload not permitted)");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a detail whose own parentheses nest (a filename)")
|
||||||
|
{
|
||||||
|
const std::string detail = "Directory does not exist (/home/pi/gcodes/plate (1).gcode)";
|
||||||
|
const std::string body = moonraker_error(400, "Bad Request", detail);
|
||||||
|
CHECK(format_error(body, "", 400) == "HTTP 400: Bad Request (" + detail + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a detail that contains the reason phrase")
|
||||||
|
{
|
||||||
|
const std::string body = moonraker_error(403, "Forbidden", "Forbidden zone: access denied");
|
||||||
|
CHECK(format_error(body, "", 403) == "HTTP 403: Forbidden (Forbidden zone: access denied)");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a detail that spans lines")
|
||||||
|
{
|
||||||
|
const std::string detail = "Move out of range\nX=250.000 Y=10.000";
|
||||||
|
const std::string body = moonraker_error(400, "Bad Request", detail);
|
||||||
|
CHECK(format_error(body, "", 400) == "HTTP 400: Bad Request (" + detail + ")");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a detail that only repeats the reason phrase is dropped")
|
||||||
|
{
|
||||||
|
const std::string body = moonraker_error(401, "Unauthorized", "Unauthorized");
|
||||||
|
CHECK(format_error(body, "", 401) == "HTTP 401: Unauthorized");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("An unhandled exception shows its type and message", "[PrintHost]")
|
||||||
|
{
|
||||||
|
const std::string frame = "Traceback (most recent call last):\n"
|
||||||
|
" File \"/home/pi/moonraker/moonraker/components/file_manager/file_manager.py\", line 1, in write\n"
|
||||||
|
" self._write(data)\n";
|
||||||
|
|
||||||
|
SECTION("a one-line message")
|
||||||
|
{
|
||||||
|
const std::string body = envelope(500, "Internal Server Error", frame + "OSError: [Errno 28] No space left on device\n");
|
||||||
|
CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (OSError: [Errno 28] No space left on device)");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a message that spans lines")
|
||||||
|
{
|
||||||
|
const std::string body = envelope(500, "Internal Server Error", frame + "ServerError: Klippy request failed\n see klippy.log\n");
|
||||||
|
CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (ServerError: Klippy request failed\n see klippy.log)");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("raised while handling an HTTPError with the same code")
|
||||||
|
{
|
||||||
|
const std::string traceback = frame + "tornado.web.HTTPError: HTTP 500: Internal Server Error (Database locked)\n\n"
|
||||||
|
"During handling of the above exception, another exception occurred:\n\n" +
|
||||||
|
frame + "OSError: [Errno 5] Input/output error\n";
|
||||||
|
const std::string body = envelope(500, "Internal Server Error", traceback);
|
||||||
|
CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (OSError: [Errno 5] Input/output error)");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a traceback with no header")
|
||||||
|
{
|
||||||
|
const std::string body = envelope(500, "Internal Server Error", "OSError: [Errno 5] Input/output error");
|
||||||
|
CHECK(format_error(body, "", 500) == "HTTP 500: Internal Server Error (OSError: [Errno 5] Input/output error)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A reason already complete in message is shown unchanged", "[PrintHost]")
|
||||||
|
{
|
||||||
|
SECTION("message is the whole reason, no trailing detail")
|
||||||
|
{
|
||||||
|
const std::string body = moonraker_error(503, "Klippy is not ready");
|
||||||
|
CHECK(format_error(body, "", 503) == "HTTP 503: Klippy is not ready");
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a message that itself contains parentheses is not duplicated")
|
||||||
|
{
|
||||||
|
const std::string reason = "Requested blocks (0-5) are unavailable";
|
||||||
|
const std::string body = moonraker_error(400, reason);
|
||||||
|
CHECK(format_error(body, "", 400) == "HTTP 400: " + reason);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A Moonraker error with no usable detail shows just the reason phrase", "[PrintHost]")
|
||||||
|
{
|
||||||
|
struct Case
|
||||||
|
{
|
||||||
|
const char* name;
|
||||||
|
const char* body;
|
||||||
|
unsigned status;
|
||||||
|
const char* expected;
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto c = GENERATE(
|
||||||
|
Case{"an empty traceback", R"JSON({"error": {"code": 500, "message": "Internal Server Error", "traceback": ""}})JSON", 500,
|
||||||
|
"HTTP 500: Internal Server Error"},
|
||||||
|
Case{"a traceback of only whitespace", R"JSON({"error": {"code": 500, "message": "Internal Server Error", "traceback": "\n \n"}})JSON",
|
||||||
|
500, "HTTP 500: Internal Server Error"});
|
||||||
|
|
||||||
|
DYNAMIC_SECTION(c.name) { CHECK(format_error(c.body, "", c.status) == c.expected); }
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A percent sign in the reason is not a format specifier", "[PrintHost]")
|
||||||
|
{
|
||||||
|
const std::string body = moonraker_error(507, "Insufficient Storage", "disk 100% full");
|
||||||
|
CHECK(format_error(body, "", 507) == "HTTP 507: Insufficient Storage (disk 100% full)");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Error bodies that are not a Moonraker envelope are left unchanged", "[PrintHost]")
|
||||||
|
{
|
||||||
|
SECTION("OctoPrint's string-valued error member")
|
||||||
|
{
|
||||||
|
const std::string body = R"JSON({"error": "File not found"})JSON";
|
||||||
|
CHECK(format_error(body, "", 404) == "HTTP 404: " + body);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("PrusaLink's top-level message, not under error")
|
||||||
|
{
|
||||||
|
const std::string body = R"JSON({"title": "Conflict", "message": "Printer is printing"})JSON";
|
||||||
|
CHECK(format_error(body, "", 409) == "HTTP 409: " + body);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a body that is not JSON")
|
||||||
|
{
|
||||||
|
const std::string html = "<html><head><title>502 Bad Gateway</title></head></html>";
|
||||||
|
CHECK(format_error(html, "", 502) == "HTTP 502: " + html);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("an error object with no traceback")
|
||||||
|
{
|
||||||
|
const std::string body = R"JSON({"error": {"code": 500, "message": "Internal Server Error"}})JSON";
|
||||||
|
CHECK(format_error(body, "", 500) == "HTTP 500: " + body);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("an error object whose traceback is null")
|
||||||
|
{
|
||||||
|
const std::string body = R"JSON({"error": {"code": 500, "message": "Internal Server Error", "traceback": null}})JSON";
|
||||||
|
CHECK(format_error(body, "", 500) == "HTTP 500: " + body);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("an envelope whose reason phrase is empty")
|
||||||
|
{
|
||||||
|
const std::string body = envelope(403, "", "Traceback (most recent call last):\nOSError: denied\n");
|
||||||
|
CHECK(format_error(body, "", 403) == "HTTP 403: " + body);
|
||||||
|
}
|
||||||
|
|
||||||
|
SECTION("a transport error with no HTTP status")
|
||||||
|
{
|
||||||
|
CHECK(format_error("", "curl:Could not connect", 0) == "curl:Could not connect");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Print host error code is read from a JSON reply", "[PrintHost]")
|
||||||
|
{
|
||||||
|
CHECK(PrintHost::get_err_code_from_body(R"({"err":0})") == 0);
|
||||||
|
CHECK(PrintHost::get_err_code_from_body(R"({"err":2})") == 2);
|
||||||
|
CHECK(PrintHost::get_err_code_from_body(R"({"status":"ok"})") == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Print host error code reports a reply that is not JSON as an error", "[PrintHost]")
|
||||||
|
{
|
||||||
|
const std::string body = GENERATE(from_range(non_json_replies));
|
||||||
|
int err = 0;
|
||||||
|
REQUIRE_NOTHROW(err = PrintHost::get_err_code_from_body(body));
|
||||||
|
CHECK(err != 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Print host error code tolerates a wrongly typed err field", "[PrintHost]")
|
||||||
|
{
|
||||||
|
const std::string body = GENERATE(as<std::string>{}, R"({"err":"busy"})", R"({"err":{"code":1}})", R"([1,2])");
|
||||||
|
CHECK_NOTHROW(PrintHost::get_err_code_from_body(body));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Flashforge material slots are read from a well-formed reply", "[PrintHost][Flashforge]")
|
||||||
|
{
|
||||||
|
const std::string body = R"({"code":0,"detail":{"hasMatlStation":true,"matlStationInfo":{"slotCnt":2,"slotInfos":[
|
||||||
|
{"slotId":1,"hasFilament":true,"materialName":"PLA","materialColor":"#FFFFFF"},
|
||||||
|
{"slotId":2,"hasFilament":false,"materialName":"","materialColor":""}]}}})";
|
||||||
|
|
||||||
|
std::vector<FlashforgeMaterialSlot> slots;
|
||||||
|
bool supports_station = false;
|
||||||
|
REQUIRE(Flashforge::parse_material_slots(body, slots, &supports_station));
|
||||||
|
CHECK(supports_station);
|
||||||
|
REQUIRE(slots.size() == 2);
|
||||||
|
CHECK(slots[0].slot_id == 1);
|
||||||
|
CHECK(slots[0].has_filament);
|
||||||
|
CHECK(slots[0].material_name == "PLA");
|
||||||
|
CHECK(slots[0].material_color == "#FFFFFF");
|
||||||
|
CHECK(slots[1].slot_id == 2);
|
||||||
|
CHECK_FALSE(slots[1].has_filament);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Flashforge material slots accept numbers as strings and flags as numbers", "[PrintHost][Flashforge]")
|
||||||
|
{
|
||||||
|
const std::string body = R"({"detail":{"matlStationInfo":{"slotInfos":[
|
||||||
|
{"slotId":"3","hasFilament":1,"materialName":null,"materialColor":7}]}}})";
|
||||||
|
|
||||||
|
std::vector<FlashforgeMaterialSlot> slots;
|
||||||
|
REQUIRE_NOTHROW(Flashforge::parse_material_slots(body, slots, nullptr));
|
||||||
|
REQUIRE(slots.size() == 1);
|
||||||
|
CHECK(slots[0].slot_id == 3);
|
||||||
|
CHECK(slots[0].has_filament);
|
||||||
|
CHECK(slots[0].material_name.empty());
|
||||||
|
CHECK(slots[0].material_color.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Flashforge material slots skip entries that are not objects", "[PrintHost][Flashforge]")
|
||||||
|
{
|
||||||
|
const std::string body = R"({"detail":{"matlStationInfo":{"slotInfos":[5,"slot",null,[],
|
||||||
|
{"slotId":4,"hasFilament":true,"materialName":"PETG"}]}}})";
|
||||||
|
|
||||||
|
std::vector<FlashforgeMaterialSlot> slots;
|
||||||
|
REQUIRE_NOTHROW(Flashforge::parse_material_slots(body, slots, nullptr));
|
||||||
|
REQUIRE(slots.size() == 1);
|
||||||
|
CHECK(slots[0].slot_id == 4);
|
||||||
|
CHECK(slots[0].material_name == "PETG");
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Flashforge material slots tolerate slot info that is not a list", "[PrintHost][Flashforge]")
|
||||||
|
{
|
||||||
|
const std::string body = GENERATE(as<std::string>{},
|
||||||
|
R"({"detail":{"matlStationInfo":{"slotInfos":5}}})",
|
||||||
|
R"({"detail":{"matlStationInfo":{"slotInfos":"none"}}})",
|
||||||
|
R"({"detail":{"matlStationInfo":7}})",
|
||||||
|
R"({"detail":"offline"})");
|
||||||
|
|
||||||
|
std::vector<FlashforgeMaterialSlot> slots;
|
||||||
|
bool ok = false;
|
||||||
|
REQUIRE_NOTHROW(ok = Flashforge::parse_material_slots(body, slots, nullptr));
|
||||||
|
CHECK(ok);
|
||||||
|
CHECK(slots.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("Flashforge material slots reject a reply that is not JSON", "[PrintHost][Flashforge]")
|
||||||
|
{
|
||||||
|
const std::string body = GENERATE(from_range(non_json_replies));
|
||||||
|
std::vector<FlashforgeMaterialSlot> slots;
|
||||||
|
bool ok = true;
|
||||||
|
REQUIRE_NOTHROW(ok = Flashforge::parse_material_slots(body, slots, nullptr));
|
||||||
|
CHECK_FALSE(ok);
|
||||||
|
CHECK(slots.empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("An upload that throws still finishes with an error", "[PrintHost][LifecycleEvents]")
|
||||||
|
{
|
||||||
|
UploadEvents run(std::make_unique<ThrowingPrintHost>());
|
||||||
|
|
||||||
|
CHECK_FALSE(run.uploaded);
|
||||||
|
CHECK(run.errors == std::vector<std::string>{"reply could not be read"});
|
||||||
|
CHECK(run.events == std::vector<LifecycleEvent>{LifecycleEvent::UploadStarted, LifecycleEvent::UploadFinished});
|
||||||
|
CHECK(run.codes == std::vector<LifecycleEvtCode>{LifecycleEvtCode::Ok, LifecycleEvtCode::Error});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("A successful upload finishes without an error", "[PrintHost][LifecycleEvents]")
|
||||||
|
{
|
||||||
|
UploadEvents run(std::make_unique<TestPrintHost>());
|
||||||
|
|
||||||
|
CHECK(run.uploaded);
|
||||||
|
CHECK(run.errors.empty());
|
||||||
|
CHECK(run.events == std::vector<LifecycleEvent>{LifecycleEvent::UploadStarted, LifecycleEvent::UploadFinished});
|
||||||
|
CHECK(run.codes == std::vector<LifecycleEvtCode>{LifecycleEvtCode::Ok, LifecycleEvtCode::Ok});
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#include <catch2/catch_all.hpp>
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include "slic3r/GUI/UserManager.hpp"
|
||||||
|
|
||||||
|
using namespace Slic3r;
|
||||||
|
|
||||||
|
TEST_CASE("User message that is not JSON is rejected without throwing", "[UserManager]")
|
||||||
|
{
|
||||||
|
const std::string payload = GENERATE(as<std::string>{}, "not json", "", "<html></html>", "{\"bind\":");
|
||||||
|
UserManager manager;
|
||||||
|
int result = 0;
|
||||||
|
REQUIRE_NOTHROW(result = manager.parse_json(payload));
|
||||||
|
CHECK(result == -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_CASE("User message without a successful bind is ignored", "[UserManager]")
|
||||||
|
{
|
||||||
|
const std::string payload = GENERATE(as<std::string>{}, "{}", R"({"bind":{"command":"unbind"}})", R"({"bind":"bind"})", "[1]");
|
||||||
|
UserManager manager;
|
||||||
|
CHECK(manager.parse_json(payload) == -1);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user