mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-11 01:41:03 +00:00
* Lock Config and Preset Files Across Instances and Write Them Atomically Every running instance shares one OrcaSlicer.conf and one user preset tree, and nothing kept their writers apart. Two instances saving at the same moment, or the cloud preset sync thread writing while the GUI thread saved, could interleave, and a reader in another instance could open a preset JSON or .info file between truncate and close and get a partial file, dropping that preset for the session with a parse error. Add InstanceLock, a scoped guard that serialises the threads of one process through a recursive mutex and other processes through an advisory OS file lock: flock on POSIX, held on the guard's own descriptor so no other close in the process can drop it, and LockFileEx on Windows. The outermost guard opens the lock file and closes it on release, so nothing stays open between saves and a data dir can be removed once nothing is saving into it; the file itself is kept, since deleting it would let a third instance lock a fresh file while the second still holds the old one. It is best effort: when the lock file cannot be opened or locked, or another instance still holds it after a second, the guard logs once and lets the write proceed, then leaves the file alone for ten seconds, so a hung instance never blocks every other one and a holder stuck in a debugger does not cost a stall per save. The guard sits at the leaf readers and writers: set_sync_info_and_save() calls save_info() under the preset collection mutex, so a batch lock around save_user_presets() would invert the order against the sync thread. The user preset scan reads its files on worker threads without the guard, since the mutex would serialise them, and takes it per file in the serial commit step, so a save never waits for the whole scan. Each read keeps the bytes of the preset and its .info as they were before parsing; commit compares them with the disk under the guard and reads a file that changed again, so it never deletes or writes back over another instance's newer save, nor installs a .json and .info from two different saves; a preset another instance removed in the meantime is not installed. Without the guard, in a cool-down, the scan still loads the presets but leaves their files alone: an unreadable file stays for the next scan, and a derived compatible printer is not written back. Read-only scans, which is what the CLI does, take no lock and create no lock file. AppConfig holds OrcaSlicer.conf.lock in load() and save(); load is included because the Windows path restores from the .bak copy. Every user preset writer and reader holds user.lock: Preset::save(), which writes no .info when the preset itself could not be written, since an .info without its preset reads as a cloud deletion request, save_info(), reload() and remove_files(), each preset the scan commits, the bundle metadata reads and write, the .info removal after a cloud-confirmed delete, the orphaned-.info scan on the sync thread, the bundle folder removal on unsubscribe and the physical printer writers and delete paths. A bundle import extracts under cache/ into a folder per process and per import, where no scan reads. Preset JSON, .info, bundle metadata, physical printer and config files, and the caches and state files that already used a temporary by hand, now go through write_file_atomically(), which writes <file>.<pid>.<n>.tmp beside the target and renames it over, so a reader that never waits sees a complete old or new file. A symlink is followed; a target that is not a regular file is written in place; and when no temporary can be created beside an existing target, or the rename itself is refused, by a Windows reader holding the file open or a mount that cannot replace in one step, the helper writes in place as before, since losing the save is worse than a torn read. On POSIX the rename replaces the target atomically where the old code removed it first and left a window with no file at all; only a mount that refuses a one-step replace gets the old remove-then-rename. A crash between temporary and rename leaves the temporary behind, which no scan reads. Preset::save() returns whether it wrote the preset, so the scan counts a compatible printer it could not write back as an error. A failed config write keeps the config dirty, and the idle handler waits ten seconds before retrying while an explicit save always tries. * Run the Cross-Process Lock Test on Every Platform The test that checks the guard yields to a lock held elsewhere forked a child to hold it, so it was left out on Windows. The OS lock belongs to the handle on Windows and to the open file description elsewhere, so a second handle in the same process is refused like another instance would be. The test now holds the lock that way and runs everywhere.
673 lines
24 KiB
C++
Executable File
673 lines
24 KiB
C++
Executable File
#include "3DPrinterOS.hpp"
|
|
|
|
#include <algorithm>
|
|
#include <sstream>
|
|
#include <system_error>
|
|
#include <exception>
|
|
#include <boost/format.hpp>
|
|
#include <boost/log/trivial.hpp>
|
|
#include <boost/property_tree/ptree.hpp>
|
|
#include <boost/property_tree/json_parser.hpp>
|
|
#include <boost/algorithm/string/predicate.hpp>
|
|
#include <nlohmann/json.hpp>
|
|
|
|
#include <wx/progdlg.h>
|
|
#include <wx/string.h>
|
|
#include <wx/event.h>
|
|
#include <wx/dialog.h>
|
|
#include <wx/radiobut.h>
|
|
|
|
#include "libslic3r/PrintConfig.hpp"
|
|
#include "libslic3r/Utils.hpp"
|
|
#include "slic3r/GUI/I18N.hpp"
|
|
#include "slic3r/GUI/GUI.hpp"
|
|
#include "slic3r/GUI/format.hpp"
|
|
#include "slic3r/GUI/GUI_Utils.hpp"
|
|
#include "slic3r/GUI/MsgDialog.hpp"
|
|
#include "slic3r/GUI/Widgets/ComboBox.hpp"
|
|
#include "slic3r/GUI/Widgets/Button.hpp"
|
|
#include "slic3r/GUI/GUI_App.hpp"
|
|
|
|
#include "Http.hpp"
|
|
#include <wx/busyinfo.h>
|
|
|
|
|
|
using json = nlohmann::json;
|
|
|
|
namespace fs = boost::filesystem;
|
|
namespace pt = boost::property_tree;
|
|
|
|
namespace {
|
|
|
|
class UploadOptionsDialog : public Slic3r::GUI::DPIDialog
|
|
{
|
|
public:
|
|
UploadOptionsDialog(wxWindow* parent,
|
|
const wxArrayString& cloud_projects,
|
|
const wxArrayString& cloud_printer_types,
|
|
const wxString preset_name)
|
|
: Slic3r::GUI::DPIDialog(parent,
|
|
wxID_ANY,
|
|
_L("3DPrinterOS Cloud upload options"),
|
|
wxDefaultPosition,
|
|
wxSize(100 * Slic3r::GUI::wxGetApp().em_unit(), -1),
|
|
wxDEFAULT_DIALOG_STYLE),
|
|
okButton(nullptr)
|
|
{
|
|
SetFont(Slic3r::GUI::wxGetApp().normal_font());
|
|
SetBackgroundColour(*wxWHITE);
|
|
SetForegroundColour(*wxBLACK);
|
|
|
|
singleRadio = new wxRadioButton(this, wxID_ANY, _L("Single file"), wxDefaultPosition, wxDefaultSize, wxRB_GROUP);
|
|
projectRadio = new wxRadioButton(this, wxID_ANY, _L("Project File"));
|
|
projectsLabel = new wxStaticText(this, wxID_ANY, _L("Project:"));
|
|
wxStaticText* printerLabel = new wxStaticText(this, wxID_ANY, _L("Printer type:"));
|
|
|
|
projectsComboBox = new wxComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, DD_NO_CHECK_ICON);
|
|
printerTypeComboBox = new wxComboBox(this, wxID_ANY, wxString(""), wxDefaultPosition, wxDefaultSize, 0, nullptr, DD_NO_CHECK_ICON | wxTE_READONLY);
|
|
|
|
printerWarningLabel = new wxStaticText(this, wxID_ANY, _L("Printer type not found, please select manually."));
|
|
printerWarningLabel->SetForegroundColour(*wxRED);
|
|
printerWarningLabel->Hide();
|
|
|
|
for (int i = 0; i < cloud_projects.size(); i++) {
|
|
projectsComboBox->Append(cloud_projects[i]);
|
|
}
|
|
if (cloud_printer_types.size() > 0) {
|
|
for (int i = 0; i < cloud_printer_types.size(); i++) {
|
|
printerTypeComboBox->Append(cloud_printer_types[i]);
|
|
if (cloud_printer_types[i].Find(preset_name) != wxNOT_FOUND && printerTypeComboBox->GetSelection() == -1) {
|
|
printerTypeComboBox->SetSelection(i);
|
|
}
|
|
}
|
|
if (printerTypeComboBox->GetCount() > 1) {
|
|
printerWarningLabel->Show();
|
|
} else {
|
|
printerTypeComboBox->SetSelection(0);
|
|
}
|
|
}
|
|
|
|
okButton = new wxButton(this, wxID_OK, _L("OK"));
|
|
wxButton* cancelButton = new wxButton(this, wxID_CANCEL, _L("Cancel"));
|
|
|
|
wxBoxSizer* radioSizer = new wxBoxSizer(wxHORIZONTAL);
|
|
wxBoxSizer* btnSizer = new wxBoxSizer(wxHORIZONTAL);
|
|
radioSizer->Add(singleRadio, 0, wxALL, 5);
|
|
radioSizer->Add(projectRadio, 0, wxALL, 5);
|
|
btnSizer->Add(okButton, 0, wxALL | wxALIGN_CENTER, 5);
|
|
btnSizer->Add(cancelButton, 0, wxALL | wxALIGN_CENTER, 5);
|
|
|
|
wxBoxSizer* sizer = new wxBoxSizer(wxVERTICAL);
|
|
sizer->Add(radioSizer, 0, wxALL, 5);
|
|
sizer->Add(projectsLabel, 0, wxALL, 5);
|
|
sizer->Add(projectsComboBox, 0, wxALL | wxEXPAND, 5);
|
|
sizer->Add(printerLabel, 0, wxALL, 5);
|
|
sizer->Add(printerTypeComboBox, 0, wxALL | wxEXPAND, 5);
|
|
sizer->Add(printerWarningLabel, 0, wxLEFT | wxRIGHT | wxBOTTOM, 5);
|
|
sizer->Add(btnSizer, 0, wxALL | wxALIGN_CENTER, 5);
|
|
SetSizer(sizer);
|
|
sizer->Fit(this);
|
|
projectsComboBox->Hide();
|
|
projectsLabel->Hide();
|
|
projectRadio->Bind(wxEVT_RADIOBUTTON, &UploadOptionsDialog::OnRadioButtonSelected, this);
|
|
singleRadio->Bind(wxEVT_RADIOBUTTON, &UploadOptionsDialog::OnRadioButtonSelected, this);
|
|
// Bind combo box selection change to validation
|
|
printerTypeComboBox->Bind(wxEVT_COMBOBOX, &UploadOptionsDialog::OnPrinterTypeChanged, this);
|
|
ValidateOkButton(); // Initial validation
|
|
Slic3r::GUI::wxGetApp().UpdateDlgDarkUI(this);
|
|
|
|
CenterOnParent();
|
|
}
|
|
|
|
void OnRadioButtonSelected(wxCommandEvent& event)
|
|
{
|
|
wxRadioButton* selectedRadio = dynamic_cast<wxRadioButton*>(event.GetEventObject());
|
|
if (selectedRadio) {
|
|
wxString label = selectedRadio->GetLabel();
|
|
if (label == _L("Project File")) {
|
|
projectsComboBox->Show();
|
|
projectsLabel->Show();
|
|
} else {
|
|
projectsComboBox->Hide();
|
|
projectsLabel->Hide();
|
|
}
|
|
Layout();
|
|
}
|
|
}
|
|
|
|
void on_dpi_changed(const wxRect& suggested_rect) {}
|
|
|
|
void OnPrinterTypeChanged(wxCommandEvent& event)
|
|
{
|
|
ValidateOkButton();
|
|
event.Skip();
|
|
}
|
|
|
|
void ValidateOkButton()
|
|
{
|
|
bool hasSelection = (printerTypeComboBox->GetSelection() != wxNOT_FOUND);
|
|
okButton->Enable(hasSelection);
|
|
}
|
|
|
|
void GetValues(std::string& project, std::string& printer_type)
|
|
{
|
|
project = projectRadio->GetValue() ? std::string(projectsComboBox->GetValue().c_str()) : "";
|
|
printer_type = std::string(printerTypeComboBox->GetValue().c_str());
|
|
}
|
|
|
|
private:
|
|
wxComboBox* projectsComboBox;
|
|
wxComboBox* printerTypeComboBox;
|
|
wxStaticText* projectsLabel;
|
|
wxStaticText* printerWarningLabel;
|
|
wxRadioButton* singleRadio;
|
|
wxRadioButton* projectRadio;
|
|
wxButton* okButton;
|
|
};
|
|
|
|
|
|
class TokenAuthDialog : public Slic3r::GUI::DPIDialog
|
|
{
|
|
public:
|
|
TokenAuthDialog(wxWindow* parent, const std::string &url, const std::string& token, const std::string &cafile, pt::ptree& resp)
|
|
: Slic3r::GUI::DPIDialog(parent,
|
|
wxID_ANY,
|
|
"3DPrinterOS",
|
|
wxDefaultPosition,
|
|
wxSize(45 * Slic3r::GUI::wxGetApp().em_unit(), -1),
|
|
wxDEFAULT_DIALOG_STYLE)
|
|
, m_url(url)
|
|
, m_token(token)
|
|
, m_cafile(cafile)
|
|
, m_resp(resp)
|
|
{
|
|
SetFont(Slic3r::GUI::wxGetApp().normal_font());
|
|
SetBackgroundColour(*wxWHITE);
|
|
SetForegroundColour(*wxBLACK);
|
|
|
|
auto* sizer = new wxBoxSizer(wxVERTICAL);
|
|
sizer->Add(new wxStaticText(this, wxID_ANY, _L("Authorizing...")), 1, wxALL | wxCENTER, 10);
|
|
auto* cancelBtn = new wxButton(this, wxID_CANCEL, _L("Cancel"));
|
|
sizer->Add(cancelBtn, 0, wxALL | wxALIGN_CENTER, 10);
|
|
SetSizerAndFit(sizer);
|
|
|
|
Bind(wxEVT_THREAD, [this](wxThreadEvent& e) { EndModal(e.GetId()); });
|
|
Bind(wxEVT_TIMER, &TokenAuthDialog::OnRetry, this);
|
|
Bind(wxEVT_SHOW, &TokenAuthDialog::OnShow, this);
|
|
Bind(wxEVT_BUTTON, &TokenAuthDialog::OnCancel, this, wxID_CANCEL);
|
|
|
|
m_timer.SetOwner(this);
|
|
Slic3r::GUI::wxGetApp().UpdateDlgDarkUI(this);
|
|
CenterOnParent();
|
|
}
|
|
|
|
void on_dpi_changed(const wxRect& suggested_rect) {}
|
|
|
|
private:
|
|
|
|
void OnShow(wxShowEvent& event)
|
|
{
|
|
if (event.IsShown() && !m_started) {
|
|
m_started = true;
|
|
SendRequest();
|
|
}
|
|
event.Skip();
|
|
}
|
|
|
|
void OnCancel(wxCommandEvent&)
|
|
{
|
|
m_cancelled = true;
|
|
if (m_http_ptr) {
|
|
m_http_ptr->cancel(); // abort the background request
|
|
}
|
|
EndModal(wxID_CANCEL);
|
|
}
|
|
|
|
void OnRetry(wxTimerEvent&) { SendRequest(); }
|
|
|
|
void SendRequest()
|
|
{
|
|
if (m_cancelled || m_attempt >= m_max_retries) {
|
|
if (m_attempt >= m_max_retries) {
|
|
m_resp.put("result", false);
|
|
m_resp.put("message", "Maximum login retries exceeded");
|
|
}
|
|
wxQueueEvent(this, new wxThreadEvent(wxEVT_THREAD, wxID_ABORT));
|
|
return;
|
|
}
|
|
|
|
m_attempt++;
|
|
std::string postBody = "token=" + m_token;
|
|
auto http = Slic3r::Http::post(m_url);
|
|
http.timeout_max(60);
|
|
if (!m_cafile.empty()) {
|
|
http.ca_file(m_cafile);
|
|
}
|
|
http.header("Content-Length", std::to_string(postBody.size()));
|
|
http.set_post_body(postBody);
|
|
http.on_error([this](std::string, std::string error, unsigned status) {
|
|
if (!m_cancelled) {
|
|
m_resp.put("result", false);
|
|
m_resp.put("message", (status != 200) ? "HTTP error: " + std::to_string(status) : error);
|
|
wxQueueEvent(this, new wxThreadEvent(wxEVT_THREAD, wxID_ABORT));
|
|
}
|
|
})
|
|
.on_complete([this](std::string body, unsigned status) {
|
|
if (!m_cancelled) {
|
|
if (status != 200) {
|
|
m_resp.put("result", false);
|
|
m_resp.put("message", "HTTP error: " + std::to_string(status));
|
|
wxQueueEvent(this, new wxThreadEvent(wxEVT_THREAD, wxID_ABORT));
|
|
return;
|
|
}
|
|
|
|
try {
|
|
std::stringstream ss(body);
|
|
pt::read_json(ss, m_resp);
|
|
} catch (...) {
|
|
m_resp.put("result", false);
|
|
m_resp.put("message", "Could not parse server response");
|
|
}
|
|
|
|
if (m_resp.get<bool>("result", false) && m_resp.get_optional<std::string>("message.session").has_value()) {
|
|
wxQueueEvent(this, new wxThreadEvent(wxEVT_THREAD, wxID_OK));
|
|
} else if (m_resp.get<bool>("result", false)) {
|
|
if (m_attempt < m_max_retries)
|
|
m_timer.StartOnce(m_retry_delay_ms);
|
|
else
|
|
wxQueueEvent(this, new wxThreadEvent(wxEVT_THREAD, wxID_ABORT));
|
|
} else {
|
|
wxQueueEvent(this, new wxThreadEvent(wxEVT_THREAD, wxID_ABORT));
|
|
}
|
|
}
|
|
});
|
|
m_http_ptr = http.perform();
|
|
}
|
|
|
|
private:
|
|
std::string m_token;
|
|
std::string m_url;
|
|
std::string m_cafile;
|
|
pt::ptree& m_resp;
|
|
wxTimer m_timer;
|
|
std::shared_ptr<Slic3r::Http> m_http_ptr;
|
|
bool m_cancelled{false};
|
|
bool m_started{false};
|
|
int m_attempt{0};
|
|
const int m_max_retries{10};
|
|
const int m_retry_delay_ms{500};
|
|
};
|
|
} // namespace
|
|
|
|
namespace Slic3r {
|
|
|
|
static const std::string API_CREDENTIALS_PATH = "3dprinteros_api_cred.json";
|
|
|
|
C3DPrinterOS::C3DPrinterOS(DynamicPrintConfig *config)
|
|
: m_host(config->opt_string("print_host"))
|
|
, m_apikey(config->opt_string("printhost_apikey"))
|
|
, m_preset_name(config->opt_string("printer_model"))
|
|
{
|
|
m_api_session_file_path = (boost::filesystem::path(Slic3r::data_dir()) / API_CREDENTIALS_PATH)
|
|
.make_preferred()
|
|
.string();
|
|
load_api_session();
|
|
}
|
|
|
|
const char *C3DPrinterOS::get_name() const { return "3DPrinterOS"; }
|
|
|
|
bool C3DPrinterOS::test(wxString &msg) const
|
|
{
|
|
return check_session(msg);
|
|
}
|
|
|
|
bool C3DPrinterOS::login(wxString& msg) const
|
|
{
|
|
// Get token for auth
|
|
msg.clear();
|
|
std::string token = get_api_auth_token(msg);
|
|
if (token.empty()) {
|
|
msg = _L("Error. Can't get API token for authorization");
|
|
return false;
|
|
}
|
|
|
|
auto login_url = make_url("noauth/apiglobal_login_with_token/" + token);
|
|
wxLaunchDefaultBrowser(login_url);
|
|
pt::ptree login_resp;
|
|
login_with_token(login_resp, token);
|
|
std::string session, email;
|
|
try {
|
|
if (login_resp.get<bool>("result")) {
|
|
session = login_resp.get<std::string>("message.session");
|
|
email = login_resp.get<std::string>("message.email");
|
|
} else {
|
|
msg = wxString(login_resp.get<std::string>("message").c_str());
|
|
return false;
|
|
}
|
|
} catch (const std::exception&) {
|
|
msg = _L("Could not parse server response.");
|
|
return false;
|
|
}
|
|
bool res = save_api_session(session, email);
|
|
if (!res) {
|
|
msg = _L("Error saving session to file");
|
|
}
|
|
return res;
|
|
}
|
|
|
|
wxString C3DPrinterOS::get_test_ok_msg() const
|
|
{
|
|
return _("Connection to 3DPrinterOS cloud works correctly.") + (!m_username.empty() ? "" + _(" Logined as user: ") + m_username : "");
|
|
}
|
|
|
|
wxString C3DPrinterOS::get_test_failed_msg(wxString &msg) const
|
|
{
|
|
return GUI::format_wxstr("%s: %s\n\n", _L("Error session check"), msg);
|
|
}
|
|
|
|
bool C3DPrinterOS::upload(
|
|
PrintHostUpload upload_data, ProgressFn prorgess_fn, ErrorFn error_fn, InfoFn info_fn
|
|
) const
|
|
{
|
|
const char *name = get_name();
|
|
const auto upload_filename = upload_data.upload_path.filename();
|
|
const auto upload_parent_path = upload_data.upload_path.parent_path();
|
|
wxString test_msg;
|
|
if (!check_session(test_msg)) {
|
|
error_fn(std::move(test_msg));
|
|
return false;
|
|
}
|
|
|
|
pt::ptree cloud_project_resp;
|
|
pt::ptree cloud_printer_types_resp;
|
|
|
|
get_cloud_projects_list(cloud_project_resp);
|
|
get_cloud_printer_types(cloud_printer_types_resp, m_preset_name);
|
|
wxArrayString cloud_projects_list;
|
|
wxArrayString cloud_printer_types_list;
|
|
|
|
try {
|
|
if (cloud_project_resp.get<bool>("result")) {
|
|
for (const auto &messageItem : cloud_project_resp.get_child("message")) {
|
|
cloud_projects_list.Add(messageItem.second.get<std::string>("name"));
|
|
}
|
|
}
|
|
|
|
if (cloud_printer_types_resp.get<bool>("result")) {
|
|
for (const auto &messageItem : cloud_printer_types_resp.get_child("message")) {
|
|
cloud_printer_types_list.Add(messageItem.second.get<std::string>("description"));
|
|
}
|
|
}
|
|
} catch (const std::exception &) {
|
|
error_fn(_L("Could not parse server response."));
|
|
return false;
|
|
}
|
|
|
|
// Show "Confirm cloud printer type and project for 3DPrinterOS upload
|
|
|
|
UploadOptionsDialog dlg(GUI::wxGetApp().GetTopWindow(), cloud_projects_list, cloud_printer_types_list, m_preset_name);
|
|
|
|
if (dlg.ShowModal() != wxID_OK) {
|
|
error_fn(_L("Canceled"));
|
|
return false;
|
|
}
|
|
|
|
std::string selected_project;
|
|
std::string selected_printer_type;
|
|
dlg.GetValues(selected_project, selected_printer_type);
|
|
std::string project_id;
|
|
std::string printer_type_id;
|
|
|
|
// search for cloud project_id by name
|
|
if (!selected_project.empty()) {
|
|
for (const auto& messageItem : cloud_project_resp.get_child("message")) {
|
|
if (messageItem.second.get<std::string>("name", "") == selected_project) {
|
|
project_id = messageItem.second.get<std::string>("id", "");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// search for cloud printer_type_id by name
|
|
for (const auto& messageItem : cloud_printer_types_resp.get_child("message")) {
|
|
if (messageItem.second.get<std::string>("description", "") == selected_printer_type) {
|
|
printer_type_id = messageItem.second.get<std::string>("id", "");
|
|
break;
|
|
}
|
|
}
|
|
|
|
bool res = true;
|
|
auto url = make_url("apiglobal/upload");
|
|
std::string file_id;
|
|
pt::ptree uploadResponse;
|
|
auto http = Http::post(std::move(url));
|
|
if (!m_cafile.empty()) {
|
|
http.ca_file(m_cafile);
|
|
}
|
|
http.form_add("session", m_apikey)
|
|
.form_add("upload_type_id", "7")
|
|
.form_add("upload_soft_name", "OrcaSlicer")
|
|
.form_add("zip", "false")
|
|
.form_add_file("file", upload_data.source_path.string(), upload_filename.string());
|
|
|
|
if (!project_id.empty()) {
|
|
http.form_add("project_id", project_id);
|
|
} else if (!selected_project.empty()) {
|
|
http.form_add("project_name", selected_project);
|
|
http.form_add("project_color", "grey");
|
|
}
|
|
|
|
http.on_complete([&](std::string body, unsigned status) {
|
|
std::stringstream ss(body);
|
|
try {
|
|
pt::read_json(ss, uploadResponse);
|
|
} catch (const std::exception &) {
|
|
uploadResponse.put("result", false);
|
|
uploadResponse.put("message", "Could not parse server response");
|
|
}
|
|
})
|
|
.on_error([&](std::string body, std::string error, unsigned status) {
|
|
error_fn(format_error(body, error, status));
|
|
res = false;
|
|
})
|
|
.on_progress([&](Http::Progress progress, bool &cancel) {
|
|
prorgess_fn(std::move(progress), cancel);
|
|
if (cancel) {
|
|
res = false;
|
|
}
|
|
})
|
|
.perform_sync();
|
|
|
|
try {
|
|
if (uploadResponse.get<bool>("result")) {
|
|
file_id = uploadResponse.get<std::string>("message.file_id");
|
|
} else {
|
|
res = false;
|
|
error_fn(uploadResponse.get<std::string>("message"));
|
|
}
|
|
} catch (const std::exception &) {
|
|
res = false;
|
|
error_fn(_L("Error during file upload"));
|
|
}
|
|
// set printer type for uploaded gcode
|
|
if (res) {
|
|
pt::ptree update_file_response;
|
|
update_file(update_file_response, file_id, printer_type_id, "OrcaSlicer");
|
|
try {
|
|
if (!update_file_response.get<bool>("result")) {
|
|
const std::string msg = update_file_response.get<std::string>("message", "Unknown update error");
|
|
BOOST_LOG_TRIVIAL(warning) << "Failed to update uploaded file: " << msg;
|
|
}
|
|
} catch (const std::exception& ex) {
|
|
BOOST_LOG_TRIVIAL(warning) << "Could not parse update response: " << ex.what();
|
|
}
|
|
if (upload_data.post_action == PrintHostPostUploadAction::StartPrint && !upload_data.use_3mf) {
|
|
auto quick_print_url = make_url("quickprint?file_id=" + file_id);
|
|
wxLaunchDefaultBrowser(quick_print_url);
|
|
}
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
void C3DPrinterOS::log_out() const
|
|
{
|
|
boost::filesystem::remove(m_api_session_file_path.c_str());
|
|
}
|
|
|
|
bool C3DPrinterOS::validate_version_text(const boost::optional<std::string> &version_text) const
|
|
{
|
|
return version_text ? boost::starts_with(*version_text, "3DPrinterOS") : true;
|
|
}
|
|
|
|
std::string C3DPrinterOS::make_url(const std::string &path) const
|
|
{
|
|
if (m_host.find("http://") == 0 || m_host.find("https://") == 0) {
|
|
if (m_host.back() == '/') {
|
|
return (boost::format("%1%%2%") % m_host % path).str();
|
|
} else {
|
|
return (boost::format("%1%/%2%") % m_host % path).str();
|
|
}
|
|
} else {
|
|
return (boost::format("https://%1%/%2%") % m_host % path).str();
|
|
}
|
|
}
|
|
|
|
std::string C3DPrinterOS::get_api_auth_token(wxString &err) const
|
|
{
|
|
std::string result;
|
|
pt::ptree resp;
|
|
std::string postBody = "app_type=plugin&app_name=" + Http::url_encode("OrcaSlicer");
|
|
send_form("apiglobal/generate_login_token", postBody, resp);
|
|
try {
|
|
if (resp.get<bool>("result")) {
|
|
result = resp.get<std::string>("message");
|
|
} else {
|
|
err = wxString(resp.get<std::string>("message").c_str());
|
|
}
|
|
} catch (const std::exception &) {
|
|
err = _L("Could not parse server response.");
|
|
}
|
|
return result;
|
|
}
|
|
|
|
void C3DPrinterOS::login_with_token(pt::ptree &resp, const std::string &token) const {
|
|
auto url = make_url("apiglobal/login_with_token");
|
|
TokenAuthDialog dlg(GUI::wxGetApp().GetTopWindow(), url, token, m_cafile, resp);
|
|
dlg.ShowModal();
|
|
}
|
|
|
|
bool C3DPrinterOS::check_session(wxString &msg) const {
|
|
std::string postBody = "session=" + m_apikey;
|
|
pt::ptree resp;
|
|
send_form("apiglobal/check_session", postBody, resp);
|
|
try {
|
|
if (resp.get<bool>("result")) {
|
|
return true;
|
|
} else {
|
|
msg = wxString(resp.get<std::string>("message").c_str());
|
|
return false;
|
|
}
|
|
|
|
} catch (const std::exception &) {
|
|
msg = _L("Could not parse server response.");
|
|
return false;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool C3DPrinterOS::save_api_session(const std::string &session, const std::string &email) const {
|
|
pt::ptree j;
|
|
j.put("session", session);
|
|
j.put("email", email);
|
|
try {
|
|
std::ostringstream json;
|
|
pt::write_json(json, j);
|
|
if (const std::error_code ec = write_file_atomically(m_api_session_file_path, json.str()))
|
|
throw std::system_error(ec);
|
|
} catch (const std::exception &err) {
|
|
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to write json to file. Path = "
|
|
<< m_api_session_file_path
|
|
<< " Reason = " << err.what();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void C3DPrinterOS::load_api_session()
|
|
{
|
|
m_apikey.clear();
|
|
if (boost::filesystem::exists(m_api_session_file_path)) {
|
|
pt::ptree j;
|
|
try {
|
|
pt::read_json(m_api_session_file_path, j);
|
|
m_apikey = j.get<std::string>("session");
|
|
m_username = j.get<std::string>("email");
|
|
} catch (const std::exception &err) {
|
|
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": load_api_session failed, reason = " << err.what();
|
|
// remove corrupted file to avoid repeated failures
|
|
try {
|
|
boost::filesystem::remove(m_api_session_file_path);
|
|
} catch (...) {}
|
|
}
|
|
};
|
|
}
|
|
|
|
void C3DPrinterOS::send_form(
|
|
const std::string &endpoint,
|
|
const std::string &postBody,
|
|
boost::property_tree::ptree &responseTree
|
|
) const
|
|
{
|
|
responseTree.clear();
|
|
auto url = make_url(endpoint);
|
|
auto http = Http::post(std::move(url));
|
|
if (!m_cafile.empty()) {
|
|
http.ca_file(m_cafile);
|
|
}
|
|
http.header("Content-length", std::to_string(postBody.size()));
|
|
http.set_post_body(postBody);
|
|
http.on_error([&](std::string body, std::string error, unsigned status) {
|
|
BOOST_LOG_TRIVIAL(error) << boost::format("Error sending form: %1%") % error;
|
|
responseTree.put("result", false);
|
|
responseTree.put("message", error);
|
|
})
|
|
.on_complete([&](std::string body, unsigned) {
|
|
std::stringstream ss(body);
|
|
try {
|
|
pt::read_json(ss, responseTree);
|
|
} catch (const std::exception &) {
|
|
responseTree.put("result", false);
|
|
responseTree.put("message", "Could not parse server response");
|
|
}
|
|
})
|
|
.perform_sync();
|
|
}
|
|
|
|
void C3DPrinterOS::get_cloud_projects_list(boost::property_tree::ptree &response) const
|
|
{
|
|
std::string postBody = std::string("session=" + m_apikey);
|
|
send_form("apiglobal/get_projects", postBody, response);
|
|
}
|
|
|
|
void C3DPrinterOS::get_cloud_printer_types(boost::property_tree::ptree &response, const std::string &query) const
|
|
{
|
|
std::string postBody = std::string("session=" + m_apikey);
|
|
if (!query.empty()) {
|
|
postBody += "&description=" + Http::url_encode(query) + "&software_version=" + Http::url_encode("OrcaSlicer");
|
|
}
|
|
send_form("apiglobal/get_printer_types", postBody, response);
|
|
}
|
|
|
|
void C3DPrinterOS::update_file(boost::property_tree::ptree &response, const std::string &file_id, const std::string &ptype, const std::string >ype) const
|
|
{
|
|
std::string postBody = "session=" + m_apikey
|
|
+ "&updates[" + file_id + "][ptype]=" + ptype
|
|
+ "&updates[" + file_id + "][gtype]=" + Http::url_encode(gtype)
|
|
+ "&updates[" + file_id + "][zip]=false";
|
|
send_form("apiglobal/file_update", postBody, response);
|
|
}
|
|
|
|
};
|
|
// namespace Slic3r
|