Merge branch 'main' into feat/plugin-feature

This commit is contained in:
SoftFever
2026-07-04 00:19:39 +08:00
27 changed files with 5945 additions and 2482 deletions

View File

@@ -2427,18 +2427,19 @@ void PrintConfigDef::init_fff_params()
// xgettext:no-c-format, no-boost-format
def = this->add("adaptive_pressure_advance_overhangs", coBools);
def->label = L("Enable adaptive pressure advance for overhangs (beta)");
def->tooltip = L("Enable adaptive PA for overhangs as well as when flow changes within the same feature. This is an experimental option, "
"as if the PA profile is not set accurately, it will cause uniformity issues on the external surfaces before and after overhangs.\n"
"Not compatible with Prusa printers as they pause to process PA changes, which causes delays and defects.");
def->label = L("Enable adaptive pressure advance within features (beta)");
def->tooltip = L("Enable adaptive PA whenever there are flow changes in a feature, such as line width changes in a corner or overhangs.\n\n"
"Not compatible with Prusa printers as they pause to process PA changes, which causes delays and defects.\n\n"
"This is an experimental option, as if the PA profile is not set accurately, it will cause uniformity issues.");
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionBools{ false });
def = this->add("adaptive_pressure_advance_bridges", coFloats);
def->label = L("Pressure advance for bridges");
def->tooltip = L("Pressure advance value for bridges. Set to 0 to disable.\n\n"
"A lower PA value when printing bridges helps reduce the appearance of slight under extrusion immediately after bridges. "
"This is caused by the pressure drop in the nozzle when printing in the air and a lower PA helps counteract this.");
def->label = L("Static pressure advance for bridges");
def->tooltip = L("Static pressure advance value for bridges. Set to 0 to apply the same pressure advance as \n"
"equivalent walls (using adaptive settings if enabled).\n\n"
"A lower PA value when printing bridges helps reduce the appearance of slight under-extrusion immediately after bridges. "
"This is caused by the pressure drop in the nozzle when printing in the air and a lower PA helps counteract this.");
def->max = 2;
def->mode = comAdvanced;
def->set_default_value(new ConfigOptionFloats { 0.0 });

View File

