Http: added support for retry with exponentail backoff, UserAccountCommunication::on_active triggered from app instrad of panel to correctly handle situation when multiple app windows opened, ConnectWebView: refactored the login script to prevent using old token

This commit is contained in:
Jan Bařtipán 2024-08-19 10:02:03 +02:00 committed by Lukas Matena
parent ac859d711c
commit 54baccd6fc
10 changed files with 127 additions and 65 deletions

View File

@ -1448,6 +1448,10 @@ bool GUI_App::on_init_inner()
this->check_updates(false); this->check_updates(false);
}); });
Bind(wxEVT_ACTIVATE_APP, [this](const wxActivateEvent &evt) {
plater_->get_user_account()->on_activate_app(evt.GetActive());
});
} }
else { else {
#ifdef __WXMSW__ #ifdef __WXMSW__

View File

@ -6473,7 +6473,6 @@ void Plater::force_print_bed_update()
void Plater::on_activate(bool active) void Plater::on_activate(bool active)
{ {
this->p->user_account->on_activate_window(active);
if (active) { if (active) {
this->p->show_delayed_error_message(); this->p->show_delayed_error_message();
} }

View File

@ -60,7 +60,7 @@ public:
bool on_connect_printers_success(const std::string& data, AppConfig* app_config, bool& out_printers_changed); bool on_connect_printers_success(const std::string& data, AppConfig* app_config, bool& out_printers_changed);
bool on_connect_uiid_map_success(const std::string& data, AppConfig* app_config, bool& out_printers_changed); bool on_connect_uiid_map_success(const std::string& data, AppConfig* app_config, bool& out_printers_changed);
void on_activate_window(bool active) { m_communication->on_activate_window(active); } void on_activate_app(bool active) { m_communication->on_activate_app(active); }
std::string get_username() const { return m_username; } std::string get_username() const { return m_username; }
std::string get_access_token(); std::string get_access_token();

View File

@ -475,6 +475,11 @@ void UserAccountCommunication::enqueue_refresh()
BOOST_LOG_TRIVIAL(error) << "Connect Printers endpoint connection failed - Not Logged in."; BOOST_LOG_TRIVIAL(error) << "Connect Printers endpoint connection failed - Not Logged in.";
return; return;
} }
if (m_session->is_enqueued(UserAccountActionID::USER_ACCOUNT_ACTION_REFRESH_TOKEN))
{
BOOST_LOG_TRIVIAL(debug) << "User Account: Token refresh already enqueued, skipping...";
return;
}
m_session->enqueue_refresh({}); m_session->enqueue_refresh({});
} }
wakeup_session_thread(); wakeup_session_thread();
@ -506,7 +511,7 @@ void UserAccountCommunication::init_session_thread()
}); });
} }
void UserAccountCommunication::on_activate_window(bool active) void UserAccountCommunication::on_activate_app(bool active)
{ {
{ {
std::lock_guard<std::mutex> lck(m_thread_stop_mutex); std::lock_guard<std::mutex> lck(m_thread_stop_mutex);
@ -514,12 +519,11 @@ void UserAccountCommunication::on_activate_window(bool active)
} }
auto now = std::time(nullptr); auto now = std::time(nullptr);
BOOST_LOG_TRIVIAL(info) << "UserAccountCommunication activate: active " << active; BOOST_LOG_TRIVIAL(info) << "UserAccountCommunication activate: active " << active;
if (active && m_next_token_refresh_at - now < 60) { if (active && m_next_token_refresh_at > 0 && m_next_token_refresh_at - now < 60) {
BOOST_LOG_TRIVIAL(info) << "Enqueue access token refresh on activation"; BOOST_LOG_TRIVIAL(info) << "Enqueue access token refresh on activation";
enqueue_refresh();
m_token_timer->Stop(); m_token_timer->Stop();
enqueue_refresh();
} }
} }
void UserAccountCommunication::wakeup_session_thread() void UserAccountCommunication::wakeup_session_thread()

View File

@ -60,7 +60,7 @@ public:
// Exchanges code for tokens and shared_session_key // Exchanges code for tokens and shared_session_key
void on_login_code_recieved(const std::string& url_message); void on_login_code_recieved(const std::string& url_message);
void on_activate_window(bool active); void on_activate_app(bool active);
void set_username(const std::string& username); void set_username(const std::string& username);
void set_remember_session(bool b); void set_remember_session(bool b);
@ -97,7 +97,7 @@ private:
wxTimer* m_token_timer; wxTimer* m_token_timer;
wxEvtHandler* m_timer_evt_handler; wxEvtHandler* m_timer_evt_handler;
std::time_t m_next_token_refresh_at; std::time_t m_next_token_refresh_at{0};
void wakeup_session_thread(); void wakeup_session_thread();
void init_session_thread(); void init_session_thread();

