From cf32b56454b13faa72bb75363ab63e1d66ba7d9f Mon Sep 17 00:00:00 2001 From: Vojtech Bubnik Date: Tue, 10 Aug 2021 15:21:53 +0200 Subject: [PATCH] Implemented extension of the G-code post-processor framework: 1) New environment variable SLIC3R_PP_HOST contains one of "File", "PrusaLink", "Repetier", "SL1Host", "OctoPrint", "FlashAir", "Duet", "AstroBox" ... 2) New environment variable SLIC3R_PP_OUTPUT_NAME contains the name of the G-code file including path (for SLIC3R_PP_HOST == "File") or a name of the file after upload to the host (PrusaLink, Octoprint ...) 3) The post-processing script may suggest a new output file name (likely based on SLIC3R_PP_OUTPUT_NAME) by saving it as a single line into a new "output name" temp file. The "output name" file name is created by suffixing the input G-code file name with ".output_name". Please note that the G-code viewer visualizes G-code before post-processing. Fixes Broken PostProcessing when script changes out-filename #6042 --- src/libslic3r/GCode/GCodeProcessor.hpp | 6 +- src/libslic3r/GCode/PostProcessor.cpp | 130 +++++++++++++++---- src/libslic3r/GCode/PostProcessor.hpp | 18 ++- src/slic3r/GUI/BackgroundSlicingProcess.cpp | 133 +++++++++++--------- src/slic3r/GUI/BackgroundSlicingProcess.hpp | 5 +- 5 files changed, 208 insertions(+), 84 deletions(-) diff --git a/src/libslic3r/GCode/GCodeProcessor.hpp b/src/libslic3r/GCode/GCodeProcessor.hpp index d83c39d09b4..999af481ded 100644 --- a/src/libslic3r/GCode/GCodeProcessor.hpp +++ b/src/libslic3r/GCode/GCodeProcessor.hpp @@ -337,9 +337,9 @@ namespace Slic3r { std::string printer; void reset() { - print = ""; - filament = std::vector(); - printer = ""; + print.clear(); + filament.clear(); + printer.clear(); } }; std::string filename; diff --git a/src/libslic3r/GCode/PostProcessor.cpp b/src/libslic3r/GCode/PostProcessor.cpp index 9a66e743bc4..2ffffc7d6d9 100644 --- a/src/libslic3r/GCode/PostProcessor.cpp +++ b/src/libslic3r/GCode/PostProcessor.cpp @@ -1,5 +1,7 @@ #include "PostProcessor.hpp" +#include "libslic3r/Utils.hpp" + #include #include #include @@ -179,41 +181,125 @@ static int run_script(const std::string &script, const std::string &gcode, std:: namespace Slic3r { -void run_post_process_scripts(const std::string &path, const DynamicPrintConfig &config) +// Run post processing script / scripts if defined. +// Returns true if a post-processing script was executed. +// Returns false if no post-processing script was defined. +// Throws an exception on error. +// host is one of "File", "PrusaLink", "Repetier", "SL1Host", "OctoPrint", "FlashAir", "Duet", "AstroBox" ... +// For a "File" target, a temp file will be created for src_path by adding a ".pp" suffix and src_path will be updated. +// In that case the caller is responsible to delete the temp file created. +// output_name is the final name of the G-code on SD card or when uploaded to PrusaLink or OctoPrint. +// If uploading to PrusaLink or OctoPrint, then the file will be renamed to output_name first on the target host. +// The post-processing script may change the output_name. +bool run_post_process_scripts(std::string &src_path, bool make_copy, const std::string &host, std::string &output_name, const DynamicPrintConfig &config) { - const auto* post_process = config.opt("post_process"); + const auto *post_process = config.opt("post_process"); if (// likely running in SLA mode post_process == nullptr || // no post-processing script post_process->values.empty()) - return; + return false; + + std::string path; + if (make_copy) { + // Don't run the post-processing script on the input file, it will be memory mapped by the G-code viewer. + // Make a copy. + path = src_path + ".pp"; + // First delete an old file if it exists. + try { + if (boost::filesystem::exists(path)) + boost::filesystem::remove(path); + } catch (const std::exception &err) { + BOOST_LOG_TRIVIAL(error) << Slic3r::format("Failed deleting an old temporary file %1% before running a post-processing script: %2%", path, err.what()); + } + // Second make a copy. + std::string error_message; + if (copy_file(src_path, path, error_message, false) != SUCCESS) + throw Slic3r::RuntimeError(Slic3r::format("Failed making a temporary copy of G-code file %1% before running a post-processing script: %2%", src_path, error_message)); + } else { + // Don't make a copy of the G-code before running the post-processing script. + path = src_path; + } + + auto delete_copy = [&path, &src_path, make_copy]() { + if (make_copy) + try { + if (boost::filesystem::exists(path)) + boost::filesystem::remove(path); + } catch (const std::exception &err) { + BOOST_LOG_TRIVIAL(error) << Slic3r::format("Failed deleting a temporary copy %1% of a G-code file %2% : %3%", path, src_path, err.what()); + } + }; - // Store print configuration into environment variables. - config.setenv_(); auto gcode_file = boost::filesystem::path(path); if (! boost::filesystem::exists(gcode_file)) throw Slic3r::RuntimeError(std::string("Post-processor can't find exported gcode file")); - for (const std::string &scripts : post_process->values) { - std::vector lines; - boost::split(lines, scripts, boost::is_any_of("\r\n")); - for (std::string script : lines) { - // Ignore empty post processing script lines. - boost::trim(script); - if (script.empty()) - continue; - BOOST_LOG_TRIVIAL(info) << "Executing script " << script << " on file " << path; - - std::string std_err; - const int result = run_script(script, gcode_file.string(), std_err); - if (result != 0) { - const std::string msg = std_err.empty() ? (boost::format("Post-processing script %1% on file %2% failed.\nError code: %3%") % script % path % result).str() - : (boost::format("Post-processing script %1% on file %2% failed.\nError code: %3%\nOutput:\n%4%") % script % path % result % std_err).str(); - BOOST_LOG_TRIVIAL(error) << msg; - throw Slic3r::RuntimeError(msg); + // Store print configuration into environment variables. + config.setenv_(); + // Let the post-processing script know the target host ("File", "PrusaLink", "Repetier", "SL1Host", "OctoPrint", "FlashAir", "Duet", "AstroBox" ...) + boost::nowide::setenv("SLIC3R_PP_HOST", host.c_str(), 1); + // Let the post-processing script know the final file name. For "File" host, it is a full path of the target file name and its location, for example pointing to an SD card. + // For "PrusaLink" or "OctoPrint", it is a file name optionally with a directory on the target host. + boost::nowide::setenv("SLIC3R_PP_OUTPUT_NAME", output_name.c_str(), 1); + + // Path to an optional file that the post-processing script may create and populate it with a single line containing the output_name replacement. + std::string path_output_name = src_path + ".output_name"; + auto remove_output_name_file = [&path_output_name, &src_path]() { + try { + if (boost::filesystem::exists(path_output_name)) + boost::filesystem::remove(path_output_name); + } catch (const std::exception &err) { + BOOST_LOG_TRIVIAL(error) << Slic3r::format("Failed deleting a file %1% carrying the final name / path of a G-code file %2%: %3%", path_output_name, src_path, err.what()); + } + }; + // Remove possible stalled path_output_name of the previous run. + remove_output_name_file(); + + try { + for (const std::string &scripts : post_process->values) { + std::vector lines; + boost::split(lines, scripts, boost::is_any_of("\r\n")); + for (std::string script : lines) { + // Ignore empty post processing script lines. + boost::trim(script); + if (script.empty()) + continue; + BOOST_LOG_TRIVIAL(info) << "Executing script " << script << " on file " << path; + std::string std_err; + const int result = run_script(script, gcode_file.string(), std_err); + if (result != 0) { + const std::string msg = std_err.empty() ? (boost::format("Post-processing script %1% on file %2% failed.\nError code: %3%") % script % path % result).str() + : (boost::format("Post-processing script %1% on file %2% failed.\nError code: %3%\nOutput:\n%4%") % script % path % result % std_err).str(); + BOOST_LOG_TRIVIAL(error) << msg; + delete_copy(); + throw Slic3r::RuntimeError(msg); + } + } + } + if (boost::filesystem::exists(path_output_name)) { + try { + // Read a single line from path_output_name, which should contain the new output name of the post-processed G-code. + boost::nowide::fstream f; + f.open(path_output_name, std::ios::in); + std::string new_output_name; + std::getline(f, new_output_name); + f.close(); + BOOST_LOG_TRIVIAL(trace) << "Post-processing script changed the file name from " << output_name << " to " << new_output_name; + output_name = new_output_name; + } catch (const std::exception &err) { + BOOST_LOG_TRIVIAL(error) << Slic3r::format("Failed reading a file %1% carrying the final name / path of a G-code file: %2%", path_output_name, err.what()); } + remove_output_name_file(); } + } catch (...) { + remove_output_name_file(); + delete_copy(); + throw; } + + src_path = std::move(path); + return true; } } // namespace Slic3r diff --git a/src/libslic3r/GCode/PostProcessor.hpp b/src/libslic3r/GCode/PostProcessor.hpp index a9196aef7c0..3ea4e0bc1e7 100644 --- a/src/libslic3r/GCode/PostProcessor.hpp +++ b/src/libslic3r/GCode/PostProcessor.hpp @@ -8,7 +8,23 @@ namespace Slic3r { -extern void run_post_process_scripts(const std::string &path, const DynamicPrintConfig &config); +// Run post processing script / scripts if defined. +// Returns true if a post-processing script was executed. +// Returns false if no post-processing script was defined. +// Throws an exception on error. +// host is one of "File", "PrusaLink", "Repetier", "SL1Host", "OctoPrint", "FlashAir", "Duet", "AstroBox" ... +// If make_copy, then a temp file will be created for src_path by adding a ".pp" suffix and src_path will be updated. +// In that case the caller is responsible to delete the temp file created. +// output_name is the final name of the G-code on SD card or when uploaded to PrusaLink or OctoPrint. +// If uploading to PrusaLink or OctoPrint, then the file will be renamed to output_name first on the target host. +// The post-processing script may change the output_name. +extern bool run_post_process_scripts(std::string &src_path, bool make_copy, const std::string &host, std::string &output_name, const DynamicPrintConfig &config); + +inline bool run_post_process_scripts(std::string &src_path, const DynamicPrintConfig &config) +{ + std::string src_path_name = src_path; + return run_post_process_scripts(src_path, false, "File", src_path_name, config); +} } // namespace Slic3r diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.cpp b/src/slic3r/GUI/BackgroundSlicingProcess.cpp index c48b8f2aafc..8a58e5ec93a 100644 --- a/src/slic3r/GUI/BackgroundSlicingProcess.cpp +++ b/src/slic3r/GUI/BackgroundSlicingProcess.cpp @@ -154,54 +154,7 @@ void BackgroundSlicingProcess::process_fff() if (this->set_step_started(bspsGCodeFinalize)) { if (! m_export_path.empty()) { wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, new wxCommandEvent(m_event_export_began_id)); - - // let the gcode window to unmap the temporary .gcode file (m_temp_output_path) - // because the scripts may want to modify it - GUI::wxGetApp().plater()->stop_mapping_gcode_window(); - - m_print->set_status(95, _utf8(L("Running post-processing scripts"))); - run_post_process_scripts(m_temp_output_path, m_fff_print->full_print_config()); - - // let the gcode window to reload and remap the temporary .gcode file (m_temp_output_path) - GUI::wxGetApp().plater()->start_mapping_gcode_window(); - - //FIXME localize the messages - // Perform the final post-processing of the export path by applying the print statistics over the file name. - std::string export_path = m_fff_print->print_statistics().finalize_output_path(m_export_path); - std::string error_message; - int copy_ret_val = CopyFileResult::SUCCESS; - try - { - copy_ret_val = copy_file(m_temp_output_path, export_path, error_message, m_export_path_on_removable_media); - } - catch (...) - { - throw Slic3r::ExportError(_utf8(L("Unknown error occured during exporting G-code."))); - } - switch (copy_ret_val) { - case CopyFileResult::SUCCESS: break; // no error - case CopyFileResult::FAIL_COPY_FILE: - throw Slic3r::ExportError((boost::format(_utf8(L("Copying of the temporary G-code to the output G-code failed. Maybe the SD card is write locked?\nError message: %1%"))) % error_message).str()); - break; - case CopyFileResult::FAIL_FILES_DIFFERENT: - throw Slic3r::ExportError((boost::format(_utf8(L("Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp."))) % export_path).str()); - break; - case CopyFileResult::FAIL_RENAMING: - throw Slic3r::ExportError((boost::format(_utf8(L("Renaming of the G-code after copying to the selected destination folder has failed. Current path is %1%.tmp. Please try exporting again."))) % export_path).str()); - break; - case CopyFileResult::FAIL_CHECK_ORIGIN_NOT_OPENED: - throw Slic3r::ExportError((boost::format(_utf8(L("Copying of the temporary G-code has finished but the original code at %1% couldn't be opened during copy check. The output G-code is at %2%.tmp."))) % m_temp_output_path % export_path).str()); - break; - case CopyFileResult::FAIL_CHECK_TARGET_NOT_OPENED: - throw Slic3r::ExportError((boost::format(_utf8(L("Copying of the temporary G-code has finished but the exported code couldn't be opened during copy check. The output G-code is at %1%.tmp."))) % export_path).str()); - break; - default: - throw Slic3r::ExportError(_utf8(L("Unknown error occured during exporting G-code."))); - BOOST_LOG_TRIVIAL(error) << "Unexpected fail code(" << (int)copy_ret_val << ") durring copy_file() to " << export_path << "."; - break; - } - - m_print->set_status(100, (boost::format(_utf8(L("G-code file exported to %1%"))) % export_path).str()); + finalize_gcode(); } else if (! m_upload_job.empty()) { wxQueueEvent(GUI::wxGetApp().mainframe->m_plater, new wxCommandEvent(m_event_export_began_id)); prepare_upload(); @@ -621,8 +574,11 @@ Print::ApplyStatus BackgroundSlicingProcess::apply(const Model &model, const Dyn // Some FFF status was invalidated, and the G-code was not exported yet. // Let the G-code preview UI know that the final G-code preview is not valid. // In addition, this early memory deallocation reduces memory footprint. - if (m_gcode_result != nullptr) + if (m_gcode_result != nullptr) { + //FIXME calling platter from here is not a staple of a good architecture. + GUI::wxGetApp().plater()->stop_mapping_gcode_window(); m_gcode_result->reset(); + } } return invalidated; } @@ -698,12 +654,73 @@ bool BackgroundSlicingProcess::invalidate_all_steps() return m_step_state.invalidate_all([this](){ this->stop_internal(); }); } -void BackgroundSlicingProcess::prepare_upload() -{ - // A print host upload job has been scheduled, enqueue it to the printhost job queue +// G-code is generated in m_temp_output_path. +// Optionally run a post-processing script on a copy of m_temp_output_path. +// Copy the final G-code to target location (possibly a SD card, if it is a removable media, then verify that the file was written without an error). +void BackgroundSlicingProcess::finalize_gcode() +{ + m_print->set_status(95, _utf8(L("Running post-processing scripts"))); + + // Perform the final post-processing of the export path by applying the print statistics over the file name. + std::string export_path = m_fff_print->print_statistics().finalize_output_path(m_export_path); + std::string output_path = m_temp_output_path; + // Both output_path and export_path ar in-out parameters. + // If post processed, output_path will differ from m_temp_output_path as run_post_process_scripts() will make a copy of the G-code to not + // collide with the G-code viewer memory mapping of the unprocessed G-code. G-code viewer maps unprocessed G-code, because m_gcode_result + // is calculated for the unprocessed G-code and it references lines in the memory mapped G-code file by line numbers. + // export_path may be changed by the post-processing script as well if the post processing script decides so, see GH #6042. + bool post_processed = run_post_process_scripts(output_path, true, "File", export_path, m_fff_print->full_print_config()); + auto remove_post_processed_temp_file = [post_processed, &output_path]() { + if (post_processed) + try { + boost::filesystem::remove(output_path); + } catch (const std::exception &ex) { + BOOST_LOG_TRIVIAL(error) << "Failed to remove temp file " << output_path << ": " << ex.what(); + } + }; - // XXX: is fs::path::string() right? + //FIXME localize the messages + std::string error_message; + int copy_ret_val = CopyFileResult::SUCCESS; + try + { + copy_ret_val = copy_file(output_path, export_path, error_message, m_export_path_on_removable_media); + remove_post_processed_temp_file(); + } + catch (...) + { + remove_post_processed_temp_file(); + throw Slic3r::ExportError(_utf8(L("Unknown error occured during exporting G-code."))); + } + switch (copy_ret_val) { + case CopyFileResult::SUCCESS: break; // no error + case CopyFileResult::FAIL_COPY_FILE: + throw Slic3r::ExportError((boost::format(_utf8(L("Copying of the temporary G-code to the output G-code failed. Maybe the SD card is write locked?\nError message: %1%"))) % error_message).str()); + break; + case CopyFileResult::FAIL_FILES_DIFFERENT: + throw Slic3r::ExportError((boost::format(_utf8(L("Copying of the temporary G-code to the output G-code failed. There might be problem with target device, please try exporting again or using different device. The corrupted output G-code is at %1%.tmp."))) % export_path).str()); + break; + case CopyFileResult::FAIL_RENAMING: + throw Slic3r::ExportError((boost::format(_utf8(L("Renaming of the G-code after copying to the selected destination folder has failed. Current path is %1%.tmp. Please try exporting again."))) % export_path).str()); + break; + case CopyFileResult::FAIL_CHECK_ORIGIN_NOT_OPENED: + throw Slic3r::ExportError((boost::format(_utf8(L("Copying of the temporary G-code has finished but the original code at %1% couldn't be opened during copy check. The output G-code is at %2%.tmp."))) % output_path % export_path).str()); + break; + case CopyFileResult::FAIL_CHECK_TARGET_NOT_OPENED: + throw Slic3r::ExportError((boost::format(_utf8(L("Copying of the temporary G-code has finished but the exported code couldn't be opened during copy check. The output G-code is at %1%.tmp."))) % export_path).str()); + break; + default: + throw Slic3r::ExportError(_utf8(L("Unknown error occured during exporting G-code."))); + BOOST_LOG_TRIVIAL(error) << "Unexpected fail code(" << (int)copy_ret_val << ") durring copy_file() to " << export_path << "."; + break; + } + m_print->set_status(100, (boost::format(_utf8(L("G-code file exported to %1%"))) % export_path).str()); +} + +// A print host upload job has been scheduled, enqueue it to the printhost job queue +void BackgroundSlicingProcess::prepare_upload() +{ // Generate a unique temp path to which the gcode/zip file is copied/exported boost::filesystem::path source_path = boost::filesystem::temp_directory_path() / boost::filesystem::unique_path("." SLIC3R_APP_KEY ".upload.%%%%-%%%%-%%%%-%%%%"); @@ -711,11 +728,15 @@ void BackgroundSlicingProcess::prepare_upload() if (m_print == m_fff_print) { m_print->set_status(95, _utf8(L("Running post-processing scripts"))); std::string error_message; - if (copy_file(m_temp_output_path, source_path.string(), error_message) != SUCCESS) { + if (copy_file(m_temp_output_path, source_path.string(), error_message) != SUCCESS) throw Slic3r::RuntimeError(_utf8(L("Copying of the temporary G-code to the output G-code failed"))); - } - run_post_process_scripts(source_path.string(), m_fff_print->full_print_config()); m_upload_job.upload_data.upload_path = m_fff_print->print_statistics().finalize_output_path(m_upload_job.upload_data.upload_path.string()); + // Make a copy of the source path, as run_post_process_scripts() is allowed to change it when making a copy of the source file + // (not here, but when the final target is a file). + std::string source_path_str = source_path.string(); + std::string output_name_str = m_upload_job.upload_data.upload_path.string(); + if (run_post_process_scripts(source_path_str, false, m_upload_job.printhost->get_name(), output_name_str, m_fff_print->full_print_config())) + m_upload_job.upload_data.upload_path = output_name_str; } else { m_upload_job.upload_data.upload_path = m_sla_print->print_statistics().finalize_output_path(m_upload_job.upload_data.upload_path.string()); diff --git a/src/slic3r/GUI/BackgroundSlicingProcess.hpp b/src/slic3r/GUI/BackgroundSlicingProcess.hpp index cf9b0724951..f87a58fd6be 100644 --- a/src/slic3r/GUI/BackgroundSlicingProcess.hpp +++ b/src/slic3r/GUI/BackgroundSlicingProcess.hpp @@ -216,9 +216,9 @@ class BackgroundSlicingProcess Print *m_fff_print = nullptr; SLAPrint *m_sla_print = nullptr; // Data structure, to which the G-code export writes its annotations. - GCodeProcessor::Result *m_gcode_result = nullptr; + GCodeProcessor::Result *m_gcode_result = nullptr; // Callback function, used to write thumbnails into gcode. - ThumbnailsGeneratorCallback m_thumbnail_cb = nullptr; + ThumbnailsGeneratorCallback m_thumbnail_cb = nullptr; SL1Archive m_sla_archive; // Temporary G-code, there is one defined for the BackgroundSlicingProcess, differentiated from the other processes by a process ID. std::string m_temp_output_path; @@ -262,6 +262,7 @@ class BackgroundSlicingProcess bool invalidate_all_steps(); // If the background processing stop was requested, throw CanceledException. void throw_if_canceled() const { if (m_print->canceled()) throw CanceledException(); } + void finalize_gcode(); void prepare_upload(); // To be executed at the background thread. ThumbnailsList render_thumbnails(const ThumbnailsParams ¶ms);