@@ -266,18 +266,6 @@ static void set_config_values(DynamicPrintConfig *config, const std::string &key
}
}
template <typename T, typename OptionType>
static void set_config_values(ModelConfig& config, const std::string &key, T value)
{
auto config_opt = config.get().option<OptionType>(key);
if (config_opt) {
config.set_key_value(key, new OptionType(config_opt->values.size(), value));
}
else {
BOOST_LOG_TRIVIAL(info) << "set_config_values: the key" << key << "is empty.";
}
}
bool Plater::has_illegal_filename_characters(const wxString& wxs_name)
{
std::string name = into_u8(wxs_name);
@@ -12928,9 +12916,9 @@ void Plater::_calib_pa_pattern(const Calib_Params& params)
auto &obj_config = obj->config;
if (speeds.size() > 1)
set_config_values<double, ConfigOptionFloatsNullable>(obj_config, "outer_wall_speed", tspd);
obj_config.set_key_value("outer_wall_speed", new ConfigOptionFloatsNullable(1, tspd));
if (accels.size() > 1)
set_config_values<double, ConfigOptionFloatsNullable>(obj_config, "outer_wall_acceleration", tacc);
obj_config.set_key_value("outer_wall_acceleration", new ConfigOptionFloatsNullable(1, tacc));
auto cur_plate = get_partplate_list().get_plate(plate_idx);
if (!cur_plate) {
@@ -13352,7 +13340,7 @@ void Plater::calib_max_vol_speed(const Calib_Params& params)
set_config_values<double, ConfigOptionFloats>(filament_config, "filament_max_volumetric_speed", 200);
filament_config->set_key_value("slow_down_layer_time", new ConfigOptionFloats{0.0});
printer_config->set_key_value("resonance_avoidance", new ConfigOptionBool{false});
set_config_values<bool, ConfigOptionBoolsNullable>(obj_cfg, "enable_overhang_speed", false);
obj_cfg.set_key_value("enable_overhang_speed", new ConfigOptionBoolsNullable(1, false));
obj_cfg.set_key_value("wall_loops", new ConfigOptionInt(1));
obj_cfg.set_key_value("alternate_extra_wall", new ConfigOptionBool(false));
obj_cfg.set_key_value("top_shell_layers", new ConfigOptionInt(0));

View File

@@ -45,8 +45,13 @@
#include <Windows.h>
#endif
#if defined(__APPLE__)
#if !defined(_WIN32)
#include <unistd.h>
#include <fcntl.h>
#include <cerrno>
#endif
#if defined(__APPLE__)
#include <uuid/uuid.h>
#endif
@@ -89,6 +94,60 @@ constexpr const char* SECRET_STORE_SERVICE = "OrcaSlicer/Auth";
constexpr const char* SECRET_STORE_USER = "orca_refresh_token";
constexpr std::chrono::seconds TOKEN_REFRESH_SKEW{900}; // 15 minutes
// Cross-process advisory lock serializing refresh-token rotation between Orca instances on
// this machine. The Supabase refresh token rotates on every use, so read -> spend -> write-back
// of the rotated successor must be one critical section across processes; otherwise a second
// instance can re-spend a token a peer already rotated, triggering `refresh_token_already_used`.
// Blocks until acquired (kernel sleep, no CPU spin). It cannot deadlock permanently because the
// refresh POST is bounded (http_post_token uses timeout_max) and both back-ends release
// automatically if the holder dies. Same machine only; cross-device concurrency is still
// governed by the server's reuse-detection grace window.
class InterProcessRefreshTokenLock
{
public:
explicit InterProcessRefreshTokenLock(const std::string& path)
{
if (path.empty()) { m_locked = true; return; } // no path -> proceed unsynchronized
#if defined(_WIN32)
const std::wstring name = L"Local\\OrcaTokenRefresh_" + std::to_wstring(std::hash<std::string>{}(path));
m_handle = ::CreateMutexW(nullptr, FALSE, name.c_str());
if (!m_handle) { m_locked = true; return; } // can't create -> don't block
const DWORD r = ::WaitForSingleObject(m_handle, INFINITE); // blocks, no spin
m_locked = (r == WAIT_OBJECT_0 || r == WAIT_ABANDONED); // ABANDONED: prior holder crashed
#else
m_fd = ::open(path.c_str(), O_RDWR | O_CREAT | O_CLOEXEC, 0600);
if (m_fd == -1) { m_locked = true; return; } // can't open -> don't block
struct flock fl{};
fl.l_type = F_WRLCK; fl.l_whence = SEEK_SET; fl.l_start = 0; fl.l_len = 0;
int rc;
do { rc = ::fcntl(m_fd, F_SETLKW, &fl); } while (rc == -1 && errno == EINTR); // blocks in kernel
m_locked = (rc != -1);
#endif
}
~InterProcessRefreshTokenLock()
{
#if defined(_WIN32)
if (m_handle) { if (m_locked) ::ReleaseMutex(m_handle); ::CloseHandle(m_handle); }
#else
if (m_fd != -1) ::close(m_fd); // closing the fd releases the lock we hold
#endif
}
bool locked() const { return m_locked; }
InterProcessRefreshTokenLock(const InterProcessRefreshTokenLock&) = delete;
InterProcessRefreshTokenLock& operator=(const InterProcessRefreshTokenLock&) = delete;
private:
bool m_locked = false;
#if defined(_WIN32)
HANDLE m_handle = nullptr;
#else
int m_fd = -1;
#endif
};
// Return a JSON field only when it is present as a string. Missing or non-string values normalize to empty.
std::string get_json_string_field(const json& j, const std::string& key)
{
@@ -1678,8 +1737,7 @@ RefreshResult OrcaCloudServiceAgent::refresh_now(const std::string& refresh_toke
}
auto worker = [this, refresh_token, reason]() {
(void) reason;
RefreshResult r = refresh_session_with_token(refresh_token);
RefreshResult r = refresh_session_with_token(refresh_token, reason);
refresh_running.store(false);
return r;
};
@@ -1699,20 +1757,39 @@ RefreshResult OrcaCloudServiceAgent::refresh_now(const std::string& refresh_toke
RefreshResult OrcaCloudServiceAgent::refresh_from_storage(const std::string& reason, bool async)
{
std::string refresh_token = get_refresh_token();
if (refresh_token.empty()) {
std::string user_secret;
if (load_user_secret(user_secret) && !user_secret.empty()) {
SessionInfo stored_session;
parse_stored_secret(user_secret, refresh_token, stored_session);
}
(void) async; // currently all callers are calling this function synchronous anyway
// Blocks until we own the lock (kernel sleep, no spin). Cannot deadlock permanently:
// the refresh POST is bounded (http_post_token timeout_max) and the lock auto-releases
// on process death, so no holder can keep it longer than that bound.
InterProcessRefreshTokenLock lock(token_lock_path());
if (!lock.locked()) {
// Lock syscall genuinely failed (rare). Proceed best-effort rather than give up.
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: token refresh lock unavailable, "
"proceeding unsynchronized (reason=" << reason << ")";
}
std::string refresh_token;
std::string user_secret;
// We read the refresh token from the OS keychain as the source of truth
if (load_user_secret(user_secret) && !user_secret.empty()) {
SessionInfo stored_session;
parse_stored_secret(user_secret, refresh_token, stored_session);
}
// We only read from memory if the read from OS keychain fails
if (refresh_token.empty())
refresh_token = get_refresh_token();
if (refresh_token.empty()) {
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: no refresh token available for refresh (reason=" << reason << ")";
return RefreshResult::AuthRejected; // no persisted token: nothing to preserve
}
return refresh_now(refresh_token, reason, async);
// Synchronous: hold the lock across the network round-trip AND the write-back that
// set_user_session performs, so the rotation is atomic w.r.t. other instances.
return refresh_now(refresh_token, reason, /*async=*/false);
}
bool OrcaCloudServiceAgent::refresh_if_expiring(std::chrono::seconds skew, const std::string& reason)
@@ -1726,8 +1803,14 @@ bool OrcaCloudServiceAgent::refresh_if_expiring(std::chrono::seconds skew, const
if (!needs_refresh)
return true;
if (refresh_from_storage(reason, false) == RefreshResult::Success) return true;
// First attempt. refresh_from_storage blocks on the cross-process lock and reads the
// freshest token from the store, so cross-instance contention is already resolved here.
RefreshResult r = refresh_from_storage(reason, false);
if (r == RefreshResult::Success) return true;
if (r == RefreshResult::AuthRejected) return false; // definitive: retrying the same token can't help
// One retry, only for a transient network failure of the refresh POST (lost response,
// timeout, 429/5xx -> Transient). The retry re-acquires the lock and re-reads the store.
std::this_thread::sleep_for(std::chrono::milliseconds(750));
return refresh_from_storage(reason + "_retry", false) == RefreshResult::Success;
}
@@ -1746,7 +1829,23 @@ static RefreshResult classify_refresh_result(unsigned http_code, bool session_es
: RefreshResult::Transient; // 2xx but unusable body
}
RefreshResult OrcaCloudServiceAgent::refresh_session_with_token(const std::string& refresh_token)
// Best-effort extraction of GoTrue's "error_code" field (e.g. "refresh_token_already_used").
// Returns "" if the body is not a JSON object or lacks the field.
static std::string extract_error_code(const std::string& body)
{
const json j = json::parse(body, nullptr, false);
if (j.is_object())
return get_json_string_field(j, "error_code");
return "";
}
static long long now_epoch_seconds()
{
return std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()).count();
}
RefreshResult OrcaCloudServiceAgent::refresh_session_with_token(const std::string& refresh_token, const std::string& reason)
{
std::string body = "{\"refresh_token\":\"" + refresh_token + "\"}";
std::string url = auth_base_url + auth_constants::TOKEN_PATH + "?grant_type=refresh_token";
@@ -1755,6 +1854,12 @@ RefreshResult OrcaCloudServiceAgent::refresh_session_with_token(const std::strin
// http_post_token sets http_code to 0 when the server could not be reached.
http_post_token(body, &response, &http_code, url);
const long long now_s = now_epoch_seconds();
const long long last_ok = last_refresh_success_epoch.load(std::memory_order_relaxed);
const long long since_last_ok = (last_ok == 0) ? -1 : (now_s - last_ok); // -1 = no success yet this process
const long long since_start = now_s - agent_start_epoch;
const std::string log_reason = reason.empty() ? "-" : reason;
bool established = false;
if (http_code >= 200 && http_code < 300) {
if (session_handler) {
@@ -1768,10 +1873,47 @@ RefreshResult OrcaCloudServiceAgent::refresh_session_with_token(const std::strin
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: token refresh parse exception - " << e.what();
}
}
if (established) {
last_refresh_success_epoch.store(now_s, std::memory_order_relaxed);
BOOST_LOG_TRIVIAL(info) << "[auth] event=refresh_ok reason=" << log_reason
<< " secs_since_last_success=" << since_last_ok;
}
} else {
// Diagnostics to classify a future refresh_token_already_used incident from logs alone
// - secs_since_last_success large (or -1 = none this process) => stale token beyond the
// reuse-grace window (the ">1h gap" residual).
// - stored_differs=true => another instance/device already rotated the token in the
// shared secret store (the multi-session residual).
const std::string error_code = extract_error_code(response);
// Only meaningful when the server actually rejected us (not a bare transport failure).
bool stored_present = false, stored_differs = false;
if (http_code >= 400) {
std::string stored_secret, stored_token;
SessionInfo ignored;
if (load_user_secret(stored_secret) && !stored_secret.empty()
&& parse_stored_secret(stored_secret, stored_token, ignored) && !stored_token.empty()) {
stored_present = true;
stored_differs = (stored_token != refresh_token);
}
}
std::string user_id_copy;
{
std::lock_guard<std::mutex> lock(session_mutex);
user_id_copy = session.user_id;
}
std::string truncated_response = response.size() > 200 ? response.substr(0, 200) + "..." : response;
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: token refresh failed - http_code=" << http_code
<< ", response_body=" << truncated_response;
BOOST_LOG_TRIVIAL(warning) << "[auth] event=refresh_rejected http_code=" << http_code
<< " error_code=" << (error_code.empty() ? "-" : error_code)
<< " reason=" << log_reason
<< " stored_present=" << (stored_present ? "true" : "false")
<< " stored_differs=" << (stored_differs ? "true" : "false")
<< " secs_since_last_success=" << since_last_ok
<< " secs_since_start=" << since_start
<< " user_id=" << (user_id_copy.empty() ? "-" : user_id_copy)
<< " response_body=" << truncated_response;
}
return classify_refresh_result(http_code, established);
@@ -2240,6 +2382,9 @@ bool OrcaCloudServiceAgent::http_post_token(const std::string& body,
resp_body = body;
BOOST_LOG_TRIVIAL(error) << "OrcaCloudServiceAgent: HTTP error - " << error;
})
// Keep this timeout finite: refresh_from_storage holds a cross-process lock across
// this call, so an unbounded refresh POST would let one instance wedge token refresh
// for every other Orca instance on the machine.
.timeout_max(30)
.perform_sync();
@@ -3447,4 +3592,13 @@ int OrcaCloudServiceAgent::fetch_plugin_changelogs(const std::vector<std::string
return first_error;
}
std::string OrcaCloudServiceAgent::token_lock_path() const
{
if (config_dir.empty())
return {};
wxFileName lock(wxString::FromUTF8(config_dir.c_str()), "orca_refresh_token.lock");
lock.Normalize();
return lock.GetFullPath().ToStdString();
}
} // namespace Slic3r

View File

@@ -322,7 +322,7 @@ public:
bool refresh_if_expiring(std::chrono::seconds skew, const std::string& reason);
RefreshResult refresh_from_storage(const std::string& reason, bool async = false);
RefreshResult refresh_now(const std::string& refresh_token, const std::string& reason, bool async = false);
RefreshResult refresh_session_with_token(const std::string& refresh_token);
RefreshResult refresh_session_with_token(const std::string& refresh_token, const std::string& reason = "");
// Session state helpers. nickname is the human-facing UI label after provider fallback resolution.
bool set_user_session(const std::string& token,
@@ -385,6 +385,9 @@ private:
std::string map_to_json(const std::map<std::string, std::string>& map);
void json_to_map(const std::string& json, std::map<std::string, std::string>& map);
// Refresh token lock
std::string token_lock_path() const;
// Member variables - configuration
std::string log_dir;
std::string config_dir;
@@ -405,6 +408,12 @@ private:
SessionInfo session;
mutable std::mutex session_mutex;
// Refresh diagnostics (see docs/analysis/refresh_token_already_used.md). Epoch seconds so the
// refresh-failure log can report token staleness without holding a lock or logging any token.
std::atomic<long long> last_refresh_success_epoch{0}; // 0 = no success yet this process
const long long agent_start_epoch{std::chrono::duration_cast<std::chrono::seconds>(
std::chrono::system_clock::now().time_since_epoch()).count()};
// Member variables - connection state
bool is_connected{false};
bool enable_track{false};