View File

@ -46,7 +46,7 @@ void UserActionPost::perform(/*UNUSED*/ wxEvtHandler* evt_handler, /*UNUSED*/ co
if (success_callback) if (success_callback)
success_callback(body); success_callback(body);
}); });
http.perform_sync(); http.perform_sync(HttpRetryOpt::default_retry());
} }
void UserActionGetWithEvent::perform(wxEvtHandler* evt_handler, const std::string& access_token, UserActionSuccessFn success_callback, UserActionFailFn fail_callback, const std::string& input) const void UserActionGetWithEvent::perform(wxEvtHandler* evt_handler, const std::string& access_token, UserActionSuccessFn success_callback, UserActionFailFn fail_callback, const std::string& input) const
@ -69,9 +69,17 @@ void UserActionGetWithEvent::perform(wxEvtHandler* evt_handler, const std::strin
wxQueueEvent(evt_handler, new UserAccountSuccessEvent(succ_evt_type, body)); wxQueueEvent(evt_handler, new UserAccountSuccessEvent(succ_evt_type, body));
}); });
http.perform_sync(); http.perform_sync(HttpRetryOpt::default_retry());
} }
bool UserAccountSession::is_enqueued(UserAccountActionID action_id) const {
return std::any_of(
std::begin(m_priority_action_queue), std::end(m_priority_action_queue),
[action_id](const ActionQueueData& item) { return item.action_id == action_id; }
);
}
void UserAccountSession::process_action_queue() void UserAccountSession::process_action_queue()
{ {
if (!m_proccessing_enabled) if (!m_proccessing_enabled)
@ -84,7 +92,7 @@ void UserAccountSession::process_action_queue()
while (!m_priority_action_queue.empty()) { while (!m_priority_action_queue.empty()) {
m_actions[m_priority_action_queue.front().action_id]->perform(p_evt_handler, m_access_token, m_priority_action_queue.front().success_callback, m_priority_action_queue.front().fail_callback, m_priority_action_queue.front().input); m_actions[m_priority_action_queue.front().action_id]->perform(p_evt_handler, m_access_token, m_priority_action_queue.front().success_callback, m_priority_action_queue.front().fail_callback, m_priority_action_queue.front().input);
if (!m_priority_action_queue.empty()) if (!m_priority_action_queue.empty())
m_priority_action_queue.pop(); m_priority_action_queue.pop_front();
} }
// regular queue has to wait until priority fills tokens // regular queue has to wait until priority fills tokens
if (!this->is_initialized()) if (!this->is_initialized())
@ -115,7 +123,7 @@ void UserAccountSession::init_with_code(const std::string& code, const std::stri
m_proccessing_enabled = true; m_proccessing_enabled = true;
// fail fn might be cancel_queue here // fail fn might be cancel_queue here
m_priority_action_queue.push({ UserAccountActionID::USER_ACCOUNT_ACTION_CODE_FOR_TOKEN m_priority_action_queue.push_back({ UserAccountActionID::USER_ACCOUNT_ACTION_CODE_FOR_TOKEN
, std::bind(&UserAccountSession::token_success_callback, this, std::placeholders::_1) , std::bind(&UserAccountSession::token_success_callback, this, std::placeholders::_1)
, std::bind(&UserAccountSession::code_exchange_fail_callback, this, std::placeholders::_1) , std::bind(&UserAccountSession::code_exchange_fail_callback, this, std::placeholders::_1)
, post_fields }); , post_fields });
@ -188,7 +196,7 @@ void UserAccountSession::enqueue_test_with_refresh()
{ {
// on test fail - try refresh // on test fail - try refresh
m_proccessing_enabled = true; m_proccessing_enabled = true;
m_priority_action_queue.push({ UserAccountActionID::USER_ACCOUNT_ACTION_TEST_ACCESS_TOKEN, nullptr, std::bind(&UserAccountSession::enqueue_refresh, this, std::placeholders::_1), {} }); m_priority_action_queue.push_back({ UserAccountActionID::USER_ACCOUNT_ACTION_TEST_ACCESS_TOKEN, nullptr, std::bind(&UserAccountSession::enqueue_refresh, this, std::placeholders::_1), {} });
} }
@ -199,7 +207,7 @@ void UserAccountSession::enqueue_refresh(const std::string& body)
"&client_id=" + client_id() + "&client_id=" + client_id() +
"&refresh_token=" + m_refresh_token; "&refresh_token=" + m_refresh_token;
m_priority_action_queue.push({ UserAccountActionID::USER_ACCOUNT_ACTION_REFRESH_TOKEN m_priority_action_queue.push_back({ UserAccountActionID::USER_ACCOUNT_ACTION_REFRESH_TOKEN
, std::bind(&UserAccountSession::token_success_callback, this, std::placeholders::_1) , std::bind(&UserAccountSession::token_success_callback, this, std::placeholders::_1)
, std::bind(&UserAccountSession::refresh_fail_callback, this, std::placeholders::_1) , std::bind(&UserAccountSession::refresh_fail_callback, this, std::placeholders::_1)
, post_fields }); , post_fields });
@ -218,9 +226,7 @@ void UserAccountSession::refresh_fail_callback(const std::string& body)
void UserAccountSession::cancel_queue() void UserAccountSession::cancel_queue()
{ {
while (!m_priority_action_queue.empty()) { m_priority_action_queue.clear();
m_priority_action_queue.pop();
}
while (!m_action_queue.empty()) { while (!m_action_queue.empty()) {
m_action_queue.pop(); m_action_queue.pop();
} }

View File

@ -150,7 +150,8 @@ public:
void enqueue_refresh(const std::string& body); void enqueue_refresh(const std::string& body);
void process_action_queue(); void process_action_queue();
bool is_initialized() { return !m_access_token.empty() || !m_refresh_token.empty(); } bool is_initialized() const { return !m_access_token.empty() || !m_refresh_token.empty(); }
bool is_enqueued(UserAccountActionID action_id) const;
std::string get_access_token() const { return m_access_token; } std::string get_access_token() const { return m_access_token; }
std::string get_refresh_token() const { return m_refresh_token; } std::string get_refresh_token() const { return m_refresh_token; }
std::string get_shared_session_key() const { return m_shared_session_key; } std::string get_shared_session_key() const { return m_shared_session_key; }
@ -179,7 +180,7 @@ private:
long long m_next_token_timeout; long long m_next_token_timeout;
std::queue<ActionQueueData> m_action_queue; std::queue<ActionQueueData> m_action_queue;
std::queue<ActionQueueData> m_priority_action_queue; std::deque<ActionQueueData> m_priority_action_queue;
std::map<UserAccountActionID, std::unique_ptr<UserAction>> m_actions; std::map<UserAccountActionID, std::unique_ptr<UserAction>> m_actions;
wxEvtHandler* p_evt_handler; wxEvtHandler* p_evt_handler;

View File

@ -636,33 +636,9 @@ wxString ConnectWebViewPanel::get_login_script(bool refresh)
window.__access_token_version = 0; window.__access_token_version = 0;
)", )",
#else #else
// R"( refresh ? "console.log('Refreshing login'); _prusaSlicer_initLogin('%s');" :
// console.log('Preparing login');
// function errorHandler(err) {
// const msg = {
// action: 'ERROR',
// error: JSON.stringify(err),
// critical: false
// };
// console.error('Login error occurred', msg);
// window._prusaSlicer.postMessage(msg);
// };
// window.fetch('/slicer/login', {method: 'POST', headers: {Authorization: 'Bearer %s'}})
// .then(function (resp) {
// console.log('Login resp', resp);
// resp.text()
// .then(function (json) { console.log('Login resp body', json); return json; })
// .then(function (body) {
// if (resp.status >= 400) errorHandler({status: resp.status, body});
// });
// })
// .catch(function (err){
// errorHandler({message: err.message, stack: err.stack});
// });
// )",
R"( R"(
console.log('Preparing login'); function _prusaSlicer_errorHandler(err) {
function errorHandler(err) {
const msg = { const msg = {
action: 'ERROR', action: 'ERROR',
error: JSON.stringify(err), error: JSON.stringify(err),
@ -672,13 +648,21 @@ wxString ConnectWebViewPanel::get_login_script(bool refresh)
window._prusaSlicer.postMessage(msg); window._prusaSlicer.postMessage(msg);
}; };
function delay(ms) { function _prusaSlicer_delay(ms) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
setTimeout(resolve, ms); setTimeout(resolve, ms);
}); });
} }
(async () => { async function _prusaSlicer_initLogin(token) {
const parts = token.split('.');
const claims = JSON.parse(atob(parts[1]));
const now = new Date().getTime() / 1000;
if (claims.exp <= now) {
console.log('Skipping initLogin as token is expired');
return;
}
let retry = false; let retry = false;
let backoff = 1000; let backoff = 1000;
const maxBackoff = 64000; const maxBackoff = 64000;
@ -688,15 +672,15 @@ wxString ConnectWebViewPanel::get_login_script(bool refresh)
try { try {
console.log('Slicer Login request'); console.log('Slicer Login request');
let resp = await fetch('/slicer/login', {method: 'POST', headers: {Authorization: 'Bearer %s'}}); let resp = await fetch('/slicer/login', {method: 'POST', headers: {Authorization: 'Bearer ' + token}});
let body = await resp.text(); let body = await resp.text();
console.log('Slicer Login resp', resp.status, body); console.log('Slicer Login resp', resp.status, body);
if (resp.status >= 500) { if (resp.status >= 500 || resp.status == 408) {
retry = true; retry = true;
} else { } else {
retry = false; retry = false;
if (resp.status >= 400) if (resp.status >= 400)
errorHandler({status: resp.status, body}); _prusaSlicer_errorHandler({status: resp.status, body});
} }
} catch (e) { } catch (e) {
console.error('Slicer Login failed', e.toString()); console.error('Slicer Login failed', e.toString());
@ -704,13 +688,18 @@ wxString ConnectWebViewPanel::get_login_script(bool refresh)
} }
if (retry) { if (retry) {
await delay(backoff + 1000 * Math.random()); await _prusaSlicer_delay(backoff + 1000 * Math.random());
if (backoff < maxBackoff) { if (backoff < maxBackoff) {
backoff *= 2; backoff *= 2;
} }
} }
} while (retry); } while (retry);
})(); }
if (window._prusaSlicer_initialLoad === undefined) {
console.log('Initial login');
_prusaSlicer_initLogin('%s');
window._prusaSlicer_initialLoad = true;
}
)", )",
#endif #endif
access_token access_token

