mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-09-28 19:31:22 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd593317fd | ||
|
|
ec0d8c225f | ||
|
|
3040ebaac1 | ||
|
|
5298e49dd2 | ||
|
|
d347a80ef8 | ||
|
|
e340a13c18 | ||
|
|
9e16cdb23b | ||
|
|
58842bab05 | ||
|
|
04204ec0d3 | ||
|
|
9ed459e132 |
+30
-129
@@ -214,9 +214,9 @@ function ShowModelInfo( pModel )
|
||||
|
||||
SendWXDebugInfo("Model Name: "+sModelName);
|
||||
|
||||
$('#ModelName').text(sModelName);
|
||||
$('#ModelName').html(sModelName);
|
||||
$('#ModelName').attr('title',sModelName);
|
||||
$('#ModelAuthorName').text(sModelAuthor);
|
||||
$('#ModelAuthorName').html(sModelAuthor);
|
||||
|
||||
switch(UploadType)
|
||||
{
|
||||
@@ -268,7 +268,7 @@ function ShowModelInfo( pModel )
|
||||
break;
|
||||
}
|
||||
|
||||
$('#Model_Desc').empty().append( SanitizeDescHtml( html_decode(sModelDesc) ) );
|
||||
$('#Model_Desc').html( html_decode(sModelDesc) );
|
||||
|
||||
let ModelPreviewList=pModel.preview_img;
|
||||
let TotalPreview=ModelPreviewList.length;
|
||||
@@ -281,15 +281,16 @@ function ShowModelInfo( pModel )
|
||||
|
||||
if(TotalPreview>0)
|
||||
{
|
||||
$('#ModelPreviewList').empty();
|
||||
let htmlPreview='';
|
||||
for(let pn=0;pn<TotalPreview;pn++)
|
||||
{
|
||||
//let FTmpPath=decodeURIComponent(ModelPreviewList[pn]);
|
||||
let FTmpPath=ModelPreviewList[pn]['filepath'];
|
||||
|
||||
$('#ModelPreviewList').append( $('<div class="swiper-slide"></div>').append( $('<img class="Model_PrevImg" />').attr('src',FTmpPath) ) );
|
||||
htmlPreview+='<div class="swiper-slide"><img class="Model_PrevImg" src="'+FTmpPath+'" /></div>';
|
||||
}
|
||||
|
||||
$('#ModelPreviewList').html(htmlPreview);
|
||||
$('#Model_Preview_Image').viewer({
|
||||
title: false,
|
||||
fullsreen: false,
|
||||
@@ -409,8 +410,7 @@ function ConstructFileHtml( ID, pItem )
|
||||
{
|
||||
let fTotal=pItem.length;
|
||||
|
||||
let pBoard=$('#'+ID+' .FileListBoard');
|
||||
pBoard.empty();
|
||||
let strHtml='';
|
||||
for( let f=0;f<fTotal;f++ )
|
||||
{
|
||||
let pOne=pItem[f];
|
||||
@@ -443,139 +443,39 @@ function ConstructFileHtml( ID, pItem )
|
||||
ImgPath='img/default.png';
|
||||
}
|
||||
|
||||
//Add html. File names come from the 3MF, so build the nodes rather than concatenating markup.
|
||||
let pIconImg=$('<img />').attr('src',ImgPath);
|
||||
let pMenu=$('<div class="FileMenu"><img src="img/s.svg" /></div>');
|
||||
//Add html
|
||||
if( strClass!='ImageIcon' )
|
||||
{
|
||||
pMenu.on('click', function(){ OnClickOpenFile(tPath); });
|
||||
strHtml+='<div class="FileItem">'+
|
||||
' <div class="'+strClass+'"><img src="'+ImgPath+'" /></div>'+
|
||||
' <div class="FileText">'+
|
||||
' <div class="FileName">'+tName+'</div>'+
|
||||
' </div>'+
|
||||
' <div class="FileMenu" onClick="OnClickOpenFile(\''+tPath+'\')"><img src="img/s.svg" /></div>'+
|
||||
'</div>';
|
||||
}
|
||||
else
|
||||
{
|
||||
ImgID++;
|
||||
let TmpImgID="AF"+ImgID;
|
||||
|
||||
pIconImg.attr('id',TmpImgID);
|
||||
pMenu.on('click', function(){ OnClickOpenImage(TmpImgID); });
|
||||
strHtml+='<div class="FileItem">'+
|
||||
' <div class="'+strClass+'"><img id="'+TmpImgID+'" src="'+ImgPath+'" /></div>'+
|
||||
' <div class="FileText">'+
|
||||
' <div class="FileName">'+tName+'</div>'+
|
||||
' </div>'+
|
||||
' <div class="FileMenu" onClick="OnClickOpenImage(\''+TmpImgID+'\')"><img src="img/s.svg" /></div>'+
|
||||
'</div>';
|
||||
}
|
||||
|
||||
let pFileItem=$('<div class="FileItem"></div>');
|
||||
pFileItem.append( $('<div></div>').addClass(strClass).append(pIconImg) );
|
||||
pFileItem.append( $('<div class="FileText"></div>').append( $('<div class="FileName"></div>').text(tName).attr('title',tName) ) );
|
||||
pFileItem.append( pMenu );
|
||||
pBoard.append( pFileItem );
|
||||
}
|
||||
|
||||
$('#'+ID+' .FileListBoard').html(strHtml);
|
||||
|
||||
if( fTotal>0 )
|
||||
$('#'+ID).show();
|
||||
}
|
||||
|
||||
|
||||
// Descriptions are untrusted 3MF metadata that may carry rich-text HTML (e.g. from MakerWorld).
|
||||
// Rebuild them from an inert parse, keeping only plain formatting tags and http(s) links and images.
|
||||
var DescAllowedTags=['P','BR','B','STRONG','I','EM','U','S','STRIKE','DEL','INS','SUB','SUP','SMALL','MARK',
|
||||
'Q','ABBR','KBD','WBR','H1','H2','H3','H4','H5','H6','UL','OL','LI','DL','DT','DD','BLOCKQUOTE','PRE','CODE',
|
||||
'HR','SPAN','DIV','FIGURE','FIGCAPTION','TABLE','CAPTION','THEAD','TBODY','TFOOT','TR','TH','TD','A','IMG'];
|
||||
// Plain attributes kept per tag; the numeric ones must be plain non-negative integers.
|
||||
var DescAllowedAttrs={'IMG':['alt','title','width','height'],'TD':['colspan','rowspan'],'TH':['colspan','rowspan'],'OL':['start']};
|
||||
var DescNumericAttrs=['width','height','colspan','rowspan','start'];
|
||||
// Dropped together with their content; any other unknown tag is unwrapped to its children.
|
||||
var DescDroppedTags=['SCRIPT','STYLE','TEMPLATE','NOSCRIPT','TEXTAREA','TITLE','IFRAME','FRAME','OBJECT','EMBED','SVG','MATH'];
|
||||
|
||||
function IsHttpUrl( strUrl )
|
||||
{
|
||||
// The scheme must be written as is, so nothing the URL parser would strip can precede or split it.
|
||||
if( typeof strUrl!='string' || !/^https?:/i.test(strUrl) )
|
||||
return false;
|
||||
try
|
||||
{
|
||||
let sProtocol=new URL(strUrl).protocol;
|
||||
return sProtocol=='http:' || sProtocol=='https:';
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Images load as soon as the page opens, so only https sources are kept: no plain-http requests to the local network.
|
||||
function IsHttpsUrl( strUrl )
|
||||
{
|
||||
return IsHttpUrl(strUrl) && new URL(strUrl).protocol=='https:';
|
||||
}
|
||||
|
||||
// Embedded YouTube players become a plain link to the video.
|
||||
function GetYouTubeEmbedUrl( pNode )
|
||||
{
|
||||
let sSrc=pNode.getAttribute('src');
|
||||
if( !IsHttpUrl(sSrc) )
|
||||
return null;
|
||||
let pUrl=new URL(sSrc);
|
||||
return ( pUrl.origin=='https://www.youtube.com' && pUrl.pathname.indexOf('/embed/')==0 ) ? pUrl.href : null;
|
||||
}
|
||||
|
||||
function CopyDescNodes( pSrc, pDst )
|
||||
{
|
||||
for( let pNode=pSrc.firstChild;pNode!=null;pNode=pNode.nextSibling )
|
||||
{
|
||||
if( pNode.nodeType==Node.TEXT_NODE )
|
||||
{
|
||||
pDst.appendChild( document.createTextNode(pNode.nodeValue) );
|
||||
continue;
|
||||
}
|
||||
if( pNode.nodeType!=Node.ELEMENT_NODE )
|
||||
continue;
|
||||
|
||||
let sTag=pNode.nodeName.toUpperCase();
|
||||
if( sTag=='IFRAME' )
|
||||
{
|
||||
let sVideoUrl=GetYouTubeEmbedUrl(pNode);
|
||||
if( sVideoUrl!=null )
|
||||
{
|
||||
let pLink=document.createElement('A');
|
||||
pLink.setAttribute('href',sVideoUrl);
|
||||
pLink.textContent=sVideoUrl;
|
||||
pDst.appendChild(pLink);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if( $.inArray(sTag,DescDroppedTags)>=0 )
|
||||
continue;
|
||||
if( $.inArray(sTag,DescAllowedTags)<0 )
|
||||
{
|
||||
CopyDescNodes(pNode,pDst);
|
||||
continue;
|
||||
}
|
||||
|
||||
let pElem=document.createElement(sTag);
|
||||
if( sTag=='A' && IsHttpUrl(pNode.getAttribute('href')) )
|
||||
pElem.setAttribute('href',pNode.getAttribute('href'));
|
||||
else if( sTag=='IMG' )
|
||||
{
|
||||
if( !IsHttpsUrl(pNode.getAttribute('src')) )
|
||||
continue;
|
||||
pElem.setAttribute('src',pNode.getAttribute('src'));
|
||||
}
|
||||
$.each( DescAllowedAttrs[sTag]||[], function(i,sAttr){
|
||||
let sValue=pNode.getAttribute(sAttr);
|
||||
if( sValue!=null && ( $.inArray(sAttr,DescNumericAttrs)<0 || /^\d+$/.test(sValue) ) )
|
||||
pElem.setAttribute(sAttr,sValue);
|
||||
});
|
||||
CopyDescNodes(pNode,pElem);
|
||||
pDst.appendChild(pElem);
|
||||
}
|
||||
}
|
||||
|
||||
function SanitizeDescHtml( strHtml )
|
||||
{
|
||||
let pFragment=document.createDocumentFragment();
|
||||
// A DOMParser document is inert: it runs no scripts and loads no resources.
|
||||
let pDoc=new DOMParser().parseFromString(strHtml,'text/html');
|
||||
if( pDoc && pDoc.body )
|
||||
CopyDescNodes(pDoc.body,pFragment);
|
||||
return pFragment;
|
||||
}
|
||||
|
||||
function ShowProfilelInfo( pProfile )
|
||||
{
|
||||
//==========Profile Info==========
|
||||
@@ -583,10 +483,10 @@ function ShowProfilelInfo( pProfile )
|
||||
let sProfileAuthor=decodeURIComponent(pProfile.author);
|
||||
let sProfileDesc=decodeURIComponent(pProfile.description);
|
||||
|
||||
$('#ProfileName').text(sProfileName);
|
||||
$('#ProfileAuthor').text(sProfileAuthor);
|
||||
$('#ProfileName').html(sProfileName);
|
||||
$('#ProfileAuthor').html(sProfileAuthor);
|
||||
|
||||
$('#Profile_Desc').empty().append( SanitizeDescHtml( html_decode(sProfileDesc) ) );
|
||||
$('#Profile_Desc').html( html_decode(sProfileDesc) );
|
||||
|
||||
let ProfilePreviewList=pProfile.preview_img;
|
||||
let TotalPreview=ProfilePreviewList.length;
|
||||
@@ -599,14 +499,15 @@ function ShowProfilelInfo( pProfile )
|
||||
|
||||
if(TotalPreview>0)
|
||||
{
|
||||
$('#ProfilePreviewList').empty();
|
||||
let htmlPreview='';
|
||||
for(let pn=0;pn<TotalPreview;pn++)
|
||||
{
|
||||
let FTmpPath=ProfilePreviewList[pn]['filepath'];
|
||||
|
||||
$('#ProfilePreviewList').append( $('<div class="swiper-slide"></div>').append( $('<img class="Model_PrevImg" />').attr('src',FTmpPath) ) );
|
||||
htmlPreview+='<div class="swiper-slide"><img class="Model_PrevImg" src="'+FTmpPath+'" /></div>';
|
||||
}
|
||||
|
||||
$('#ProfilePreviewList').html(htmlPreview);
|
||||
$('#Profile_Preview_Image').viewer({
|
||||
title: false,
|
||||
fullsreen: false,
|
||||
|
||||
+10
-5
@@ -2504,7 +2504,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
||||
|
||||
{
|
||||
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.msg = path;
|
||||
ctx.cancellation_check = [print]() { return print->canceled(); };
|
||||
@@ -2556,7 +2557,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
||||
}
|
||||
{
|
||||
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.msg = std::string(path) + "\n" + err_msg;
|
||||
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());
|
||||
{
|
||||
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.msg = std::string(path) + "\n" + ex.what();
|
||||
ctx.cancellation_check = [print]() { return print->canceled(); };
|
||||
@@ -2693,7 +2696,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
||||
if (ret) {
|
||||
{
|
||||
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.msg = std::string(path) + "\nFailed to rename the output G-code file: " + ret.message();
|
||||
ctx.cancellation_check = [print]() { return print->canceled(); };
|
||||
@@ -2712,7 +2716,8 @@ void GCode::do_export(Print* print, const char* path, GCodeProcessorResult* resu
|
||||
|
||||
{
|
||||
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.msg = path;
|
||||
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;
|
||||
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.cancellation_check = [this]() { return canceled(); };
|
||||
fire_lifecycle_event(LifecycleEvent::SliceStarted, ctx);
|
||||
@@ -3344,7 +3345,8 @@ void Print::process(long long *time_cost_with_cache, bool use_cache)
|
||||
|
||||
{
|
||||
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.cancellation_check = [this]() { return canceled(); };
|
||||
fire_lifecycle_event(LifecycleEvent::SliceGeometryFinished, ctx);
|
||||
@@ -4950,7 +4952,8 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
|
||||
{
|
||||
{
|
||||
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.msg = file;
|
||||
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();
|
||||
{
|
||||
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.msg = file + "\n" + ex.what();
|
||||
ctx.cancellation_check = [this]() { return canceled(); };
|
||||
@@ -4994,7 +4998,8 @@ void Print::export_gcode_from_previous_file(const std::string& file, GCodeProces
|
||||
|
||||
{
|
||||
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.msg = file;
|
||||
ctx.cancellation_check = [this]() { return canceled(); };
|
||||
|
||||
@@ -260,11 +260,6 @@ extern bool is_json_file(const std::string& path);
|
||||
// Both '/' and '\\' are treated as separators on every platform, so an archive rejected on one OS
|
||||
// is rejected on all of them.
|
||||
extern bool is_path_within_root(const std::string &rel_path, const boost::filesystem::path &root);
|
||||
// True if path names an entry strictly inside root: it must be spelled with root as its prefix,
|
||||
// and must still resolve inside root once symlinks are followed.
|
||||
extern bool is_absolute_path_within_root(const boost::filesystem::path &path, const boost::filesystem::path &root);
|
||||
// True if opening a file with this name through the desktop would run it as a program or script.
|
||||
extern bool is_executable_file_name(const std::string &file_name);
|
||||
|
||||
// Orca: custom protocal support utils
|
||||
inline bool is_orca_open(const std::string& url) { return boost::starts_with(url, "orcaslicer://open"); }
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
#include <boost/shared_ptr.hpp>
|
||||
|
||||
#include <boost/algorithm/string/predicate.hpp>
|
||||
#include <boost/algorithm/string/case_conv.hpp>
|
||||
#include <boost/filesystem.hpp>
|
||||
#include <boost/filesystem/path.hpp>
|
||||
#include <boost/nowide/fstream.hpp>
|
||||
@@ -1113,34 +1112,6 @@ bool is_path_within_root(const std::string &rel_path, const boost::filesystem::p
|
||||
}
|
||||
}
|
||||
|
||||
bool is_absolute_path_within_root(const boost::filesystem::path &path, const boost::filesystem::path &root)
|
||||
{
|
||||
const boost::filesystem::path rel = path.lexically_relative(root);
|
||||
return !rel.empty() && rel != "." && is_path_within_root(rel.string(), root);
|
||||
}
|
||||
|
||||
bool is_executable_file_name(const std::string &file_name)
|
||||
{
|
||||
static const std::vector<std::string> executable_extensions = {
|
||||
// Windows
|
||||
"exe", "com", "bat", "cmd", "scr", "pif", "msi", "msp", "msc", "cpl", "lnk", "url", "hta", "js", "jse", "vbs",
|
||||
"vbe", "wsf", "wsh", "ps1", "psm1", "reg", "jar", "appref-ms", "scf", "inf", "py", "pyw",
|
||||
// macOS
|
||||
"app", "command", "pkg", "terminal", "workflow",
|
||||
// Linux
|
||||
"sh", "bash", "run", "desktop", "appimage"};
|
||||
// Windows ignores trailing dots and spaces, so "setup.exe." still runs as an .exe.
|
||||
const size_t end = file_name.find_last_not_of(". ");
|
||||
if (end == std::string::npos)
|
||||
return false;
|
||||
const std::string name = file_name.substr(0, end + 1);
|
||||
const size_t dot = name.find_last_of('.');
|
||||
if (dot == std::string::npos || name.find_first_of("/\\", dot) != std::string::npos)
|
||||
return false;
|
||||
const std::string extension = boost::algorithm::to_lower_copy(name.substr(dot + 1));
|
||||
return std::find(executable_extensions.begin(), executable_extensions.end(), extension) != executable_extensions.end();
|
||||
}
|
||||
|
||||
bool is_img_file(const std::string &path)
|
||||
{
|
||||
return boost::iends_with(path, ".png") || boost::iends_with(path, ".svg");
|
||||
|
||||
@@ -12800,8 +12800,6 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
|
||||
notification_manager->set_slicing_progress_export_possible();
|
||||
|
||||
// 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 bool stops showing export finished notification even when process_completed_with_error is false
|
||||
bool has_error = false;
|
||||
@@ -12848,8 +12846,18 @@ void Plater::priv::on_process_completed(SlicingProcessCompletedEvent &evt)
|
||||
|
||||
{
|
||||
Slic3r::LifecycleEventContext ctx;
|
||||
ctx.name = lifecycle_job_name;
|
||||
ctx.code = evt.cancelled() ? Slic3r::LifecycleEvtCode::Warn : (has_error ? Slic3r::LifecycleEvtCode::Error : Slic3r::LifecycleEvtCode::Ok);
|
||||
if (const PrintBase* print = this->background_process.current_print()) {
|
||||
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());
|
||||
Slic3r::fire_lifecycle_event(Slic3r::LifecycleEvent::SlicingJobComplete, ctx);
|
||||
}
|
||||
|
||||
@@ -27,7 +27,6 @@
|
||||
#include "GUI_App.hpp"
|
||||
#include "GUI_ObjectList.hpp"
|
||||
#include "MainFrame.hpp"
|
||||
#include "MsgDialog.hpp"
|
||||
#include <slic3r/GUI/Widgets/WebView.hpp>
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
@@ -294,24 +293,9 @@ void ProjectPanel::OnScriptMessage(wxWebViewEvent& evt)
|
||||
if (!accessory_path.empty()) {
|
||||
std::string decode_path = wxGetApp().url_decode(accessory_path.ToStdString());
|
||||
fs::path path(decode_path);
|
||||
// Only open the project's own extracted auxiliary files, with the root built as in Reload().
|
||||
fs::path aux_root(encode_path(wxGetApp().plater()->model().get_auxiliary_file_temp_path().c_str()));
|
||||
|
||||
if (is_absolute_path_within_root(path, aux_root) && fs::is_regular_file(path)) {
|
||||
// Attachments come with the project, so ask before running one that is a program or script.
|
||||
bool open = true;
|
||||
if (is_executable_file_name(path.filename().string())) {
|
||||
MessageDialog dlg(this,
|
||||
wxString::Format(_L("\"%s\" is a program or script. Opening it will run it on this computer.\n\n"
|
||||
"Only open attachments from projects you trust. Open it anyway?"),
|
||||
from_path(path.filename())),
|
||||
_L("Open attachment"), wxICON_WARNING | wxYES_NO);
|
||||
open = dlg.ShowModal() == wxID_YES;
|
||||
}
|
||||
if (open)
|
||||
wxLaunchDefaultApplication(path.wstring(), 0);
|
||||
} else {
|
||||
BOOST_LOG_TRIVIAL(warning) << "open_3mf_accessory: ignoring path outside the project auxiliary directory: " << decode_path;
|
||||
if (fs::exists(path)) {
|
||||
wxLaunchDefaultApplication(path.wstring(), 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,14 +31,15 @@ int UserManager::parse_json(std::string payload)
|
||||
{
|
||||
bool restored_json = false;
|
||||
json j;
|
||||
json j_pre = json::parse(payload);
|
||||
if (j_pre.empty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
//bind/unbind
|
||||
|
||||
try {
|
||||
json j_pre = json::parse(payload);
|
||||
if (j_pre.empty()) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (j_pre.contains("bind")) {
|
||||
if (j_pre["bind"].contains("command")) {
|
||||
|
||||
|
||||
@@ -274,13 +274,4 @@ bool Duet::start_print(wxString &msg, const std::string &filename, ConnectionTyp
|
||||
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;
|
||||
void disconnect(ConnectionType connectionType) 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;
|
||||
}
|
||||
|
||||
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
|
||||
std::string ESP3D::get_short_name(const std::string& filename) const
|
||||
{
|
||||
|
||||
@@ -33,7 +33,6 @@ private:
|
||||
std::string m_console_port;
|
||||
|
||||
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 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))
|
||||
return false;
|
||||
|
||||
const auto parsed = json::parse(body, nullptr, false, true);
|
||||
if (parsed.is_discarded()) {
|
||||
if (!parse_material_slots(body, slots, supports_material_station)) {
|
||||
msg = _(L("Flashforge returned an invalid JSON response."));
|
||||
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& station = detail.contains("matlStationInfo") ? detail["matlStationInfo"] :
|
||||
detail.contains("MatlStationInfo") ? detail["MatlStationInfo"] : json();
|
||||
@@ -542,12 +552,21 @@ bool Flashforge::fetch_material_slots(std::vector<FlashforgeMaterialSlot>& slots
|
||||
if (supports_material_station != nullptr)
|
||||
*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) {
|
||||
if (!slot.is_object())
|
||||
continue;
|
||||
FlashforgeMaterialSlot info;
|
||||
info.slot_id = slot.value("slotId", static_cast<int>(slots.size()) + 1);
|
||||
info.has_filament = slot.value("hasFilament", false);
|
||||
info.material_name = slot.value("materialName", std::string());
|
||||
info.material_color = slot.value("materialColor", std::string());
|
||||
info.slot_id = static_cast<int>(slots.size()) + 1;
|
||||
if (const auto it = slot.find("slotId"); it != slot.end())
|
||||
try_parse_json_int(*it, info.slot_id);
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -670,13 +689,4 @@ std::string Flashforge::extract_host_name() const
|
||||
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
|
||||
|
||||
@@ -45,6 +45,8 @@ public:
|
||||
PrintHostPostUploadActions get_post_upload_actions() const override { return PrintHostPostUploadAction::StartPrint; }
|
||||
std::string get_host() const override { return m_host; }
|
||||
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);
|
||||
|
||||
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;
|
||||
std::string make_http_url(const std::string& path) const;
|
||||
std::string extract_host_name() const;
|
||||
int get_err_code_from_body(const std::string &body) const;
|
||||
bool connect(wxString& msg) 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;
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
@@ -34,7 +34,6 @@ private:
|
||||
|
||||
std::string get_upload_url(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;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -3,9 +3,13 @@
|
||||
#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/string.h>
|
||||
#include <wx/app.h>
|
||||
@@ -115,10 +119,81 @@ std::string PrintHost::get_print_host_webui(DynamicPrintConfig* config)
|
||||
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) {
|
||||
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);
|
||||
} else {
|
||||
if (error.find("curl:Timeout was reached") != std::string::npos) {
|
||||
@@ -245,7 +320,12 @@ void PrintHostJobQueue::priv::bg_thread_main()
|
||||
% job.cancelled;
|
||||
|
||||
if (! job.cancelled) {
|
||||
perform_job(std::move(job));
|
||||
// A failing job must not stop the worker, or later jobs would stay queued forever.
|
||||
try {
|
||||
perform_job(std::move(job));
|
||||
} catch (const std::exception &e) {
|
||||
emit_error(e.what());
|
||||
}
|
||||
}
|
||||
|
||||
remove_source();
|
||||
|
||||
@@ -87,6 +87,8 @@ public:
|
||||
|
||||
static PrintHost* get_print_host(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
|
||||
virtual bool is_cloud() const { return false; }
|
||||
|
||||
@@ -654,13 +654,4 @@ bool UltiMaker::start_print(wxString &msg, const std::string &filename, Connecti
|
||||
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 disconnect(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/Model.hpp"
|
||||
#include "libslic3r/GCodeReader.hpp"
|
||||
#include "libslic3r/GCode/GCodeProcessor.hpp"
|
||||
#include "libslic3r/Exception.hpp"
|
||||
#include "libslic3r/LifecycleEvents.hpp"
|
||||
|
||||
#include "test_helpers.hpp"
|
||||
#include "test_utils.hpp"
|
||||
@@ -22,7 +25,10 @@
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
using namespace Slic3r;
|
||||
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);
|
||||
}
|
||||
|
||||
struct ScopedLifecycleHook
|
||||
{
|
||||
explicit ScopedLifecycleHook(LifecycleHookFn hook) { set_lifecycle_hook_fn(std::move(hook)); }
|
||||
~ScopedLifecycleHook() { set_lifecycle_hook_fn(nullptr); }
|
||||
};
|
||||
|
||||
} // 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]")
|
||||
{
|
||||
Model model;
|
||||
|
||||
@@ -152,65 +152,3 @@ TEST_CASE("resolve_cli_input_path leaves inputs that must not be completed uncha
|
||||
REQUIRE(resolve_cli_input_path("").empty());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("is_absolute_path_within_root accepts only entries inside the root", "[utils]") {
|
||||
namespace fs = boost::filesystem;
|
||||
ScopedTemporaryDir outer;
|
||||
const fs::path root = outer.path() / "Auxiliaries";
|
||||
fs::create_directories(root / "Others");
|
||||
const fs::path inside = root / "Others" / "note.txt";
|
||||
const fs::path outside = outer.path() / "secret.txt";
|
||||
std::ofstream(inside.string()) << "inside";
|
||||
std::ofstream(outside.string()) << "outside";
|
||||
|
||||
SECTION("a file inside the root") {
|
||||
REQUIRE(is_absolute_path_within_root(inside, root));
|
||||
}
|
||||
SECTION("a path inside the root whose file does not exist yet") {
|
||||
REQUIRE(is_absolute_path_within_root(root / "Others" / "missing.txt", root));
|
||||
}
|
||||
SECTION("the root itself") {
|
||||
REQUIRE_FALSE(is_absolute_path_within_root(root, root));
|
||||
}
|
||||
SECTION("a parent-directory escape spelled under the root") {
|
||||
REQUIRE_FALSE(is_absolute_path_within_root(root / "Others" / ".." / ".." / "secret.txt", root));
|
||||
}
|
||||
SECTION("an absolute path elsewhere") {
|
||||
REQUIRE_FALSE(is_absolute_path_within_root(outside, root));
|
||||
}
|
||||
SECTION("a sibling directory sharing the root's name as a prefix") {
|
||||
const fs::path sibling = outer.path() / "Auxiliaries2" / "note.txt";
|
||||
REQUIRE_FALSE(is_absolute_path_within_root(sibling, root));
|
||||
}
|
||||
SECTION("a relative path") {
|
||||
REQUIRE_FALSE(is_absolute_path_within_root(fs::path("Others") / "note.txt", root));
|
||||
}
|
||||
SECTION("an empty path") {
|
||||
REQUIRE_FALSE(is_absolute_path_within_root(fs::path(), root));
|
||||
}
|
||||
#ifndef _WIN32
|
||||
// Creating symlinks on Windows needs elevated rights or developer mode.
|
||||
SECTION("a symlink inside the root that points outside") {
|
||||
const fs::path link = root / "Others" / "link.txt";
|
||||
fs::create_symlink(outside, link);
|
||||
REQUIRE_FALSE(is_absolute_path_within_root(link, root));
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
TEST_CASE("is_executable_file_name flags attachments that would run as programs", "[utils]") {
|
||||
const std::string executable = GENERATE(as<std::string>{},
|
||||
"setup.exe", "SETUP.EXE", "Manual.pdf.exe", "run.bat", "start.cmd", "shortcut.lnk", "site.url", "script.ps1",
|
||||
"macro.vbs", "tool.jar", "script.py", "Install.command", "Tool.app", "installer.pkg", "install.sh",
|
||||
"launcher.desktop", "Printer.AppImage", "setup.exe.", "setup.exe ", "setup.exe. .", ".exe");
|
||||
INFO(executable);
|
||||
CHECK(is_executable_file_name(executable));
|
||||
}
|
||||
|
||||
TEST_CASE("is_executable_file_name leaves documents and models alone", "[utils]") {
|
||||
const std::string document = GENERATE(as<std::string>{},
|
||||
"Manual.pdf", "BOM.xlsx", "notes.txt", "photo.JPG", "assembly.step", "part.stl", "project.3mf", "readme",
|
||||
"exe", "setup.exe.pdf", "", "...", "dir.exe/readme");
|
||||
INFO(document);
|
||||
CHECK_FALSE(is_executable_file_name(document));
|
||||
}
|
||||
|
||||
@@ -18,12 +18,14 @@ add_executable(${_TEST_NAME}_tests
|
||||
test_plugin_install.cpp
|
||||
test_plugin_lifecycle.cpp
|
||||
test_plugin_printer_agent.cpp
|
||||
test_printhost.cpp
|
||||
test_slicing_pipeline_bindings.cpp
|
||||
test_slicing_pipeline_config.cpp
|
||||
test_plugin_sort.cpp
|
||||
test_plugin_cloud_metadata.cpp
|
||||
test_plugin_audit.cpp
|
||||
test_shortcuts.cpp
|
||||
test_user_manager.cpp
|
||||
../fff_print/test_helpers.cpp
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,316 @@
|
||||
#include <catch2/catch_all.hpp>
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <nlohmann/json.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 {}; }
|
||||
};
|
||||
|
||||
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());
|
||||
}
|
||||
@@ -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