View File

@ -12,6 +12,7 @@
#include <deque> #include <deque>
#include <sstream> #include <sstream>
#include <exception> #include <exception>
#include <random>
#include <boost/filesystem/fstream.hpp> // IWYU pragma: keep #include <boost/filesystem/fstream.hpp> // IWYU pragma: keep
#include <boost/filesystem/path.hpp> #include <boost/filesystem/path.hpp>
#include <boost/filesystem.hpp> #include <boost/filesystem.hpp>
@ -154,7 +155,7 @@ struct Http::priv
std::string curl_error(CURLcode curlcode); std::string curl_error(CURLcode curlcode);
std::string body_size_error(); std::string body_size_error();
void http_perform(); void http_perform(const HttpRetryOpt& retry_opts = HttpRetryOpt::no_retry());
}; };
Http::priv::priv(const std::string &url) Http::priv::priv(const std::string &url)
@ -340,8 +341,20 @@ std::string Http::priv::body_size_error()
return (boost::format("HTTP body data size exceeded limit (%1% bytes)") % limit).str(); return (boost::format("HTTP body data size exceeded limit (%1% bytes)") % limit).str();
} }
void Http::priv::http_perform() bool is_transient_error(CURLcode res, long http_status)
{ {
if (res == CURLE_OK || res == CURLE_HTTP_RETURNED_ERROR)
return http_status == 408 || http_status >= 500;
return res == CURLE_COULDNT_CONNECT || res == CURLE_COULDNT_RESOLVE_HOST ||
res == CURLE_OPERATION_TIMEDOUT;
}
void Http::priv::http_perform(const HttpRetryOpt& retry_opts)
{
using namespace std::chrono_literals;
static thread_local std::mt19937 generator;
std::uniform_int_distribution<std::chrono::milliseconds::rep> randomized_delay(retry_opts.initial_delay.count(), (retry_opts.initial_delay.count() * 3) / 2);
::curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); ::curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L);
::curl_easy_setopt(curl, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL); ::curl_easy_setopt(curl, CURLOPT_POSTREDIR, CURL_REDIR_POST_ALL);
::curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writecb); ::curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, writecb);
@ -375,7 +388,28 @@ void Http::priv::http_perform()
::curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, postfields.size()); ::curl_easy_setopt(curl, CURLOPT_POSTFIELDSIZE_LARGE, postfields.size());
} }
CURLcode res = ::curl_easy_perform(curl); bool retry;
CURLcode res;
long http_status = 0;
std::chrono::milliseconds delay = std::chrono::milliseconds(randomized_delay(generator));
size_t num_retries = 0;
do {
res = ::curl_easy_perform(curl);
if (res == CURLE_OK)
::curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_status);
retry = delay >= 0ms && is_transient_error(res, http_status);
if (retry && retry_opts.max_retries > 0 && num_retries >= retry_opts.max_retries)
retry = false;
if (retry) {
num_retries++;
BOOST_LOG_TRIVIAL(error)
<< "HTTP Transient error (code=" << res << ", http_status=" << http_status
<< "), retrying in " << delay.count() / 1000.0f << " s";
std::this_thread::sleep_for(delay);
delay = std::min(delay * 2, retry_opts.max_delay);
}
} while (retry);
putFile.reset(); putFile.reset();
@ -397,8 +431,6 @@ void Http::priv::http_perform()
if (errorfn) { errorfn(std::move(buffer), curl_error(res), 0); } if (errorfn) { errorfn(std::move(buffer), curl_error(res), 0); }
}; };
} else { } else {
long http_status = 0;
::curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &http_status);
if (http_status >= 400) { if (http_status >= 400) {
if (errorfn) { errorfn(std::move(buffer), std::string(), http_status); } if (errorfn) { errorfn(std::move(buffer), std::string(), http_status); }
@ -420,6 +452,23 @@ Http::Http(const std::string &url) : p(new priv(url)) {}
// Public // Public
const HttpRetryOpt& HttpRetryOpt::default_retry()
{
using namespace std::chrono_literals;
static HttpRetryOpt val = {500ms, 64s, 0};
return val;
}
const HttpRetryOpt& HttpRetryOpt::no_retry()
{
using namespace std::chrono_literals;
static HttpRetryOpt val = {0ms};
return val;
}
Http::Http(Http &&other) : p(std::move(other.p)) {} Http::Http(Http &&other) : p(std::move(other.p)) {}
Http::~Http() Http::~Http()
@ -609,13 +658,13 @@ Http& Http::cookie_jar(const std::string& file_path)
return *this; return *this;
} }
Http::Ptr Http::perform() Http::Ptr Http::perform(const HttpRetryOpt& retry_opts)
{ {
auto self = std::make_shared<Http>(std::move(*this)); auto self = std::make_shared<Http>(std::move(*this));
if (self->p) { if (self->p) {
auto io_thread = std::thread([self](){ auto io_thread = std::thread([self, &retry_opts](){
self->p->http_perform(); self->p->http_perform(retry_opts);
}); });
self->p->io_thread = std::move(io_thread); self->p->io_thread = std::move(io_thread);
} }
@ -623,9 +672,9 @@ Http::Ptr Http::perform()
return self; return self;
} }
void Http::perform_sync() void Http::perform_sync(const HttpRetryOpt& retry_opts)
{ {
if (p) { p->http_perform(); } if (p) { p->http_perform(retry_opts); }
} }
void Http::cancel() void Http::cancel()

View File

@ -11,10 +11,20 @@
#include <string> #include <string>
#include <functional> #include <functional>
#include <boost/filesystem/path.hpp> #include <boost/filesystem/path.hpp>
#include <chrono>
namespace Slic3r { namespace Slic3r {
struct HttpRetryOpt
{
std::chrono::milliseconds initial_delay;
std::chrono::milliseconds max_delay;
size_t max_retries{0};
static const HttpRetryOpt& no_retry();
static const HttpRetryOpt& default_retry();
};
/// Represetns a Http request /// Represetns a Http request
class Http : public std::enable_shared_from_this<Http> { class Http : public std::enable_shared_from_this<Http> {
@ -133,9 +143,9 @@ public:
Http& set_referer(const std::string& referer); Http& set_referer(const std::string& referer);
// Starts performing the request in a background thread // Starts performing the request in a background thread
Ptr perform(); Ptr perform(const HttpRetryOpt& retry_opts = HttpRetryOpt::no_retry());
// Starts performing the request on the current thread // Starts performing the request on the current thread
void perform_sync(); void perform_sync(const HttpRetryOpt &retry_opts = HttpRetryOpt::no_retry());
// Cancels a request in progress // Cancels a request in progress
void cancel(); void cancel();