Compare commits

..
Author SHA1 Message Date
Hanif Koh f6f7f7c77b Load a CLI Project's Printer and Process Settings as the GUI Does
The CLI filled only the keys a project lacked from its system preset.
The GUI builds a project preset differently: the project's values go
over the default preset, without the print-host keys, and every key
the project does not list in different_settings_to_system is refreshed
to its base system preset's current value. After a profile update the
two sliced the same project differently.

That step now lives in Preset::load_external_config, which takes plain
configs and gets the base preset from a callback, so the collection
lookup stays in PresetCollection. PresetBundle::project_different_keys
builds the kept-key set from a project's escaped entry, adding the
preset bookkeeping keys, and is used by both the GUI and the CLI.
PresetCollection::load_external_preset calls the shared step with no
change in behaviour.

The CLI now builds the project's printer and process configs with the
same step, passing the system preset from resolve_system_preset. That
drops the hand-kept skip list for print-host and variant-layout keys
and the legacy-key check: the shared step already excludes print-host
keys and maps per-variant values onto the base preset's variant layout.
The --uptodate path and a printer or process given on the command line
are left as they were.

Because the GUI's kept-key set always holds the bookkeeping keys, the
refresh runs for every project that names a system preset, so the CLI
now loads that preset's vendor on every such run.
2026-09-30 16:35:49 +08:00
Hanif Koh 526a30225f Fill Settings Missing From a CLI Project From Its System Presets
A project saved before a printer or process option existed has no value
for it. The GUI takes such keys from the project's system preset; the
CLI left them at the option default, so e.g. extruder_clearance_dist_to_rod
sliced as 40 instead of the P1S's 33.

The CLI now resolves the project's system printer and process presets by
name and copies the keys the project lacks, skipping preset bookkeeping,
print-host keys, the extruder variant layout and keys the legacy handler
drops. PresetBundle::resolve_system_preset finds the vendor through its
manifest or preset cache, so it also works in release builds, which ship
vendors as caches only.
2026-09-30 16:35:49 +08:00
Ian Chua dc0e269186 test: bumping OFL version to test OTA workflow [To be reverted before 2.5.0 alpha] (#16017)
Merged by /bot merge on behalf of @peachismomo (id 52488812).
Grants: resources/profiles/OrcaFilamentLibrary/filament/Elegoo, resources/profiles/OrcaFilamentLibrary.json, resources/profiles/Elegoo, resources/profiles/Elegoo.json
Head: ff6a2f2056
2026-09-30 08:25:43 +00:00
Error404JoyNotFound da0611a301 Fix : Add curr_bed_type to built-in placeholders in G-code editor (#15987) 2026-09-30 14:59:46 +08:00
Hanif Koh 561737f407 Fix the CLI 3MF Export Crash After Rendering a Plate Thumbnail
Since the CLI can open an OpenGL context (#15745) it renders plate
thumbnails on export, and the viewport restore at the end of
render_thumbnail_internal (#15674) then reads the plater through the wx
application. The CLI has neither, so every --export-3mf on a machine
with a display died with a segmentation fault after the first
thumbnail. Skip the restore when there is no application or no plater;
the GUI path is unchanged.
2026-09-30 14:46:44 +08:00
Ian Chua 7d42ad17a4 fix: malformed jq filter in OFL publisher barrier (#16012) 2026-09-30 14:19:06 +08:00
31 changed files with 763 additions and 1257 deletions
+1 -1
View File
@@ -146,7 +146,7 @@ jobs:
"repos/${{ github.repository }}/actions/workflows/post_merge_profiles.yml/runs" \
-f branch="$branch" -f event=workflow_dispatch -f per_page=100)"
run_id="$(jq -r --arg marker "[OFL cron $dispatch_id]" \
'[.workflow_runs[] | select((.display_title // "") | contains($marker))] \
'[.workflow_runs[] | select((.display_title // "") | contains($marker))]
| sort_by(.created_at) | last | .id // empty' <<< "$runs_json")"
[ -n "$run_id" ] && break
sleep 5
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "OrcaFilamentLibrary",
"version": "02.04.00.16",
"version": "02.04.00.17",
"force_update": "0",
"description": "Orca Filament Library",
"filament_list": [
@@ -6,7 +6,7 @@
"filament_id": "OFvgE0Zh",
"instantiation": "false",
"fan_cooling_layer_time": [
"81"
"85"
],
"fan_min_speed": [
"50"
+45 -4
View File
@@ -2116,7 +2116,12 @@ int CLI::run(int argc, char **argv)
// One resolver for the whole run, so presets from the same vendor tree share its load.
std::unique_ptr<PresetBundle> system_preset_resolver;
auto resolve_preset = [&ensure_cli_preset_bundle, &system_preset_resolver](const std::string &file, DynamicPrintConfig &config,
auto ensure_system_preset_resolver = [&system_preset_resolver]() -> PresetBundle & {
if (!system_preset_resolver)
system_preset_resolver = std::make_unique<PresetBundle>();
return *system_preset_resolver;
};
auto resolve_preset = [&ensure_cli_preset_bundle, &ensure_system_preset_resolver](const std::string &file, DynamicPrintConfig &config,
std::string &config_type, const std::string &config_from,
bool probe_type, std::string &error) {
const auto *inherits = config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS);
@@ -2126,9 +2131,7 @@ int CLI::run(int argc, char **argv)
PresetBundle *bundle = nullptr;
bool allow_source_manifest = false;
if (config_from == "system") {
if (!system_preset_resolver)
system_preset_resolver = std::make_unique<PresetBundle>();
bundle = system_preset_resolver.get();
bundle = &ensure_system_preset_resolver();
allow_source_manifest = true;
} else {
bundle = ensure_cli_preset_bundle(error);
@@ -3120,6 +3123,44 @@ int CLI::run(int argc, char **argv)
return 0;
};
// Load the project's printer and process settings as the GUI loads its presets: over the default preset,
// with every key the project does not list as changed, including keys saved before an option existed,
// taken from its current system preset.
auto load_project_preset = [this, &ensure_system_preset_resolver, &current_different_settings, filament_count](const std::string &system_name, Preset::Type type) {
if (system_name.empty())
return;
// Preset bookkeeping the CLI keeps in its own groups, e.g. inherits_group and print_compatible_printers.
static const std::set<std::string> bookkeeping_keys = {"inherits", "compatible_printers", "compatible_prints", "compatible_printers_condition",
"compatible_prints_condition", "print_settings_id", "printer_settings_id"};
const size_t index = type == Preset::TYPE_PRINTER ? filament_count + 1 : 0;
PresetBundle &resolver = ensure_system_preset_resolver();
DynamicPrintConfig system_config;
t_config_option_keys keys;
const DynamicPrintConfig config = Preset::load_external_config(type,
type == Preset::TYPE_PRINTER ? resolver.printers.default_preset_for(m_print_config).config : resolver.prints.default_preset().config,
m_print_config, PresetBundle::project_different_keys(index < current_different_settings.size() ? current_different_settings[index] : std::string()),
[&](const std::string &) -> DynamicPrintConfig * {
std::string error;
if (resolver.resolve_system_preset(system_config, type, system_name, config_substitution_rule, error))
return &system_config;
BOOST_LOG_TRIVIAL(warning) << boost::format("CLI: system preset '%1%' not resolved (%2%); the project keeps its values") % system_name % error;
return nullptr;
}, &keys);
for (const std::string &key : keys) {
const ConfigOption *opt = config.option(key);
const ConfigOption *old = m_print_config.option(key);
if (bookkeeping_keys.count(key) != 0 || opt == nullptr || (old != nullptr && *old == *opt))
continue;
BOOST_LOG_TRIVIAL(info) << boost::format("CLI: %1% from '%2%': %3% -> %4%") % key % system_name % (old ? old->serialize() : std::string("(missing)")) % opt->serialize();
m_print_config.set_key_value(key, opt->clone());
}
};
// The --uptodate path refreshes the project from its own system configs.
if (new_printer_name.empty() && load_machine_config.empty())
load_project_preset(current_printer_system_name, Preset::TYPE_PRINTER);
if (new_process_name.empty() && load_process_config.empty())
load_project_preset(current_process_system_name, Preset::TYPE_PRINT);
std::vector<std::string>& different_settings = m_print_config.option<ConfigOptionStrings>("different_settings_to_system", true)->values;
std::vector<std::string>& inherits_group = m_print_config.option<ConfigOptionStrings>("inherits_group", true)->values;
inherits_group.resize(filament_count + 2, std::string());
+64 -46
View File
@@ -5,7 +5,6 @@
//BBS
#include "Preset.hpp"
#include "Exception.hpp"
#include "InstanceLock.hpp"
#include "LocalesUtils.hpp"
#include "Thread.hpp"
#include "format.hpp"
@@ -744,13 +743,10 @@ static bool verify_config_file_checksum(boost::nowide::ifstream &ifs)
#ifdef USE_JSON_CONFIG
std::string AppConfig::load(bool read_only)
std::string AppConfig::load()
{
json j;
// Keep another instance from replacing or restoring the file mid-read.
InstanceLock instance_lock(read_only ? std::string() : lock_path());
// 1) Read the complete config file into a boost::property_tree.
namespace pt = boost::property_tree;
pt::ptree tree;
@@ -1000,6 +996,7 @@ void AppConfig::save()
// The config is first written to a file with a PID suffix and then moved
// to avoid race conditions with multiple instances of Slic3r
const auto path = config_path();
std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str();
json j;
@@ -1129,18 +1126,43 @@ void AppConfig::save()
j["local_machines"][local_machine.first] = m_json;
}
const std::string config_str = j.dump(1, '\t');
if (write_config_file(path, config_str + "\n", config_str))
boost::nowide::ofstream c;
c.open(path_pid, std::ios::out | std::ios::trunc);
c << j.dump(1, '\t') << std::endl;
#ifdef WIN32
// WIN32 specific: The final "rename_file()" call is not safe in case of an application crash, there is no atomic "rename file" API
// provided by Windows (sic!). Therefore we save a MD5 checksum to be able to verify file corruption. In addition,
// we save the config file into a backup first before moving it to the final destination.
c << appconfig_md5_hash_line(j.dump(1, '\t'));
#endif
c.close();
if (c.fail()) {
BOOST_LOG_TRIVIAL(error) << "Failed to write new configuration to " << path_pid << "; aborting attempt to overwrite original configuration";
return;
}
#ifdef WIN32
// Make a backup of the configuration file before copying it to the final destination.
std::string error_message;
std::string backup_path = (boost::format("%1%.bak") % path).str();
// Copy configuration file with PID suffix into the configuration file with "bak" suffix.
if (copy_file(path_pid, backup_path, error_message, false) != SUCCESS)
BOOST_LOG_TRIVIAL(error) << "Copying from " << path_pid << " to " << backup_path << " failed. Failed to create a backup configuration.";
#endif
// Rename the config atomically.
// On Windows, the rename is likely NOT atomic, thus it may fail if PrusaSlicer crashes on another thread in the meanwhile.
// To cope with that, we already made a backup of the config on Windows.
rename_file(path_pid, path);
m_dirty = false;
}
#else
std::string AppConfig::load(bool read_only)
std::string AppConfig::load()
{
// Keep another instance from replacing or restoring the file mid-read.
InstanceLock instance_lock(read_only ? std::string() : lock_path());
// 1) Read the complete config file into a boost::property_tree.
namespace pt = boost::property_tree;
pt::ptree tree;
@@ -1278,6 +1300,7 @@ void AppConfig::save()
// The config is first written to a file with a PID suffix and then moved
// to avoid race conditions with multiple instances of Slic3r
const auto path = config_path();
std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str();
std::stringstream config_ss;
if (m_mode == EAppMode::Editor)
@@ -1313,39 +1336,39 @@ void AppConfig::save()
// One empty line before the MD5 sum.
config_ss << std::endl;
const std::string config_str = config_ss.str();
if (write_config_file(path, config_str, config_str))
std::string config_str = config_ss.str();
boost::nowide::ofstream c;
c.open(path_pid, std::ios::out | std::ios::trunc);
c << config_str;
#ifdef WIN32
// WIN32 specific: The final "rename_file()" call is not safe in case of an application crash, there is no atomic "rename file" API
// provided by Windows (sic!). Therefore we save a MD5 checksum to be able to verify file corruption. In addition,
// we save the config file into a backup first before moving it to the final destination.
c << appconfig_md5_hash_line(config_str);
#endif
c.close();
if (c.fail()) {
BOOST_LOG_TRIVIAL(error) << "Failed to write new configuration to " << path_pid << "; aborting attempt to overwrite original configuration";
return;
}
#ifdef WIN32
// Make a backup of the configuration file before copying it to the final destination.
std::string error_message;
std::string backup_path = (boost::format("%1%.bak") % path).str();
// Copy configuration file with PID suffix into the configuration file with "bak" suffix.
if (copy_file(path_pid, backup_path, error_message, false) != SUCCESS)
BOOST_LOG_TRIVIAL(error) << "Copying from " << path_pid << " to " << backup_path << " failed. Failed to create a backup configuration.";
#endif
// Rename the config atomically.
// On Windows, the rename is likely NOT atomic, thus it may fail if PrusaSlicer crashes on another thread in the meanwhile.
// To cope with that, we already made a backup of the config on Windows.
rename_file(path_pid, path);
m_dirty = false;
}
#endif
bool AppConfig::write_config_file(const std::string &path, std::string body, const std::string &checksum_source)
{
// Everything before this is assembly; only the writes need the other instances kept out.
InstanceLock instance_lock(lock_path());
#ifdef WIN32
// WIN32 specific: the final replace is not safe in case of an application crash, there is no atomic "rename file" API
// provided by Windows (sic!). Therefore we save a MD5 checksum to be able to verify file corruption. In addition,
// we save the config file into a backup first before moving it to the final destination.
body += appconfig_md5_hash_line(checksum_source);
#endif
// Not flushed to the device: the idle handler saves on the GUI thread after
// any change, and the rename already gives a complete old or new file.
if (const std::error_code ec = write_file_atomically(path, body)) {
BOOST_LOG_TRIVIAL(error) << "Failed to write the configuration " << path << ": " << ec.message() << "; trying again in 10 s";
m_retry_save_at = std::chrono::steady_clock::now() + std::chrono::seconds(10);
return false;
}
m_retry_save_at = {};
#ifdef WIN32
// Written after the config, so the backup never holds a state that was not confirmed written.
const std::string backup_path = (boost::format("%1%.bak") % path).str();
if (const std::error_code ec = write_file_atomically(backup_path, body))
BOOST_LOG_TRIVIAL(error) << "Failed to write the backup configuration " << backup_path << ": " << ec.message();
#endif
return true;
}
bool AppConfig::get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const
{
const auto it_v = m_vendors.find(vendor);
@@ -1834,11 +1857,6 @@ void AppConfig::reset_selections()
}
}
std::string AppConfig::lock_path()
{
return Slic3r::data_dir().empty() ? std::string() : config_path() + ".lock";
}
std::string AppConfig::config_path()
{
#ifdef USE_JSON_CONFIG
@@ -1875,7 +1893,7 @@ bool AppConfig::exists()
std::string AppConfig::load_if_exists()
{
return boost::filesystem::exists(loading_path()) ? load(/*read_only=*/true) : std::string();
return boost::filesystem::exists(loading_path()) ? load() : std::string();
}
}; // namespace Slic3r
+1 -16
View File
@@ -2,7 +2,6 @@
#define slic3r_AppConfig_hpp_
#include <set>
#include <chrono>
#include <map>
#include <string>
#include "nlohmann/json.hpp"
@@ -123,18 +122,14 @@ public:
// Load the slic3r.ini from a user profile directory (or a datadir, if configured).
// Return an error string, or an empty string on success.
std::string load(bool read_only = false);
std::string load();
// Treat a missing config as default state; otherwise load it normally.
// The CLI's load: it never saves, so it takes no lock and creates no lock file.
std::string load_if_exists();
// Store the slic3r.ini into a user profile directory (or a datadir, if configured).
void save();
// Does this config need to be saved?
bool dirty() const { return m_dirty; }
// False for ten seconds after a failed write, so the idle handler does not
// repeat a hopeless attempt on every event; an explicit save() always tries.
bool save_due() const { return std::chrono::steady_clock::now() >= m_retry_save_at; }
void set_dirty() { m_dirty = true; }
@@ -344,8 +339,6 @@ public:
// Get the default config path from Slic3r::data_dir().
std::string config_path();
// Lock file guarding config_path() against other running instances; empty without a data dir.
std::string lock_path();
// Returns true if the user's data directory comes from before Slic3r 1.40.0 (no updating)
bool legacy_datadir() const { return m_legacy_datadir; }
@@ -456,16 +449,8 @@ private:
// Preset for each machine
MachineSettingMap m_printer_settings;
// Writes the assembled config text, and on Windows its checksum and a backup copy; false when the
// config itself could not be written, in which case the caller stays dirty and retries. `checksum_source`
// is the text load() will verify, which for the JSON config ends before the trailing newline.
bool write_config_file(const std::string &path, std::string body, const std::string &checksum_source);
// Has any value been modified since the config.ini has been last saved or loaded?
bool m_dirty;
// After a failed write, save_due() is false for the next ten seconds, so the
// idle handler does not repeat a hopeless write on every event.
std::chrono::steady_clock::time_point m_retry_save_at{};
// Original version found in the ini file before it was overwritten
Semver m_orig_version;
// Whether the existing version is before system profiles & configuration updating
-2
View File
@@ -306,8 +306,6 @@ set(lisbslic3r_sources
Geometry/VoronoiUtils.cpp
Geometry/VoronoiUtils.hpp
Geometry/VoronoiVisualUtils.hpp
InstanceLock.cpp
InstanceLock.hpp
Int128.hpp
KDTreeIndirect.hpp
Layer.cpp
+5 -3
View File
@@ -1538,9 +1538,11 @@ void ConfigBase::save_to_json(const std::string &file, const std::string &name,
// Serialize first: if that throws (invalid UTF-8), the existing file stays untouched.
std::ostringstream ss;
this->save_to_json(ss, name, from, version);
if (const std::error_code ec = write_file_atomically(file, ss.str()))
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << boost::format(": failed to save config to %1%: %2%") % file % ec.message();
else
boost::nowide::ofstream c;
c.open(file, std::ios::out | std::ios::trunc);
c << ss.str();
c.close();
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file;
}
-165
View File
@@ -1,165 +0,0 @@
#include "InstanceLock.hpp"
#include <map>
#include <memory>
#include <system_error>
#include <thread>
#include <boost/filesystem.hpp>
#include <boost/log/trivial.hpp>
#include <boost/nowide/fstream.hpp>
#ifdef _WIN32
#include <boost/interprocess/sync/file_lock.hpp>
#include <boost/nowide/convert.hpp>
#else
#include <cerrno>
#include <fcntl.h>
#include <sys/file.h>
#include <unistd.h>
#endif
namespace Slic3r {
#ifdef _WIN32
// LockFileEx, held by this handle alone.
using NativeFileLock = boost::interprocess::file_lock;
#else
// flock(2) rather than an fcntl lock: it belongs to this open file description,
// so any other code in the process that opens and closes the lock file, as a
// backup or an export walking the data dir might, cannot drop it. An fcntl
// lock would go with the first such close.
class NativeFileLock
{
public:
explicit NativeFileLock(const char *path) : m_fd(::open(path, O_RDWR | O_CREAT | O_CLOEXEC, 0644))
{
if (m_fd < 0)
throw std::system_error(errno, std::generic_category(), path);
}
~NativeFileLock() { ::close(m_fd); }
bool try_lock()
{
if (::flock(m_fd, LOCK_EX | LOCK_NB) == 0)
return true;
// A signal (a child exiting, for one) interrupts the call like any other; the caller polls again.
if (errno == EWOULDBLOCK || errno == EINTR)
return false;
throw std::system_error(errno, std::generic_category(), "flock");
}
void unlock() { ::flock(m_fd, LOCK_UN); }
private:
int m_fd;
};
#endif
// One slot per lock file, shared by every guard in the process: one lock
// object per path behind a mutex is what makes the guard re-entrant and safe
// to use from the preset sync thread and the GUI thread at once. The lock
// file is opened by the outermost guard and closed when it goes, so the file
// is never held open between guards: whatever is at the path is what gets
// locked, and a data dir can be removed once nothing is saving into it. The
// file is kept rather than deleted on release because the lock state lives in
// the kernel on the open file, and deleting it would let a third instance
// lock a fresh file while the second still holds the old one.
struct InstanceLock::Slot
{
std::recursive_mutex mutex;
// Non-null exactly while this process holds the file lock.
std::unique_ptr<NativeFileLock> file_lock;
int depth{0};
// Until this point, after a guard could not open, lock or wait out the
// file, guards do not touch it.
std::chrono::steady_clock::time_point cooldown_until{};
};
InstanceLock::Slot &InstanceLock::slot_for(const std::string &lock_file_path)
{
// Never freed: a save during static destruction still needs its slot.
static auto *registry_mutex = new std::mutex();
static auto *registry = new std::map<std::string, std::unique_ptr<Slot>>();
std::lock_guard<std::mutex> guard(*registry_mutex);
std::unique_ptr<Slot> &slot = (*registry)[lock_file_path];
if (! slot)
slot = std::make_unique<Slot>();
return *slot;
}
// Starts the cool-down. Called with the slot mutex held.
void InstanceLock::defer(Slot &slot, const std::string &reason)
{
slot.cooldown_until = std::chrono::steady_clock::now() + cooldown;
BOOST_LOG_TRIVIAL(warning) << reason << "; proceeding without the lock for the next " << cooldown.count() << " ms";
}
// Creates the lock file if needed and opens it, or starts the cool-down.
// Called with the slot mutex held.
bool InstanceLock::open_lock_file(Slot &slot, const std::string &lock_file_path)
{
try {
#ifdef _WIN32
// The lock opens an existing file; created once, on the first miss.
const std::wstring wide_path = boost::nowide::widen(lock_file_path);
try {
slot.file_lock = std::make_unique<NativeFileLock>(wide_path.c_str());
} catch (const std::exception &) {
boost::nowide::ofstream(lock_file_path, std::ios::app).close();
slot.file_lock = std::make_unique<NativeFileLock>(wide_path.c_str());
}
#else
slot.file_lock = std::make_unique<NativeFileLock>(lock_file_path.c_str());
#endif
return true;
} catch (const std::exception &e) {
defer(slot, "Cannot open lock file " + lock_file_path + ": " + e.what() + " (check its owner and permissions)");
return false;
}
}
InstanceLock::InstanceLock(const std::string &lock_file_path, std::chrono::milliseconds timeout)
{
if (lock_file_path.empty())
return;
m_slot = &slot_for(lock_file_path);
m_slot_guard = std::unique_lock<std::recursive_mutex>(m_slot->mutex);
const auto now = std::chrono::steady_clock::now();
if (m_slot->depth == 0 && now >= m_slot->cooldown_until && open_lock_file(*m_slot, lock_file_path)) {
const auto deadline = now + timeout;
bool taken = false;
for (;;) {
try {
if ((taken = m_slot->file_lock->try_lock()))
break;
} catch (const std::exception &e) {
defer(*m_slot, "Cannot lock " + lock_file_path + ": " + e.what());
break;
}
if (std::chrono::steady_clock::now() >= deadline) {
defer(*m_slot, "Another instance has held " + lock_file_path + " for over " + std::to_string(timeout.count()) + " ms");
break;
}
std::this_thread::sleep_for(std::chrono::milliseconds(5));
}
if (! taken)
m_slot->file_lock.reset();
}
// Counted last, so a throw above leaves the slot exactly as it was found.
++ m_slot->depth;
m_locked = m_slot->file_lock != nullptr;
}
InstanceLock::~InstanceLock()
{
if (m_slot == nullptr)
return;
if (-- m_slot->depth == 0 && m_slot->file_lock) {
try {
m_slot->file_lock->unlock();
} catch (const std::exception &e) {
BOOST_LOG_TRIVIAL(warning) << "Cannot unlock instance lock: " << e.what();
}
m_slot->file_lock.reset();
}
}
} // namespace Slic3r
-60
View File
@@ -1,60 +0,0 @@
#pragma once
#include <chrono>
#include <mutex>
#include <string>
namespace Slic3r {
// Scoped write lock on a file shared by every running instance of the
// application, such as the app config or the user preset directory: threads
// of this process are serialised through a recursive mutex, other processes
// through an advisory OS file lock on `lock_file_path`. The lock file is
// created on first use and kept; the OS releases the lock when its holder
// exits, so a crashed instance never leaves a stale lock behind.
//
// Best effort: when the lock file cannot be opened or locked, or another
// instance still holds it after `timeout`, the guard keeps only the in-process
// mutex, locked() reports false and the write proceeds, since a hung instance
// must never block another one from saving. For `cooldown` afterwards guards
// leave the file alone. The wait for the in-process mutex is bounded only by
// the longest critical section, so a guard covers a few file operations and
// nothing slower.
//
// Lock order: the preset collection mutex may be held when a guard is taken
// (set_sync_info_and_save() calls save_info() under it), never the reverse;
// that is why the guards sit at the leaf readers and writers and why a guard
// must not be added around save_user_presets(), which takes the collection
// mutex through delete_preset().
class InstanceLock
{
public:
// Long against a critical section of milliseconds, short against the GUI
// thread, which is where most guards are taken.
static constexpr std::chrono::milliseconds default_timeout{1000};
// Long enough that a holder stuck in a debugger does not cost a stall per
// save; mutable so tests can shorten it.
static inline std::chrono::milliseconds cooldown{10000};
// An empty path makes the guard a no-op.
explicit InstanceLock(const std::string &lock_file_path, std::chrono::milliseconds timeout = default_timeout);
~InstanceLock();
InstanceLock(const InstanceLock &) = delete;
InstanceLock &operator=(const InstanceLock &) = delete;
// True while this process holds the cross-process file lock.
bool locked() const { return m_locked; }
private:
struct Slot;
static Slot &slot_for(const std::string &lock_file_path);
static bool open_lock_file(Slot &slot, const std::string &lock_file_path);
static void defer(Slot &slot, const std::string &reason);
Slot *m_slot{nullptr};
std::unique_lock<std::recursive_mutex> m_slot_guard;
bool m_locked{false};
};
} // namespace Slic3r
+70 -153
View File
@@ -51,9 +51,6 @@
#include "libslic3r.h"
#include "LifecycleEvents.hpp"
#include "Utils.hpp"
#include "InstanceLock.hpp"
#include <sstream>
#include "Time.hpp"
#include "PlaceholderParser.hpp"
#include "libslic3r/GCode/Thumbnails.hpp"
@@ -110,32 +107,6 @@ std::string get_preset_canonical_name(const std::string &preset_bare_name, const
}
}
std::string user_presets_lock_path(bool read_only)
{
return read_only || data_dir().empty() ? std::string() : (fs::path(data_dir()) / (PRESET_USER_DIR ".lock")).string();
}
// Removes a preset file the scan could not load, and its .info, under the lock.
// The scan passes read_only when it could not take the lock, since the file may
// then be another instance's fresh write that it merely raced.
static void remove_preset_files(const std::string &preset_file, bool read_only)
{
if (read_only)
return;
InstanceLock instance_lock(user_presets_lock_path());
// A symlink whose target is missing stays, since the target may come back; one
// that cannot even be followed is removed like any other unreadable file.
auto remove = [](const fs::path &file) {
boost::system::error_code ec;
if (fs::status(file, ec).type() != fs::file_not_found)
fs::remove(file, ec);
};
fs::path file_path(preset_file);
remove(file_path);
file_path.replace_extension(".info");
remove(file_path);
}
std::string get_preset_bare_name(const std::string &canonical_name)
{
const auto pos = canonical_name.find_last_of('/');
@@ -678,20 +649,18 @@ void Preset::save_info(std::string file)
file = idx_file.string();
}
boost::nowide::ofstream c;
c.open(file, std::ios::out | std::ios::trunc);
std::string sync_info_to_save;
//BBS: hold is used for stop requesting to server this time
if (this->sync_info.compare("hold") != 0)
sync_info_to_save = this->sync_info;
std::ostringstream c;
c << "sync_info" << " = " << sync_info_to_save << std::endl;
c << "user_id" << " = " << this->user_id << std::endl;
c << "setting_id" << " = " << this->setting_id << std::endl;
c << "base_id" << " = " << this->base_id << std::endl;
c << "updated_time" << " = " << std::to_string(this->updated_time) << std::endl;
InstanceLock instance_lock(user_presets_lock_path());
if (const std::error_code ec = write_file_atomically(file, c.str()))
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to save " << file << ": " << ec.message();
c.close();
}
void Preset::remove_files(bool cloud_already_deleted)
@@ -700,7 +669,6 @@ void Preset::remove_files(bool cloud_already_deleted)
if (this->is_project_embedded) {
return;
}
InstanceLock instance_lock(user_presets_lock_path());
// Erase the preset file.
boost::nowide::remove(this->file.c_str());
fs::path idx_path(this->file);
@@ -718,11 +686,11 @@ void Preset::remove_files(bool cloud_already_deleted)
}
//BBS: add logic for only difference save
bool Preset::save(DynamicPrintConfig* parent_config)
void Preset::save(DynamicPrintConfig* parent_config)
{
//BBS: add project embedded preset logic
if (this->is_project_embedded)
return true;
return;
//BBS: change to json format
//this->config.save(this->file);
std::string from_str;
@@ -737,16 +705,12 @@ bool Preset::save(DynamicPrintConfig* parent_config)
else
from_str = std::string("Default");
boost::filesystem::create_directories(fs::path(this->file).parent_path());
const std::string bare_name = get_preset_bare_name(this->name);
// What gets written: the diff against the parent, the config plus its
// filament id, or the config as is. Built before the lock is taken so the
// exclusive window covers only the file writes.
DynamicPrintConfig temp_config;
const DynamicPrintConfig *to_save = &this->config;
//BBS: only save difference if it has parent
if (parent_config) {
DynamicPrintConfig temp_config;
std::vector<std::string> dirty_options = config.diff(*parent_config);
std::string extruder_id_name, extruder_variant_name;
@@ -782,22 +746,13 @@ bool Preset::save(DynamicPrintConfig* parent_config)
opt_dst->set(opt_src);
}
}
to_save = &temp_config;
temp_config.save_to_json(this->file, bare_name, from_str, this->version.to_string());
} else if (!filament_id.empty() && inherits().empty()) {
temp_config = config;
DynamicPrintConfig temp_config = config;
temp_config.set_key_value(BBL_JSON_KEY_FILAMENT_ID, new ConfigOptionString(filament_id));
to_save = &temp_config;
}
std::ostringstream json;
to_save->save_to_json(json, bare_name, from_str, this->version.to_string());
InstanceLock instance_lock(user_presets_lock_path());
boost::filesystem::create_directories(fs::path(this->file).parent_path());
if (const std::error_code ec = write_file_atomically(this->file, json.str())) {
// No .info either: one without its preset reads as a cloud deletion request.
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to save " << this->file << ": " << ec.message();
return false;
temp_config.save_to_json(this->file, bare_name, from_str, this->version.to_string());
} else {
this->config.save_to_json(this->file, bare_name, from_str, this->version.to_string());
}
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " save config for: " << this->name << " and filament_id: " << filament_id << " and base_id: " << this->base_id;
@@ -807,7 +762,6 @@ bool Preset::save(DynamicPrintConfig* parent_config)
idx_file.replace_extension(".info");
this->save_info(idx_file.string());
}
return true;
}
void Preset::reload(Preset const &parent)
@@ -819,7 +773,6 @@ void Preset::reload(Preset const &parent)
std::string reason;
ForwardCompatibilitySubstitutionRule substitution_rule = ForwardCompatibilitySubstitutionRule::Disable;
try {
InstanceLock instance_lock(user_presets_lock_path());
ConfigSubstitutions config_substitutions = config.load_from_json(file, substitution_rule, key_values, reason);
this->config = parent.config;
this->config.apply(std::move(config));
@@ -1023,6 +976,37 @@ void Preset::get_extruder_names_and_keysets(Type type, std::string& extruder_id_
}
}
DynamicPrintConfig Preset::load_external_config(Type type, const DynamicPrintConfig &default_config, const DynamicPrintConfig &project_config,
const std::set<std::string> &different_settings_list,
const std::function<DynamicPrintConfig *(const std::string &inherits)> &find_base,
t_config_option_keys *keys)
{
// Load the preset over a default preset, so that the missing fields are filled in from the default preset.
DynamicPrintConfig cfg(default_config);
// SoftFever: ignore print connection info from project
auto cfg_keys = cfg.keys();
cfg_keys.erase(std::remove_if(cfg_keys.begin(), cfg_keys.end(),
[](std::string &val) {
return val == "print_host" || val == "print_host_webui" || val == "printhost_apikey" ||
val == "printhost_cafile" || val == "printhost_user" || val == "printhost_password" || val == "printhost_port";
}),
cfg_keys.end());
cfg.apply_only(project_config, cfg_keys, true);
//add different settings check logic, replace the old system preset's default value with new system preset's default values
if (!different_settings_list.empty()) {
if (DynamicPrintConfig *base_config = find_base(Preset::inherits(cfg))) {
std::string extruder_id_name, extruder_variant_name;
std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
Preset::get_extruder_names_and_keysets(type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2);
cfg.update_non_diff_values_to_base_config(*base_config, cfg_keys, different_settings_list, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
}
}
if (keys)
*keys = std::move(cfg_keys);
return cfg;
}
bool Preset::has_lidar(PresetBundle *preset_bundle)
{
bool has_lidar = false;
@@ -1718,22 +1702,6 @@ std::string PresetCollection::canonical_preset_name(const std::string &name, con
return get_preset_canonical_name(parsed.bare, origin);
}
PresetCollection::PresetFilesOnDisk PresetCollection::PresetFilesOnDisk::read(const boost::filesystem::path &file)
{
auto read_if_present = [](const fs::path &path) -> std::optional<std::string> {
std::string bytes;
try {
load_string_file(path, bytes);
} catch (const std::exception &) {
return std::nullopt;
}
return bytes;
};
fs::path info_path(file);
info_path.replace_extension(".info");
return { read_if_present(file), read_if_present(info_path) };
}
PresetCollection::UserPresetLoad PresetCollection::resolve_user_preset(
const boost::filesystem::path &file, const std::string &canonical_name,
const PresetOrigin &load_origin, ForwardCompatibilitySubstitutionRule substitution_rule,
@@ -1745,9 +1713,6 @@ PresetCollection::UserPresetLoad PresetCollection::resolve_user_preset(
Preset &preset = out.preset;
preset.bundle_id = load_origin.bundle_id;
preset.file = file.string();
// Before either file is parsed, so a save that lands during the parse still shows
// up as a difference when commit compares.
out.on_disk = PresetFilesOnDisk::read(file);
// Load the preset file, apply preset values on top of defaults.
try {
fs::path idx_path(preset.file);
@@ -1860,8 +1825,14 @@ void PresetCollection::commit_user_preset(UserPresetLoad &&loaded, std::deque<Pr
++m_errors;
BOOST_LOG_TRIVIAL(error) << error;
}
if (loaded.discard_file)
remove_preset_files(loaded.preset.file, read_only);
if (loaded.discard_file && !read_only) {
fs::path file_path(loaded.preset.file);
if (fs::exists(file_path))
fs::remove(file_path);
file_path.replace_extension(".info");
if (fs::exists(file_path))
fs::remove(file_path);
}
if (!loaded.install)
return;
@@ -1869,12 +1840,9 @@ void PresetCollection::commit_user_preset(UserPresetLoad &&loaded, std::deque<Pr
if (loaded.save_compatible_printers) {
// A filesystem error from the rewrite is counted, and the preset still loads.
try {
if (read_only)
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " added compatible_printers for preset: " << preset.name << " (not written back)";
else if (preset.save(nullptr))
if (!read_only)
preset.save(nullptr);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " added compatible_printers for preset: " << preset.name;
else
++m_errors; // save() logged why
} catch (const std::runtime_error &err) {
++m_errors;
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " can not write compatible_printers back to " << preset.file << ": " << err.what();
@@ -1960,32 +1928,11 @@ void PresetCollection::load_presets(
return this->resolve_user_preset(files[i].path, files[i].canonical_name, resolved_origin, substitution_rule,
extruder_id_name, extruder_variant_name, key_set1, key_set2);
},
[&](size_t i, UserPresetLoad &&loaded) {
// Resolve read the files without the lock, so another instance may have saved
// over them since. A file that changed is resolved again under the lock before
// commit removes or rewrites it, which also keeps its .json and .info from two
// different saves apart. Without the lock nothing is checked, so commit leaves
// the files alone.
const std::string lock_path = user_presets_lock_path(read_only);
InstanceLock instance_lock(lock_path);
if (instance_lock.locked()) {
const PresetFilesOnDisk on_disk = PresetFilesOnDisk::read(files[i].path);
boost::system::error_code ec;
// The link itself, and a stat error counts as present, so a file that is
// there but cannot be read is still counted and removed by commit.
if (! on_disk.json && fs::symlink_status(files[i].path, ec).type() == fs::file_not_found)
return; // removed by another instance since it was read
if (! (on_disk == loaded.on_disk)) {
CNumericLocalesSetter locales_setter;
loaded = this->resolve_user_preset(files[i].path, files[i].canonical_name, resolved_origin, substitution_rule,
extruder_id_name, extruder_variant_name, key_set1, key_set2);
}
}
const bool leave_files = read_only || (! lock_path.empty() && ! instance_lock.locked());
[&](size_t, UserPresetLoad &&loaded) {
// Committing can remove an unreadable preset's file, and a filesystem error
// there is reported without stopping the rest of the directory.
try {
this->commit_user_preset(std::move(loaded), presets_loaded, substitutions, preset_loaded_fn, leave_files);
this->commit_user_preset(std::move(loaded), presets_loaded, substitutions, preset_loaded_fn, read_only);
} catch (const std::runtime_error &err) {
errors_cummulative += err.what();
errors_cummulative += "\n";
@@ -2295,10 +2242,7 @@ void PresetCollection::set_sync_info_and_save(std::string name, std::string sett
preset->setting_id = setting_id;
if (update_time > 0)
preset->updated_time = update_time;
if (preset->sync_info == "update")
preset->save(nullptr);
else
preset->save_info();
preset->sync_info == "update" ? preset->save(nullptr) : preset->save_info();
break;
}
}
@@ -2727,20 +2671,6 @@ std::pair<Preset*, bool> PresetCollection::load_external_preset(
const Semver file_version,
const std::string filament_id)
{
// Load the preset over a default preset, so that the missing fields are filled in from the default preset.
DynamicPrintConfig cfg(this->default_preset_for(combined_config).config);
// SoftFever: ignore print connection info from project
auto keys = cfg.keys();
keys.erase(std::remove_if(keys.begin(), keys.end(),
[](std::string &val) {
return val == "print_host" || val == "print_host_webui" || val == "printhost_apikey" ||
val == "printhost_cafile" || val == "printhost_user" || val == "printhost_password" || val == "printhost_port";
}),
keys.end());
cfg.apply_only(combined_config, keys, true);
std::string &inherits = Preset::inherits(cfg);
//add different settings check logic, replace the old system preset's default value with new system preset's default values
std::deque<Preset>::iterator it = this->find_preset_internal(original_name);
bool found = it != m_presets.end() && it->name == original_name;
if (! found) {
@@ -2749,24 +2679,25 @@ std::pair<Preset*, bool> PresetCollection::load_external_preset(
found = it != m_presets.end();
}
std::string extruder_id_name, extruder_variant_name;
std::set<std::string> *key_set1 = nullptr, *key_set2 = nullptr;
Preset::get_extruder_names_and_keysets(m_type, extruder_id_name, extruder_variant_name, &key_set1, &key_set2);
if (!inherits.empty() && (different_settings_list.size() > 0)) {
t_config_option_keys keys;
DynamicPrintConfig cfg = Preset::load_external_config(m_type, this->default_preset_for(combined_config).config, combined_config, different_settings_list,
[this, &original_name, &path, found, it](const std::string &inherits) -> DynamicPrintConfig * {
if (!inherits.empty()) {
auto iter = this->find_preset_internal(inherits);
if (iter == m_presets.end() || iter->name != inherits)
iter = this->find_preset_renamed(inherits);
if (iter != m_presets.end()) {
//std::vector<std::string> dirty_options = cfg.diff(iter->config);
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": change preset %1% inherit %2% 's value to %3% 's values")%original_name %inherits %path;
cfg.update_non_diff_values_to_base_config(iter->config, keys, different_settings_list, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
if (iter == m_presets.end())
return nullptr;
BOOST_LOG_TRIVIAL(info) << "load_external_preset" << boost::format(": change preset %1% inherit %2% 's value to %3% 's values")%original_name %inherits %path;
return &iter->config;
}
if (found && it->is_system) {
BOOST_LOG_TRIVIAL(info) << "load_external_preset" << boost::format(": change preset %1% 's value to %2% 's values")%original_name %path;
return &it->config;
}
else if (found && it->is_system && (different_settings_list.size() > 0)) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << boost::format(": change preset %1% 's value to %2% 's values")%original_name %path;
cfg.update_non_diff_values_to_base_config(it->config, keys, different_settings_list, extruder_id_name, extruder_variant_name, *key_set1, *key_set2);
}
return nullptr;
}, &keys);
std::string &inherits = Preset::inherits(cfg);
//BBS: add config related logs
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, type %1% , path %2%, name %3%, original_name %4%, inherits %5%")%Preset::get_type_string(m_type) %path %name %original_name %inherits;
@@ -4380,15 +4311,8 @@ void PhysicalPrinter::update_preset_names_in_config()
}
}
void PhysicalPrinter::save(DynamicPrintConfig* /* parent_config */)
{
InstanceLock instance_lock(user_presets_lock_path());
this->config.save_to_json(this->file, std::string("Physical_Printer"), std::string("User"), std::string(SLIC3R_VERSION));
}
void PhysicalPrinter::save(const std::string& file_name_from, const std::string& file_name_to)
{
InstanceLock instance_lock(user_presets_lock_path());
// rename the file
boost::nowide::rename(file_name_from.data(), file_name_to.data());
this->file = file_name_to;
@@ -4520,7 +4444,6 @@ void PhysicalPrinterCollection::load_printers(
continue;
}
try {
InstanceLock instance_lock(user_presets_lock_path());
PhysicalPrinter printer(name, this->default_config());
printer.file = dir_entry.path().string();
// Load the preset file, apply preset values on top of defaults.
@@ -4713,10 +4636,7 @@ bool PhysicalPrinterCollection::delete_printer(const std::string& name)
const PhysicalPrinter& printer = *it;
// Erase the preset file.
{
InstanceLock instance_lock(user_presets_lock_path());
boost::nowide::remove(printer.file.c_str());
}
m_printers.erase(it);
return true;
}
@@ -4728,10 +4648,7 @@ bool PhysicalPrinterCollection::delete_selected_printer()
const PhysicalPrinter& printer = this->get_selected_printer();
// Erase the preset file.
{
InstanceLock instance_lock(user_presets_lock_path());
boost::nowide::remove(printer.file.c_str());
}
// Remove the preset from the list.
m_printers.erase(m_printers.begin() + m_idx_selected);
// unselect all printers
+11 -23
View File
@@ -8,7 +8,6 @@
#include <unordered_set>
#include <functional>
#include <mutex>
#include <optional>
#include <boost/algorithm/string/predicate.hpp>
#include <boost/filesystem/path.hpp>
#include <boost/property_tree/ptree_fwd.hpp>
@@ -331,8 +330,7 @@ public:
//BBS: add logic for only difference save
//if parent_config is null, save all keys, otherwise, only save difference
// Returns false when the preset file could not be written.
bool save(DynamicPrintConfig* parent_config);
void save(DynamicPrintConfig* parent_config);
void reload(Preset const & parent);
// Return a label of this preset, consisting of a name and a "(modified)" suffix, if this preset is dirty.
@@ -405,6 +403,15 @@ public:
std::string get_current_printer_type(PresetBundle *preset_bundle); // get current preset type
static void get_extruder_names_and_keysets(Type type, std::string& extruder_id_name, std::string& extruder_variant_name, std::set<std::string>** p_key_set1, std::set<std::string>** p_key_set2);
// Config of a preset loaded from a project or config file: the project's values over the type's
// default preset config, without the print-host keys. When different_settings_list is not empty,
// every key not listed in it is then refreshed from the base system preset, which find_base returns
// for the project's "inherits" (nullptr when there is none), with the listed per-variant values
// mapped onto the base's extruder variants. keys, if given, receives the keys taken from the project.
static DynamicPrintConfig load_external_config(Type type, const DynamicPrintConfig &default_config, const DynamicPrintConfig &project_config,
const std::set<std::string> &different_settings_list,
const std::function<DynamicPrintConfig *(const std::string &inherits)> &find_base,
t_config_option_keys *keys = nullptr);
std::string get_printer_id() const { return vendor ? vendor->id : ""; }
bool has_lidar(PresetBundle *preset_bundle);
@@ -490,12 +497,6 @@ std::string get_preset_canonical_name(const std::string &preset_bare_name, const
// Tail segment of a canonical name — what's written to the bundle's .json filename and JSON "name" field.
std::string get_preset_bare_name(const std::string &canonical_name);
// Lock file guarding every user preset file under data_dir() against other
// running instances and the preset sync thread. Empty without a data dir, and
// for a read-only load (the CLI), which never rewrites or deletes and may run
// many jobs on one data dir.
std::string user_presets_lock_path(bool read_only = false);
// Resolve an origin from a directory path when the caller passes Kind::Auto.
PresetOrigin detect_origin_from_path(const boost::filesystem::path &path, const PresetOrigin &explicit_origin = PresetOrigin());
@@ -905,16 +906,6 @@ protected:
void set_custom_preset_alias(Preset &preset);
private:
// A preset file and its .info as read from disk, std::nullopt for one that is missing.
struct PresetFilesOnDisk
{
std::optional<std::string> json;
std::optional<std::string> info;
static PresetFilesOnDisk read(const boost::filesystem::path &file);
bool operator==(const PresetFilesOnDisk &rhs) const { return json == rhs.json && info == rhs.info; }
};
// One preset file read and flattened against the presets already in this
// collection, before anything the collection shares has been touched.
struct UserPresetLoad
@@ -932,9 +923,6 @@ private:
bool discard_file { false };
// The .info file read beside the preset, which commit logs.
std::string info_file;
// Both files as they were before the preset was read from them. Resolve runs
// without the instance lock, so commit compares this with the disk under it.
PresetFilesOnDisk on_disk;
// Counted and logged by commit, in the order the directory listed the files.
std::vector<std::string> errors;
PresetsConfigSubstitutions substitutions;
@@ -1123,7 +1111,7 @@ public:
//BBS: change to json format
//void save() { this->config.save(this->file); }
void save(DynamicPrintConfig* parent_config);
void save(DynamicPrintConfig* parent_config) { this->config.save_to_json(this->file, std::string("Physical_Printer"), std::string("User"), std::string(SLIC3R_VERSION)); }
void save(const std::string& file_name_from, const std::string& file_name_to);
void update_from_preset(const Preset& preset);
+69 -70
View File
@@ -1,4 +1,3 @@
#include <atomic>
#include <cassert>
#include <chrono>
#include <ctime>
@@ -14,7 +13,6 @@
#include "libslic3r.h"
#include "I18N.hpp"
#include "Utils.hpp"
#include "InstanceLock.hpp"
#include "LocalesUtils.hpp"
#include "Model.hpp"
#include "TriangleSelector.hpp"
@@ -583,18 +581,20 @@ bool PresetBundle::resolve_preset_config(DynamicPrintConfig &config, Preset::Typ
const PresetBundle *PresetBundle::load_source_vendor(const boost::filesystem::path &root_dir,
const std::string &vendor_id,
ForwardCompatibilitySubstitutionRule compatibility_rule,
std::string &error)
std::string &error, bool allow_cache)
{
auto key = std::make_tuple(root_dir.string(), vendor_id, compatibility_rule);
auto key = std::make_tuple(root_dir.string(), vendor_id, compatibility_rule, allow_cache);
if (auto it = m_source_vendor_bundles.find(key); it != m_source_vendor_bundles.end())
return it->second.get();
// The library loads with no base of its own, so the tree a vendor inherits from
// is the same one that resolves the library's own presets.
const std::string library_file = std::string(ORCA_FILAMENT_LIBRARY);
const PresetBundle *library = nullptr;
if (vendor_id != ORCA_FILAMENT_LIBRARY &&
boost::filesystem::is_regular_file(root_dir / (std::string(ORCA_FILAMENT_LIBRARY) + ".json"))) {
library = load_source_vendor(root_dir, ORCA_FILAMENT_LIBRARY, compatibility_rule, error);
(boost::filesystem::is_regular_file(root_dir / (library_file + ".json")) ||
(allow_cache && boost::filesystem::is_regular_file(root_dir / (library_file + ".opc"))))) {
library = load_source_vendor(root_dir, ORCA_FILAMENT_LIBRARY, compatibility_rule, error, allow_cache);
if (library == nullptr) {
error = "OrcaFilamentLibrary contains invalid presets";
return nullptr;
@@ -603,7 +603,7 @@ const PresetBundle *PresetBundle::load_source_vendor(const boost::filesystem::pa
auto bundle = std::make_unique<PresetBundle>();
bundle->m_preserve_vendor_source_paths = true;
bundle->load_vendor_configs_from_json(root_dir.string(), vendor_id, LoadSystem, compatibility_rule, library, false);
bundle->load_vendor_configs_from_json(root_dir.string(), vendor_id, LoadSystem, compatibility_rule, library, allow_cache);
if (bundle->error_count() != 0) {
error = "Vendor bundle contains invalid presets";
return nullptr;
@@ -646,6 +646,43 @@ bool PresetBundle::resolve_preset_config_type(DynamicPrintConfig &config, Preset
return true;
}
bool PresetBundle::resolve_system_preset(DynamicPrintConfig &config, Preset::Type type, const std::string &name,
ForwardCompatibilitySubstitutionRule compatibility_rule, std::string &error)
{
const std::string vendor_id = find_preset_vendor(name, type);
if (vendor_id.empty()) {
error = "No vendor lists the preset";
return false;
}
// Release builds ship a vendor as its preset cache alone, without the profile JSONs.
auto installed = [&vendor_id](const fs::path &root) {
return fs::is_regular_file(root / (vendor_id + ".json")) || fs::is_regular_file(root / (vendor_id + ".opc"));
};
fs::path root_dir = fs::path(data_dir()) / PRESET_SYSTEM_DIR;
if (!installed(root_dir))
root_dir = fs::path(resources_dir()) / PRESET_PROFILES_DIR;
const bool cache_only = !fs::is_regular_file(root_dir / (vendor_id + ".json"));
try {
const PresetBundle *vendor = load_source_vendor(root_dir, vendor_id, compatibility_rule, error, cache_only);
if (vendor == nullptr)
return false;
const PresetCollection &collection = type == Preset::TYPE_PRINTER ? vendor->printers :
type == Preset::TYPE_PRINT ? vendor->prints : vendor->filaments;
const Preset *preset = collection.find_preset(name, false);
if (preset == nullptr) {
error = "Preset was not found in its vendor bundle";
return false;
}
config = preset->config;
} catch (const std::exception &ex) {
error = ex.what();
return false;
}
error.clear();
return true;
}
PresetBundle::PresetBundle(const PresetBundle &rhs)
{
*this = rhs;
@@ -1235,13 +1272,6 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For
const auto user_load_t0 = std::chrono::steady_clock::now();
// Reads one bundle's metadata under the lock, per file, so the lock is never
// held when bundles.WriteLock() is taken afterwards.
auto load_bundle_metadata = [read_only](const fs::path &metadata_file, BundleMetadata &metadata) {
InstanceLock instance_lock(user_presets_lock_path(read_only));
return metadata.load_from_json(metadata_file.string());
};
// Load bundle metadata from _local directory first
fs::path local_dir(folder / PRESET_LOCAL_DIR);
if (fs::exists(local_dir)) {
@@ -1255,7 +1285,7 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For
if (!fs::exists(metadata_file)) continue;
BundleMetadata metadata;
if (!load_bundle_metadata(metadata_file, metadata)) continue;
if (!metadata.load_from_json(metadata_file.string())) continue;
metadata.print_presets.clear();
metadata.filament_presets.clear();
metadata.printer_presets.clear();
@@ -1290,7 +1320,7 @@ PresetsConfigSubstitutions PresetBundle::load_user_presets(std::string user, For
if (!fs::exists(metadata_file)) continue;
BundleMetadata metadata;
if (!load_bundle_metadata(metadata_file, metadata)) continue;
if (!metadata.load_from_json(metadata_file.string())) continue;
metadata.print_presets.clear();
metadata.filament_presets.clear();
metadata.printer_presets.clear();
@@ -1624,12 +1654,10 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector<std::string>
if (ec) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " create directory failed: " << ec.message();
//create temp folder
//std::string user_default_temp_dir = data_dir() + "/" + PRESET_USER_DIR + "/" + DEFAULT_USER_FOLDER_NAME + "/" + "temp";
// Under cache/, per process and per import, so two instances importing
// at once do not clear each other's extraction and no preset scan reads it.
static std::atomic<unsigned> import_counter{0};
fs::path temp_folder(fs::path(data_dir()) / "cache" / ("import." + std::to_string(get_current_pid()) + "." + std::to_string(import_counter++)));
fs::path temp_folder(configs_folder / "temp");
std::string user_default_temp_dir = temp_folder.make_preferred().string();
fs::create_directories(temp_folder, ec);
if (fs::exists(temp_folder)) fs::remove_all(temp_folder);
fs::create_directory(temp_folder, ec);
if (ec) BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " create directory failed: " << ec.message();
file = boost::filesystem::path(file).make_preferred().string();
@@ -1641,9 +1669,6 @@ PresetsConfigSubstitutions PresetBundle::import_presets(std::vector<std::string>
status = mz_zip_reader_init_cfile(&zip_archive, zipFile, 0, MZ_ZIP_FLAG_CASE_SENSITIVE | MZ_ZIP_FLAG_IGNORE_PATH);
if (MZ_FALSE == status) {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Failed to initialize reader ZIP archive";
if (zipFile != nullptr)
std::fclose(zipFile);
fs::remove_all(temp_folder, ec);
return substitutions;
} else {
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " Success to initialize reader ZIP archive";
@@ -2251,9 +2276,10 @@ void PresetBundle::remove_user_presets_directory(const std::string preset_folder
return;
}
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, delete directory : %1%") % dir_user_presets;
boost::system::error_code ec;
InstanceLock instance_lock(user_presets_lock_path());
fs::remove_all(fs::path(dir_user_presets), ec);
fs::path folder(dir_user_presets);
if (fs::exists(folder)) {
fs::remove_all(folder);
}
}
void PresetBundle::update_system_preset_setting_ids(std::map<std::string, std::map<std::string, std::string>>& system_presets)
@@ -4678,6 +4704,15 @@ const std::set<std::string> ignore_settings_list ={
"print_settings_id", "filament_settings_id", "printer_settings_id"
};
std::set<std::string> PresetBundle::project_different_keys(const std::string &different_settings)
{
std::vector<std::string> keys;
Slic3r::unescape_strings_cstyle(different_settings, keys);
std::set<std::string> keys_set(keys.begin(), keys.end());
keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end());
return keys_set;
}
DynamicPrintConfig PresetBundle::full_fff_config(bool apply_extruder, std::optional<std::vector<int>> filament_maps_new, std::optional<std::vector<int>> filament_volume_maps_new) const
{
DynamicPrintConfig out;
@@ -5385,15 +5420,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
if (!is_published) {
//BBS: add different settings logic
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load print preset from print_settings_id");
std::vector<std::string> print_different_keys_vector;
std::string print_different_settings = different_values[0];
Slic3r::unescape_strings_cstyle(print_different_settings, print_different_keys_vector);
std::set<std::string> print_different_keys_set(print_different_keys_vector.begin(), print_different_keys_vector.end());
//if (!has_different_settings_to_system) {
// print_different_keys_set.clear();
//}
//else
print_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end());
std::set<std::string> print_different_keys_set = project_different_keys(different_values[0]);
if (!print_compatible_printers.empty()) {
ConfigOptionStrings* compatible_printers = config.option<ConfigOptionStrings>("compatible_printers", true);
compatible_printers->values = print_compatible_printers;
@@ -5404,15 +5431,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
//clear compatible printers
clear_compatible_printers(config);
std::vector<std::string> printer_different_keys_vector;
std::string printer_different_settings = different_values[num_filaments + 1];
Slic3r::unescape_strings_cstyle(printer_different_settings, printer_different_keys_vector);
std::set<std::string> printer_different_keys_set(printer_different_keys_vector.begin(), printer_different_keys_vector.end());
//if (!has_different_settings_to_system) {
// printer_different_keys_set.clear();
//}
//else
printer_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end());
std::set<std::string> printer_different_keys_set = project_different_keys(different_values[num_filaments + 1]);
//BBS: add config related logs
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(": load printer preset from printer_settings_id");
load_preset(this->printers, num_filaments + 1, "printer_settings_id", printer_different_keys_set, std::string());
@@ -5431,15 +5450,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
Preset *loaded = nullptr;
//BBS: add different settings logic
std::vector<std::string> filament_different_keys_vector;
std::string filament_different_settings = different_values[1];
Slic3r::unescape_strings_cstyle(filament_different_settings, filament_different_keys_vector);
std::set<std::string> filament_different_keys_set(filament_different_keys_vector.begin(), filament_different_keys_vector.end());
//if (!has_different_settings_to_system) {
// filament_different_keys_set.clear();
//}
//else
filament_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end());
std::set<std::string> filament_different_keys_set = project_different_keys(different_values[1]);
std::string filament_id = filament_ids[0];
//BBS: add config related logs
@@ -5502,15 +5513,7 @@ void PresetBundle::load_config_file_config(const std::string &name_or_path, bool
cfg.opt_string("inherits", true) = inherits_values[i + 1];
//BBS: add different settings logic
std::vector<std::string> filament_different_keys_vector;
std::string filament_different_settings = different_values[i+1];
Slic3r::unescape_strings_cstyle(filament_different_settings, filament_different_keys_vector);
std::set<std::string> filament_different_keys_set(filament_different_keys_vector.begin(), filament_different_keys_vector.end());
//if (!has_different_settings_to_system) {
// filament_different_keys_set.clear();
//}
//else
filament_different_keys_set.insert(ignore_settings_list.begin(), ignore_settings_list.end());
std::set<std::string> filament_different_keys_set = project_different_keys(different_values[i+1]);
std::string filament_id = filament_ids[i];
@@ -8200,13 +8203,9 @@ bool BundleMetadata::save_to_json(const std::string& path) const
j["filament_presets"] = strip_prefix(this->filament_presets);
j["printer_presets"] = strip_prefix(this->printer_presets);
const std::string content = j.dump(4);
InstanceLock instance_lock(user_presets_lock_path());
if (const std::error_code ec = write_file_atomically(path, content)) {
BOOST_LOG_TRIVIAL(error) << "Failed to save bundle metadata to " << path << ": " << ec.message();
return false;
}
return true;
boost::nowide::ofstream ofs(path);
ofs << j.dump(4);
return ofs.good();
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(error) << "Failed to save bundle metadata to " << path << ": " << e.what();
return false;
+10 -2
View File
@@ -235,6 +235,9 @@ public:
// ORCA: utility function to find the vendor for a given preset name
static std::string find_preset_vendor(const std::string& preset_name, Preset::Type type);
// Keys a project keeps when its presets are loaded: those listed in its escaped
// "different_settings_to_system" entry for the preset, plus the preset bookkeeping keys.
static std::set<std::string> project_different_keys(const std::string &different_settings);
PresetBundle();
PresetBundle(const PresetBundle &rhs);
@@ -274,6 +277,10 @@ public:
const std::string &source_file,
ForwardCompatibilitySubstitutionRule compatibility_rule,
std::string &error, bool allow_source_manifest = true);
// Resolve a system preset by name. The vendor tree is read from data_dir()/system when installed
// there, as the GUI reads it, and from the bundled profiles otherwise.
bool resolve_system_preset(DynamicPrintConfig &config, Preset::Type type, const std::string &name,
ForwardCompatibilitySubstitutionRule compatibility_rule, std::string &error);
// Load selections (current print, current filaments, current printer) from config.ini
// This is done just once on application start up.
@@ -808,13 +815,14 @@ private:
// Vendor trees loaded by resolve_preset_config's manifest path, so every preset
// resolved through this bundle shares one load per source root and vendor. The
// filament library is one such tree, shared by every vendor under its root.
std::map<std::tuple<std::string, std::string, ForwardCompatibilitySubstitutionRule>, std::unique_ptr<PresetBundle>>
// A tree read from its preset cache is kept apart: its presets carry no source file.
std::map<std::tuple<std::string, std::string, ForwardCompatibilitySubstitutionRule, bool>, std::unique_ptr<PresetBundle>>
m_source_vendor_bundles;
const PresetBundle *load_source_vendor(const boost::filesystem::path &root_dir,
const std::string &vendor_id,
ForwardCompatibilitySubstitutionRule compatibility_rule,
std::string &error);
std::string &error, bool allow_cache = false);
// Orca: validation only - flag any printer with two or more compatible
// filament presets sharing one filament_id (ambiguous AMS subtype match).
+28 -6
View File
@@ -400,24 +400,46 @@ bool write_cache_blob(const std::string& path, const std::string& blob)
{
boost::crc_32_type crc;
crc.process_bytes(blob.data(), blob.size());
// Written beside the target and moved into place: a cache is truncated and
// rewritten in full, so a write that dies partway would otherwise leave a
// header claiming more body than the file holds.
// Written beside the target and moved into place, as AppConfig::save does:
// a cache is truncated and rewritten in full, so a write that dies partway
// would otherwise leave a header claiming more body than the file holds.
// The PID suffix also keeps two instances writing the same vendor from
// interleaving.
const std::string tmp_path = path + "." + std::to_string(get_current_pid()) + ".tmp";
try {
boost::filesystem::create_directories(boost::filesystem::path(path).parent_path());
{
boost::nowide::ofstream ofs(tmp_path, std::ios::binary | std::ios::trunc);
if (!ofs.is_open()) {
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: cannot open for writing: " << tmp_path;
return false;
}
CacheFileHeader fhdr;
fhdr.magic = CACHE_MAGIC;
fhdr.version = CACHE_VERSION;
fhdr.data_size = static_cast<uint64_t>(blob.size());
fhdr.crc32 = crc.checksum();
const std::string_view header(reinterpret_cast<const char*>(&fhdr), sizeof(fhdr));
if (const std::error_code ec = write_file_atomically(path, { header, std::string_view(blob) }, /*binary=*/true)) {
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << path << "): " << ec.message();
ofs.write(reinterpret_cast<const char*>(&fhdr), sizeof(fhdr));
ofs.write(blob.data(), static_cast<std::streamsize>(blob.size()));
ofs.close(); // flush; close() raises failbit on error
if (! ofs.good()) {
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << tmp_path << ")";
boost::system::error_code ec;
boost::filesystem::remove(tmp_path, ec);
return false;
}
}
if (const std::error_code ec = rename_file(tmp_path, path)) {
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: could not move " << tmp_path << " into place: " << ec.message();
boost::system::error_code rm;
boost::filesystem::remove(tmp_path, rm);
return false;
}
return true;
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "VendorCacheFile: write failed (" << path << "): " << e.what();
boost::system::error_code ec;
boost::filesystem::remove(tmp_path, ec);
return false;
}
}
+1
View File
@@ -12595,6 +12595,7 @@ OtherSlicingStatesConfigDef::OtherSlicingStatesConfigDef()
new_def("initial_no_support_extruder", coInt, "Initial no support extruder", "Zero-based index of the first extruder used for printing without support. Same as initial_no_support_tool.");
new_def("in_head_wrap_detect_zone", coBool, "In head wrap detect zone", "Indicates if the first layer overlaps with the head wrap zone.");
new_def("curr_bed_type", coString, "Current bed type", "Name of the currently selected bed plate type (e.g. 'Textured PEI Plate', 'Smooth High Temp Plate').");
}
PrintStatisticsConfigDef::PrintStatisticsConfigDef()
-17
View File
@@ -8,8 +8,6 @@
#include <functional>
#include <type_traits>
#include <system_error>
#include <initializer_list>
#include <string_view>
#include <regex>
#include <boost/system/error_code.hpp>
@@ -226,21 +224,6 @@ extern std::vector<std::string> split_string(const std::string &str, char delimi
// On Windows, the file explorer (or anti-virus or whatever else) often locks the file
// for a short while, so the file may not be movable. Retry while we see recoverable errors.
extern std::error_code rename_file(const std::string &from, const std::string &to);
// Write `chunks`, in order, to `path` through a temporary file beside it that is
// then renamed over the target, so a concurrent reader sees the old or the new
// file, never a partial one. The temporary is removed on failure and an existing
// target keeps its permissions. Text mode unless `binary`, so Windows writes CRLF
// as the streams this replaces did. A target that is not a regular file (a
// device or pipe) is written in place, since replacing it would change what it
// is, and so is an existing target beside which no temporary can be created or
// whose replace the filesystem refuses; a symlink is followed and the file it
// names is replaced. On Windows a reader holding the
// target open without sharing its deletion, which the C runtime does not, makes
// the replace fall back to the in-place write too, so an unlocked reader there
// can still see a partial file.
extern std::error_code write_file_atomically(const std::string &path, std::initializer_list<std::string_view> chunks, bool binary = false);
inline std::error_code write_file_atomically(const std::string &path, const std::string &content, bool binary = false)
{ return write_file_atomically(path, { std::string_view(content) }, binary); }
enum CopyFileResult {
SUCCESS = 0,
+2 -90
View File
@@ -9,8 +9,6 @@
#include <stdio.h>
#include <filesystem>
#include <sstream>
#include <cerrno>
#include <mutex>
#include <iomanip>
#include <algorithm>
#include <cmath>
@@ -705,99 +703,13 @@ namespace WindowsSupport
std::error_code rename_file(const std::string &from, const std::string &to)
{
#ifdef _WIN32
// Retries and moves an open destination aside itself.
return WindowsSupport::rename(from, to);
#else
// rename(2) replaces an existing target atomically; removing it first would
// leave a window in which the file does not exist at all.
if (boost::nowide::rename(from.c_str(), to.c_str()) == 0)
return {};
const int err = errno;
// Some mounts (sshfs, gvfs, MTP and a few SMB setups) refuse to replace an
// existing target in one step, each with the error it sees fit; every error
// is worth the remove-then-rename this always did, except the ones no retry
// can help: nothing at the source, a different device, or a directory where
// a file was expected and the reverse.
const bool worth_retrying = err != ENOENT && err != EXDEV && err != ENOTDIR && err != EISDIR;
if (worth_retrying && boost::nowide::remove(to.c_str()) == 0 && boost::nowide::rename(from.c_str(), to.c_str()) == 0)
return {};
return std::make_error_code(static_cast<std::errc>(err));
boost::nowide::remove(to.c_str());
return std::make_error_code(static_cast<std::errc>(boost::nowide::rename(from.c_str(), to.c_str())));
#endif
}
static std::error_code write_whole_file(const std::string &path, std::initializer_list<std::string_view> chunks, bool binary)
{
errno = 0;
FILE *file = boost::nowide::fopen(path.c_str(), binary ? "wb" : "w");
if (file == nullptr)
return std::make_error_code(errno != 0 ? static_cast<std::errc>(errno) : std::errc::io_error);
bool ok = true;
for (const std::string_view chunk : chunks)
ok = ok && std::fwrite(chunk.data(), 1, chunk.size(), file) == chunk.size();
ok = ok && std::fflush(file) == 0;
const int err = ok ? 0 : errno;
ok = std::fclose(file) == 0 && ok;
if (ok)
return {};
return std::make_error_code(err != 0 ? static_cast<std::errc>(err) : std::errc::io_error);
}
// The in-place fallback truncates the target, so two threads of this process
// on the same file must not both be in it. One mutex for all such writes: they
// are the rare case. Never freed, like the InstanceLock registry, so a save
// during static destruction still finds it.
static std::error_code write_in_place(const std::string &path, std::initializer_list<std::string_view> chunks, bool binary)
{
static auto *mutex = new std::mutex();
std::lock_guard<std::mutex> guard(*mutex);
return write_whole_file(path, chunks, binary);
}
std::error_code write_file_atomically(const std::string &path, std::initializer_list<std::string_view> chunks, bool binary)
{
boost::system::error_code bec;
const boost::filesystem::file_status target = boost::filesystem::symlink_status(path, bec);
const bool target_exists = ! bec && boost::filesystem::exists(target);
if (target_exists && boost::filesystem::is_symlink(target)) {
// A config or preset kept in a dotfiles repository: the link stays,
// the file it points to is replaced like any other.
const boost::filesystem::path resolved = boost::filesystem::canonical(path, bec);
if (! bec && boost::filesystem::is_regular_file(resolved, bec))
return write_file_atomically(resolved.string(), chunks, binary);
}
if (target_exists && ! boost::filesystem::is_regular_file(target))
return write_in_place(path, chunks, binary);
// Unique per process and per call, so two threads writing one target
// without a lock never share a temporary.
static std::atomic<unsigned> counter{0};
const std::string tmp_path = path + "." + std::to_string(get_current_pid()) + "." + std::to_string(counter++) + ".tmp";
if (const std::error_code ec = write_whole_file(tmp_path, chunks, binary)) {
boost::nowide::remove(tmp_path.c_str());
if (! target_exists)
return ec;
// A directory that lets this process write its files but not create
// one: losing the save is worse than a reader seeing a partial file.
BOOST_LOG_TRIVIAL(warning) << "Cannot create a temporary beside " << path << " (" << ec.message() << "); writing in place";
return write_in_place(path, chunks, binary);
}
#ifndef _WIN32
// Not on Windows, where a read-only bit on the temporary would stop the rename itself.
if (target_exists)
boost::filesystem::permissions(tmp_path, target.permissions(), bec);
#endif
if (const std::error_code ec = rename_file(tmp_path, path)) {
boost::nowide::remove(tmp_path.c_str());
// A reader on Windows holding the target open without FILE_SHARE_DELETE,
// or a mount that cannot replace a file at all. Losing the save is worse
// than a reader seeing a partial file, so write in place the way this
// used to work before the atomic path existed.
BOOST_LOG_TRIVIAL(warning) << "Cannot replace " << path << " (" << ec.message() << "); writing in place";
return write_in_place(path, chunks, binary);
}
return {};
}
#ifdef __linux__
// Copied from boost::filesystem.
// Called by copy_file_linux() in case linux sendfile() API is not supported.
+5 -2
View File
@@ -6546,8 +6546,11 @@ void GLCanvas3D::render_thumbnail_internal(ThumbnailData& thumbnail_data, const
// glsafe(::glClearColor(1.0f, 1.0f, 1.0f, 1.0f));
BOOST_LOG_TRIVIAL(info) << boost::format("render_thumbnail: finished");
// Puts the canvas viewport back in place of the thumbnail one set above.
wxGetApp().plater()->get_camera().apply_viewport();
// Puts the canvas viewport back in place of the thumbnail one set above. The CLI renders
// thumbnails with no application and no plater, so there is no canvas viewport to restore.
if (wxTheApp != nullptr)
if (Plater *plater = wxGetApp().plater(); plater != nullptr)
plater->get_camera().apply_viewport();
}
void GLCanvas3D::render_thumbnail_framebuffer(ThumbnailData& thumbnail_data, unsigned int w, unsigned int h, const ThumbnailsParams& thumbnail_params,
+6 -15
View File
@@ -85,7 +85,6 @@
#include "libslic3r/Model.hpp"
#include "libslic3r/I18N.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/InstanceLock.hpp"
#include "libslic3r/Thread.hpp"
#include "libslic3r/miniz_extension.hpp"
#include "libslic3r/Utils.hpp"
@@ -3571,7 +3570,7 @@ bool GUI_App::on_init_inner()
update_publish_status();
}
if (m_post_initialized && app_config->dirty() && app_config->save_due())
if (m_post_initialized && app_config->dirty())
app_config->save();
});
@@ -7665,11 +7664,8 @@ void GUI_App::start_sync_user_preset(bool with_progress_dlg)
// Delete the bundle folder and bundle
fs::path bundle_folder = fs::path(bundle.path.c_str()).parent_path();
{
boost::system::error_code ec;
InstanceLock instance_lock(user_presets_lock_path());
boost::filesystem::remove_all(bundle_folder, ec);
}
preset_bundle->bundles.WriteLock();
preset_bundle->bundles.m_bundles.erase(bundle.id);
@@ -8956,7 +8952,6 @@ void GUI_App::preset_deleted_from_cloud(std::string setting_id)
// Delete the .info file after cloud deletion is confirmed
if (!preset_file_path.empty() && fs::exists(fs::path(preset_file_path))) {
InstanceLock instance_lock(user_presets_lock_path());
boost::nowide::remove(preset_file_path.c_str());
BOOST_LOG_TRIVIAL(info) << "Deleted .info file after cloud confirmation: " << preset_file_path;
}
@@ -9019,21 +9014,17 @@ void GUI_App::scan_orphaned_info_files()
fs::path preset_file = info_file;
preset_file.replace_extension(".json");
// If .json doesn't exist, .info is orphaned. Read under the lock, so a
// remove_files() in another instance is seen whole or not at all; the
// delete queue's own mutex is taken after the lock is released.
std::string setting_id;
{
InstanceLock instance_lock(user_presets_lock_path());
if (!fs::exists(preset_file))
setting_id = extract_setting_id_from_info(info_file.string());
}
// If .json doesn't exist, .info is orphaned
if (!fs::exists(preset_file)) {
// Extract setting_id from .info file
std::string setting_id = extract_setting_id_from_info(info_file.string());
if (!setting_id.empty()) {
// Add to need_delete_presets
delete_preset_from_cloud(setting_id, info_file.string());
BOOST_LOG_TRIVIAL(info) << "Found orphaned .info file on startup: " << info_file.string();
}
}
}
if (ec)
BOOST_LOG_TRIVIAL(warning) << "scan_orphaned_info_files: failed to scan " << type_dir.string() << ": " << ec.message();
}
+13 -2
View File
@@ -1478,7 +1478,9 @@ bool GuideFrame::BuildProfileDataFromVendors()
return false;
// Written through a temp file and moved into place, as the preset caches
// are: half a cache must never be readable.
// are: half a cache must never be readable, and the PID suffix keeps two
// instances from interleaving on one temp file.
const std::string tmp_path = cache_file.string() + "." + std::to_string(get_current_pid()) + ".tmp";
try {
json out;
out["format"] = 1;
@@ -1487,9 +1489,18 @@ bool GuideFrame::BuildProfileDataFromVendors()
for (const char* key : { "model", "machine", "filament", "process" })
profile[key] = m_ProfileJson[key];
boost::filesystem::create_directories(cache_file.parent_path());
if (const std::error_code ec = write_file_atomically(cache_file.string(), out.dump(-1, ' ', false, json::error_handler_t::ignore), /*binary=*/true))
{
boost::nowide::ofstream ofs(tmp_path, std::ios::binary | std::ios::trunc);
ofs << out.dump(-1, ' ', false, json::error_handler_t::ignore);
ofs.close();
if (! ofs.good())
throw std::runtime_error("write failed");
}
if (const std::error_code ec = rename_file(tmp_path, cache_file.string()))
throw std::runtime_error(ec.message());
} catch (const std::exception& e) {
boost::system::error_code rm;
boost::filesystem::remove(tmp_path, rm);
BOOST_LOG_TRIVIAL(warning) << "GuideFrame: could not write the profile data cache: " << e.what();
}
return true;
+3 -5
View File
@@ -2,7 +2,6 @@
#include <algorithm>
#include <sstream>
#include <system_error>
#include <exception>
#include <boost/format.hpp>
#include <boost/log/trivial.hpp>
@@ -581,10 +580,9 @@ bool C3DPrinterOS::save_api_session(const std::string &session, const std::strin
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);
auto temp_path = m_api_session_file_path + ".tmp";
pt::write_json(temp_path, j);
boost::filesystem::rename(temp_path, m_api_session_file_path);
} catch (const std::exception &err) {
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << ": failed to write json to file. Path = "
<< m_api_session_file_path
+24 -5
View File
@@ -1475,8 +1475,15 @@ void OrcaCloudServiceAgent::save_sync_state()
if (sync_state_path.empty())
return;
if (const std::error_code ec = write_file_atomically(sync_state_path, std::to_string(sync_state.last_sync_timestamp)))
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: failed to save the sync state: " << ec.message();
try {
std::string tmp_path = sync_state_path + ".tmp";
std::ofstream ofs(tmp_path, std::ios::out | std::ios::trunc);
if (ofs.good()) {
ofs << std::to_string(sync_state.last_sync_timestamp);
ofs.close();
boost::filesystem::rename(tmp_path, sync_state_path);
}
} catch (...) {}
}
void OrcaCloudServiceAgent::clear_sync_state()
@@ -1565,10 +1572,22 @@ void OrcaCloudServiceAgent::persist_user_secret(const std::string& secret)
wxFileName::Mkdir(path.GetPath(), wxS_DIR_DEFAULT, wxPATH_MKDIR_FULL);
}
if (const std::error_code ec = write_file_atomically(secret_fallback_path, signed_payload, /*binary=*/true))
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: cannot write user secret file " << secret_fallback_path << ": " << ec.message();
else
const std::string tmp_path = secret_fallback_path + ".tmp";
std::ofstream ofs(tmp_path, std::ios::out | std::ios::trunc | std::ios::binary);
if (ofs.good()) {
ofs << signed_payload;
ofs.flush();
ofs.close();
if (wxRenameFile(wxString::FromUTF8(tmp_path.c_str()), wxString::FromUTF8(secret_fallback_path.c_str()), true)) {
stored = true;
} else {
wxRemoveFile(wxString::FromUTF8(tmp_path.c_str()));
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: failed to atomically replace user secret file";
}
} else {
BOOST_LOG_TRIVIAL(warning) << "OrcaCloudServiceAgent: cannot open user secret file for write - " << secret_fallback_path;
}
} else {
// Use wxSecretStore only
wxSecretStore store = wxSecretStore::GetDefault();
+15 -4
View File
@@ -249,10 +249,21 @@ bool PluginConfig::save()
return false;
}
// Written beside the target and moved into place, so a crash mid-write cannot truncate an
// existing config.
if (const std::error_code ec = write_file_atomically(path, root.dump(1, '\t') + "\n")) {
BOOST_LOG_TRIVIAL(error) << "PluginConfig: failed to write " << path << ": " << ec.message() << "; keeping the existing config";
// Write to a PID-suffixed file and rename it into place, so a crash mid-write cannot truncate an
// existing config. Same approach as AppConfig::save().
const std::string path_pid = (boost::format("%1%.%2%") % path % get_current_pid()).str();
boost::nowide::ofstream file;
file.open(path_pid, std::ios::out | std::ios::trunc);
file << root.dump(1, '\t') << std::endl;
file.close();
if (file.fail()) {
BOOST_LOG_TRIVIAL(error) << "PluginConfig: failed to write " << path_pid << "; keeping the existing config";
return false;
}
if (const std::error_code rename_ec = rename_file(path_pid, path)) {
BOOST_LOG_TRIVIAL(error) << "PluginConfig: failed to move " << path_pid << " onto " << path << ": " << rename_ec.message();
return false;
}
+8
View File
@@ -15,3 +15,11 @@ set_tests_properties(cli_strict_mode PROPERTIES
LABELS "CLI;RequiresApp"
SKIP_RETURN_CODE 77
TIMEOUT 900)
add_test(NAME cli_project_missing_keys
COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/test_cli_project_missing_keys.sh $<TARGET_FILE:OrcaSlicer> ${ORCA_CLI_TEST_PYTHON}
${CMAKE_SOURCE_DIR}/resources/profiles/BBL)
set_tests_properties(cli_project_missing_keys PROPERTIES
LABELS "CLI;RequiresApp"
SKIP_RETURN_CODE 77
TIMEOUT 900)
@@ -0,0 +1,96 @@
#!/usr/bin/env bash
# End-to-end check that the CLI loads a project's printer and process settings as the GUI does.
#
# The GUI takes every key a project does not list as changed from the project's current system preset:
# keys saved before an option existed, and keys holding an older system value. Keys the project lists
# in different_settings_to_system keep the project's value. A project is exported from the shipped
# Bambu Lab P1S presets; one printer key and one process key are removed, one printer key and one
# process key are changed without being listed, one key is changed and listed, and it is sliced again.
#
# usage: test_cli_project_missing_keys.sh <orca-slicer binary> <python3> <resources/profiles/BBL>
set -u
BIN="${1:-}"
PY="${2:-python3}"
PROFILES="${3:-}"
# 77 is the test's SKIP_RETURN_CODE.
[ -x "$BIN" ] || { echo "SKIP: orca-slicer binary not found: $BIN"; exit 77; }
[ -d "$PROFILES" ] || { echo "FAIL: profiles directory not found: $PROFILES"; exit 1; }
WORK="$(mktemp -d "${TMPDIR:-/tmp}/orca-cli-missing-keys.XXXXXX")"
trap 'rm -rf "$WORK"' EXIT
"$PY" - "$WORK/cube.stl" <<'EOF'
import sys
v = [(x, y, z) for z in (0, 10) for y in (0, 10) for x in (0, 10)]
with open(sys.argv[1], "w") as f:
f.write("solid cube\n")
for a, b, c, d in ((0, 2, 3, 1), (4, 5, 7, 6), (0, 1, 5, 4), (2, 6, 7, 3), (0, 4, 6, 2), (1, 3, 7, 5)):
for tri in ((v[a], v[b], v[c]), (v[a], v[c], v[d])):
f.write("facet normal 0 0 0\nouter loop\n")
for p in tri:
f.write("vertex %g %g %g\n" % p)
f.write("endloop\nendfacet\n")
f.write("endsolid cube\n")
EOF
# slice <tag> <input> [option...]: slice into $WORK/<tag>/out.3mf with a fresh data directory.
slice() {
local out="$WORK/$1" input="$2"; shift 2
mkdir -p "$out"
timeout 300 "$BIN" --datadir "$out/datadir" "$@" --slice 0 --outputdir "$out" --export-3mf out.3mf "$input" \
> "$out/log" 2>&1 || { echo "FAIL: $1: orca-slicer exited $?"; tail -n 40 "$out/log"; exit 1; }
}
slice base "$WORK/cube.stl" \
--load-settings "$PROFILES/machine/Bambu Lab P1S 0.4 nozzle.json;$PROFILES/process/0.20mm Standard @BBL X1C.json" \
--load-filaments "$PROFILES/filament/Bambu PLA Basic @BBL P1S 0.4 nozzle.json"
# The removed keys, with their option defaults from PrintConfig.cpp; stale keys changed without being
# listed as different, which must come back with the system value; and a listed key the project keeps.
"$PY" - "$WORK/base/out.3mf" "$WORK/old.3mf" <<'EOF' || exit $?
import json, sys, zipfile
src, dst = sys.argv[1], sys.argv[2]
missing = {"extruder_clearance_dist_to_rod": "40", "sparse_infill_density": "20%"}
with zipfile.ZipFile(src) as zin, zipfile.ZipFile(dst, "w", zipfile.ZIP_DEFLATED) as zout:
for item in zin.infolist():
data = zin.read(item.filename)
if item.filename == "Metadata/project_settings.config":
config = json.loads(data)
for key, default in missing.items():
if config[key] == default:
print("SKIP: %s is %s in the system preset, the option default, so the test cannot tell them apart" % (key, default))
sys.exit(77)
expected = {key: config.pop(key) for key in missing}
for key in ("top_shell_layers", "extruder_clearance_height_to_rod"):
expected[key] = config[key]
config[key] = str(int(float(config[key])) + 1)
expected["wall_loops"] = str(int(config["wall_loops"]) + 1)
config["wall_loops"] = expected["wall_loops"]
different = config["different_settings_to_system"]
different[0] = ";".join([k for k in different[0].split(";") if k] + ["wall_loops"])
data = json.dumps(config, indent=4)
zout.writestr(item, data)
with open(dst + ".expected.json", "w") as f:
json.dump(expected, f)
EOF
slice project "$WORK/old.3mf"
"$PY" - "$WORK/project/out.3mf" "$WORK/old.3mf.expected.json" <<'EOF'
import json, sys, zipfile
with zipfile.ZipFile(sys.argv[1]) as z:
config = json.loads(z.read("Metadata/project_settings.config"))
with open(sys.argv[2]) as f:
expected = json.load(f)
errors = ["%s is %r, want %r" % (key, config.get(key), want) for key, want in expected.items() if config.get(key) != want]
for e in errors:
print("FAIL: " + e)
sys.exit(1 if errors else 0)
EOF
status=$?
[ "$status" -eq 0 ] || { tail -n 40 "$WORK/project/log"; exit 1; }
echo "PASS"
-1
View File
@@ -56,7 +56,6 @@ add_executable(${_TEST_NAME}_tests
test_ordering_strategies.cpp
# test_png_io.cpp
test_indexed_triangle_set.cpp
test_instance_lock.cpp
../libnest2d/printer_parts.cpp
)
-228
View File
@@ -1,228 +0,0 @@
#include <catch2/catch_all.hpp>
#include <atomic>
#include <chrono>
#include <thread>
#include <boost/filesystem.hpp>
#include "libslic3r/InstanceLock.hpp"
#include "test_utils.hpp"
#ifdef _WIN32
#include <boost/interprocess/sync/file_lock.hpp>
#include <boost/nowide/convert.hpp>
#include <boost/nowide/fstream.hpp>
#else
#include <fcntl.h>
#include <sys/file.h>
#include <unistd.h>
#endif
using namespace Slic3r;
using namespace std::chrono_literals;
// Sets a process-wide knob for one test and restores it however the test ends.
template<typename T> struct ScopedStaticValue
{
T &ref;
T saved;
ScopedStaticValue(T &ref, T value) : ref(ref), saved(ref) { ref = value; }
~ScopedStaticValue() { ref = saved; }
};
TEST_CASE("InstanceLock creates its lock file and holds it for the guard's scope", "[InstanceLock]")
{
ScopedTemporaryFile lock_file(".lock");
const std::string path = lock_file.string();
{
InstanceLock lock(path);
REQUIRE(lock.locked());
REQUIRE(boost::filesystem::exists(path));
}
// Released: a fresh guard gets the lock at once instead of waiting out a timeout.
const auto started = std::chrono::steady_clock::now();
InstanceLock again(path, 5000ms);
REQUIRE(again.locked());
// Well inside the timeout it would otherwise have waited out; loose enough for a loaded runner.
REQUIRE(std::chrono::steady_clock::now() - started < 4000ms);
}
TEST_CASE("InstanceLock nests within one thread", "[InstanceLock]")
{
ScopedTemporaryFile lock_file(".lock");
const std::string path = lock_file.string();
InstanceLock outer(path);
{
InstanceLock inner(path, 100ms);
REQUIRE(inner.locked());
}
// The inner guard leaving does not release the outer one.
REQUIRE(outer.locked());
}
TEST_CASE("InstanceLock is a no-op for an empty path and survives an unwritable one", "[InstanceLock]")
{
ScopedTemporaryDir dir;
InstanceLock none("");
REQUIRE_FALSE(none.locked());
// The directory does not exist, so the lock file cannot be created; the
// guard still constructs and the write it guards can go ahead.
InstanceLock unwritable((dir.path() / "missing" / "shared.lock").string(), 100ms);
REQUIRE_FALSE(unwritable.locked());
}
TEST_CASE("InstanceLock retries a lock file it could not open once the cool-down passes", "[InstanceLock]")
{
ScopedTemporaryDir dir;
const std::string path = (dir.path() / "later" / "shared.lock").string();
ScopedStaticValue cooldown(InstanceLock::cooldown, 300ms);
bool before_dir, during_cooldown, after_cooldown;
const auto started = std::chrono::steady_clock::now();
{
InstanceLock lock(path, 100ms);
before_dir = lock.locked();
}
boost::filesystem::create_directories(dir.path() / "later");
{
InstanceLock lock(path, 100ms);
during_cooldown = lock.locked();
}
const bool second_guard_inside_cooldown = std::chrono::steady_clock::now() - started < InstanceLock::cooldown;
std::this_thread::sleep_for(400ms);
{
InstanceLock lock(path, 100ms);
after_cooldown = lock.locked();
}
REQUIRE_FALSE(before_dir);
// A loaded runner may take longer than the cool-down to get here; then the
// second guard legitimately retried, so only assert when the timing held.
if (second_guard_inside_cooldown)
REQUIRE_FALSE(during_cooldown);
REQUIRE(after_cooldown);
}
TEST_CASE("InstanceLock reopens a lock file that was replaced on disk", "[InstanceLock]")
{
ScopedTemporaryFile lock_file(".lock");
const std::string path = lock_file.string();
{
InstanceLock lock(path);
REQUIRE(lock.locked());
}
boost::filesystem::remove(path);
InstanceLock lock(path);
REQUIRE(lock.locked());
// Each outermost guard opens the file afresh, so the deleted path is back.
REQUIRE(boost::filesystem::exists(path));
}
TEST_CASE("InstanceLock serialises the threads of one process", "[InstanceLock]")
{
ScopedTemporaryFile lock_file(".lock");
const std::string path = lock_file.string();
std::atomic<bool> holder_ready{false};
std::atomic<bool> holder_released{false};
std::thread holder([&] {
InstanceLock lock(path);
holder_ready = true;
std::this_thread::sleep_for(150ms);
holder_released = true;
});
while (! holder_ready)
std::this_thread::yield();
bool released_before_acquire = false;
{
InstanceLock lock(path);
released_before_acquire = holder_released;
}
holder.join();
REQUIRE(released_before_acquire);
}
// Holds the OS lock on a file through a handle of its own, as another instance would. The lock
// belongs to the handle on Windows and to the open file description elsewhere, so the guard's
// handle is refused while this one holds it.
class OtherHolder
{
public:
explicit OtherHolder(const std::string &path)
{
#ifdef _WIN32
boost::nowide::ofstream(path, std::ios::app).close();
m_lock = boost::interprocess::file_lock(boost::nowide::widen(path).c_str());
m_held = m_lock.try_lock();
#else
m_fd = ::open(path.c_str(), O_RDWR | O_CREAT, 0644);
m_held = m_fd >= 0 && ::flock(m_fd, LOCK_EX | LOCK_NB) == 0;
#endif
}
~OtherHolder() { release(); }
OtherHolder(const OtherHolder &) = delete;
OtherHolder &operator=(const OtherHolder &) = delete;
bool held() const { return m_held; }
void release()
{
#ifdef _WIN32
if (m_held)
m_lock.unlock();
m_lock = boost::interprocess::file_lock();
#else
if (m_fd >= 0)
::close(m_fd);
m_fd = -1;
#endif
m_held = false;
}
private:
#ifdef _WIN32
boost::interprocess::file_lock m_lock;
#else
int m_fd{-1};
#endif
bool m_held{false};
};
TEST_CASE("InstanceLock yields to a lock held through another handle and reports it", "[InstanceLock]")
{
ScopedTemporaryFile lock_file(".lock");
const std::string path = lock_file.string();
ScopedStaticValue cooldown(InstanceLock::cooldown, 300ms);
OtherHolder other(path);
REQUIRE(other.held());
bool locked_while_other_holds;
{
InstanceLock lock(path, 100ms);
locked_while_other_holds = lock.locked();
}
// The timed-out wait starts a cool-down: the next guard does not touch the file.
const auto started = std::chrono::steady_clock::now();
bool locked_during_cooldown;
{
InstanceLock lock(path, 5000ms);
locked_during_cooldown = lock.locked();
}
const auto cooldown_wait = std::chrono::steady_clock::now() - started;
other.release();
REQUIRE_FALSE(locked_while_other_holds);
REQUIRE_FALSE(locked_during_cooldown);
REQUIRE(cooldown_wait < 4000ms);
// Once the cool-down passes, the lock the other holder released is taken again.
std::this_thread::sleep_for(400ms);
InstanceLock lock(path);
REQUIRE(lock.locked());
}
+243 -203
View File
@@ -5,7 +5,6 @@
#include <fstream>
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/ParallelResolve.hpp"
#include "libslic3r/AppConfig.hpp"
#include "libslic3r/Model.hpp"
#include "libslic3r/TriangleMesh.hpp"
@@ -18,10 +17,6 @@
#include <iostream>
#include <initializer_list>
#ifndef _WIN32
#include <unistd.h> // geteuid
#endif
using namespace Slic3r;
namespace {
@@ -5576,6 +5571,35 @@ struct ScopedDataDir
~ScopedDataDir() { set_data_dir(previous); }
};
// resources_dir() is process-wide too; system preset lookups scan its profiles directory.
struct ScopedResourcesDir
{
std::string previous = resources_dir();
explicit ScopedResourcesDir(const fs::path &dir) { set_resources_dir(dir.string()); }
~ScopedResourcesDir() { set_resources_dir(previous); }
};
// An "Acme" vendor under root whose "Acme Printer" inherits extruder_clearance_dist_to_rod from an
// abstract base, with the printer in a nested sub_path so the name cannot be derived from the file.
void write_acme_printer_vendor(const fs::path &root, double dist_to_rod)
{
const fs::path machine_dir = root / "Acme" / "machine";
fs::create_directories(machine_dir / "nested");
std::ofstream((root / "Acme.json").string())
<< R"({"version":"1.0.0","name":"Acme",)"
<< R"("machine_model_list":[{"name":"Acme One","sub_path":"machine/model.json"}],"machine_list":[)"
<< R"({"name":"fdm_acme_common","sub_path":"machine/base.json"},)"
<< R"({"name":"Acme Printer","sub_path":"machine/nested/printer.json"}]})";
std::ofstream((machine_dir / "model.json").string())
<< R"({"type":"machine_model","name":"Acme One","nozzle_diameter":"0.4"})";
std::ofstream((machine_dir / "base.json").string())
<< R"({"type":"machine","name":"fdm_acme_common","from":"system","instantiation":"false",)"
<< R"("extruder_clearance_dist_to_rod":")" << dist_to_rod << R"("})";
std::ofstream((machine_dir / "nested" / "printer.json").string())
<< R"({"type":"machine","name":"Acme Printer","from":"system","instantiation":"true","inherits":"fdm_acme_common",)"
<< R"("printer_model":"Acme One","printer_variant":"0.4"})";
}
std::string read_file(const fs::path &file)
{
std::ifstream in(file.string(), std::ios::binary);
@@ -5690,213 +5714,229 @@ TEST_CASE("A project saved with pressure advance per filament applies it to ever
check_double_vector(petg.opt<ConfigOptionFloatsNullable>("filament_flow_ratio")->values, { 0.97 });
}
TEST_CASE("A user preset saved over by another instance while its directory loads is read again under the lock", "[Preset][Bundle][InstanceLock]")
TEST_CASE("A system preset resolves by name from the bundled profiles", "[Preset][Bundle]")
{
ScopedTemporaryDir temp_dir;
ScopedDataDir data_dir_scope(temp_dir.path());
RenameTestCollection coll;
ScopedDataDir data(temp_dir.path() / "data");
ScopedResourcesDir resources(temp_dir.path() / "resources");
write_acme_printer_vendor(temp_dir.path() / "resources" / "profiles", 33.);
Preset &parent = add_inmemory_preset(coll, "Parent Process");
parent.config.option<ConfigOptionFloat>("layer_height", true)->value = 0.24;
parent.is_system = true;
// Fewer than one batch, so every file is read before the first one is committed.
constexpr int children = 8;
static_assert(children <= int(resolve_batch_size));
const fs::path dir = temp_dir.path() / PRESET_PRINT_NAME;
for (int i = 0; i < children; ++ i)
write_minimal_child(dir / ("Child " + std::to_string(i) + ".json"), "Child " + std::to_string(i), "Parent Process");
// The first commit stands in for another instance saving every other preset after
// they were read and before they are committed.
std::string first;
auto save_the_others = [&](Preset &preset) {
if (! first.empty())
return;
first = preset.name;
for (int i = 0; i < children; ++ i) {
const std::string name = "Child " + std::to_string(i);
if (name != first)
std::ofstream((dir / (name + ".json")).string())
<< R"({"type":"process","name":")" << name
<< R"(","from":"User","version":"1.0.0","inherits":"Parent Process","layer_height":"0.3"})";
}
};
PresetsConfigSubstitutions substitutions;
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Disable, save_the_others);
REQUIRE_FALSE(first.empty());
CHECK(coll.error_count() == 0);
for (int i = 0; i < children; ++ i) {
const std::string name = "Child " + std::to_string(i);
const Preset *child = coll.find_preset(name);
REQUIRE(child != nullptr);
CHECK_THAT(child->config.opt_float("layer_height"), Catch::Matchers::WithinAbs(name == first ? 0.24 : 0.3, 1e-9));
}
}
TEST_CASE("A user preset removed by another instance while its directory loads is not installed", "[Preset][Bundle][InstanceLock]")
{
ScopedTemporaryDir temp_dir;
ScopedDataDir data_dir_scope(temp_dir.path());
RenameTestCollection coll;
Preset &parent = add_inmemory_preset(coll, "Parent Process");
parent.is_system = true;
// Fewer than one batch, so every file is read before the first one is committed.
constexpr int children = 8;
static_assert(children <= int(resolve_batch_size));
const fs::path dir = temp_dir.path() / PRESET_PRINT_NAME;
for (int i = 0; i < children; ++ i)
write_minimal_child(dir / ("Child " + std::to_string(i) + ".json"), "Child " + std::to_string(i), "Parent Process");
std::string first;
auto remove_the_others = [&](Preset &preset) {
if (! first.empty())
return;
first = preset.name;
for (int i = 0; i < children; ++ i)
if (const std::string name = "Child " + std::to_string(i); name != first)
fs::remove(dir / (name + ".json"));
};
PresetsConfigSubstitutions substitutions;
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Disable, remove_the_others);
REQUIRE_FALSE(first.empty());
CHECK(coll.error_count() == 0);
CHECK(coll.size() == 3); // the default preset, the parent and the first child
CHECK(coll.find_preset(first) != nullptr);
}
TEST_CASE("Without the instance lock a user preset loads but its file is not written back", "[Preset][Bundle][InstanceLock]")
{
ScopedTemporaryDir temp_dir;
ScopedDataDir data_dir_scope(temp_dir.path());
// A directory where the lock file belongs, so the lock cannot be taken.
fs::create_directories(fs::path(user_presets_lock_path()));
PresetBundle bundle;
const fs::path file = temp_dir.path() / PRESET_FILAMENT_NAME / "My PLA @Test Printer.json";
write_preset_with_inherits(bundle.filaments.default_preset().config, file, "My PLA @Test Printer", std::string());
const std::string before = read_file(file);
PresetsConfigSubstitutions substitutions;
bundle.filaments.load_presets(temp_dir.path().string(), PRESET_FILAMENT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::EnableSilent);
const Preset *preset = bundle.filaments.find_preset("My PLA @Test Printer");
REQUIRE(preset != nullptr);
CHECK(preset->config.option<ConfigOptionStrings>("compatible_printers")->values == std::vector<std::string>{ "Test Printer" });
CHECK(read_file(file) == before);
DynamicPrintConfig config;
std::string error;
REQUIRE(bundle.resolve_system_preset(config, Preset::TYPE_PRINTER, "Acme Printer",
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
CHECK(error.empty());
CHECK_THAT(config.opt_float("extruder_clearance_dist_to_rod"), Catch::Matchers::WithinAbs(33., 1e-6));
}
#ifndef _WIN32
// File permissions stop reading only on POSIX.
TEST_CASE("A user preset that cannot be read under the instance lock is counted and removed", "[Preset][Bundle][InstanceLock]")
{
if (::geteuid() == 0)
SKIP("file permissions do not stop root");
ScopedTemporaryDir temp_dir;
ScopedDataDir data_dir_scope(temp_dir.path());
RenameTestCollection coll;
Preset &parent = add_inmemory_preset(coll, "Parent Process");
parent.is_system = true;
const fs::path file = temp_dir.path() / PRESET_PRINT_NAME / "Unreadable.json";
write_minimal_child(file, "Unreadable", "Parent Process");
fs::permissions(file, fs::no_perms);
PresetsConfigSubstitutions substitutions;
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Disable);
CHECK(coll.error_count() == 1);
CHECK(coll.find_preset("Unreadable") == nullptr);
CHECK_FALSE(fs::exists(file));
}
#endif
#ifndef _WIN32
// Creating a symlink needs no privilege only on POSIX.
TEST_CASE("A user preset that is a symlink to itself is counted and removed while the rest still load", "[Preset][Bundle][InstanceLock]")
TEST_CASE("A system preset resolves from the data directory copy of its vendor", "[Preset][Bundle]")
{
ScopedTemporaryDir temp_dir;
ScopedDataDir data_dir_scope(temp_dir.path());
RenameTestCollection coll;
ScopedDataDir data(temp_dir.path() / "data");
ScopedResourcesDir resources(temp_dir.path() / "resources");
write_acme_printer_vendor(temp_dir.path() / "resources" / "profiles", 33.);
write_acme_printer_vendor(temp_dir.path() / "data" / PRESET_SYSTEM_DIR, 35.);
Preset &parent = add_inmemory_preset(coll, "Parent Process");
parent.is_system = true;
const fs::path dir = temp_dir.path() / PRESET_PRINT_NAME;
write_minimal_child(dir / "Good A.json", "Good A", "Parent Process");
write_minimal_child(dir / "Good B.json", "Good B", "Parent Process");
const fs::path loop = dir / "Loop.json";
fs::create_symlink(loop.filename(), loop);
PresetsConfigSubstitutions substitutions;
REQUIRE_NOTHROW(coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Disable));
CHECK(coll.error_count() == 1);
CHECK(coll.find_preset("Good A") != nullptr);
CHECK(coll.find_preset("Good B") != nullptr);
CHECK(coll.find_preset("Loop") == nullptr);
boost::system::error_code ec;
CHECK(fs::symlink_status(loop, ec).type() == fs::file_not_found);
}
#endif
#ifndef _WIN32
// Creating a symlink needs no privilege only on POSIX.
TEST_CASE("A user preset symlinked to a missing target is counted but the link is kept", "[Preset][Bundle][InstanceLock]")
{
ScopedTemporaryDir temp_dir;
ScopedDataDir data_dir_scope(temp_dir.path());
RenameTestCollection coll;
const fs::path dir = temp_dir.path() / PRESET_PRINT_NAME;
const fs::path link = dir / "Linked.json";
fs::create_directories(dir);
fs::create_symlink(temp_dir.path() / "unmounted" / "Linked.json", link);
PresetsConfigSubstitutions substitutions;
coll.load_presets(temp_dir.path().string(), PRESET_PRINT_NAME, substitutions,
ForwardCompatibilitySubstitutionRule::Disable);
CHECK(coll.error_count() == 1);
CHECK(coll.find_preset("Linked") == nullptr);
boost::system::error_code ec;
CHECK(fs::symlink_status(link, ec).type() == fs::symlink_file);
}
#endif
#ifndef _WIN32
// The read-only bit on a directory stops file creation only on POSIX.
TEST_CASE("A user filament whose derived compatible printer cannot be written back still loads with the failure counted", "[Preset][Bundle]")
{
if (::geteuid() == 0)
SKIP("a read-only directory does not stop root");
ScopedTemporaryDir temp_dir;
PresetBundle bundle;
const fs::path dir = temp_dir.path() / PRESET_FILAMENT_NAME;
const fs::path file = dir / "My PLA @Test Printer.json";
write_preset_with_inherits(bundle.filaments.default_preset().config, file, "My PLA @Test Printer", std::string());
const std::string before = read_file(file);
fs::permissions(file, fs::owner_read);
fs::permissions(dir, fs::owner_read | fs::owner_exe);
PresetsConfigSubstitutions substitutions;
bundle.filaments.load_presets(temp_dir.path().string(), PRESET_FILAMENT_NAME, substitutions,
DynamicPrintConfig config;
std::string error;
REQUIRE(bundle.resolve_system_preset(config, Preset::TYPE_PRINTER, "Acme Printer",
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
CHECK_THAT(config.opt_float("extruder_clearance_dist_to_rod"), Catch::Matchers::WithinAbs(35., 1e-6));
}
TEST_CASE("A system preset resolves from a vendor shipped as its preset cache alone", "[Preset][Bundle]")
{
ScopedTemporaryDir temp_dir;
ScopedDataDir data(temp_dir.path() / "data");
ScopedResourcesDir resources(temp_dir.path() / "resources");
const fs::path profiles = temp_dir.path() / "resources" / "profiles";
write_acme_printer_vendor(profiles, 33.);
PresetBundle writer;
writer.set_generate_vendor_caches(true);
writer.load_vendor_configs_from_json(profiles.string(), "Acme", PresetBundle::LoadSystem,
ForwardCompatibilitySubstitutionRule::EnableSilent);
// Restored before any assertion, so a failure never leaves an unremovable directory behind.
fs::permissions(dir, fs::owner_all);
fs::permissions(file, fs::owner_read | fs::owner_write);
const Preset *preset = bundle.filaments.find_preset("My PLA @Test Printer");
REQUIRE(preset != nullptr);
CHECK(preset->config.option<ConfigOptionStrings>("compatible_printers")->values == std::vector<std::string>{ "Test Printer" });
CHECK(bundle.filaments.error_count() == 1);
CHECK(read_file(file) == before);
REQUIRE(fs::exists(profiles / "Acme.opc"));
// Release builds ship the cache and drop the profile JSONs, manifest included.
fs::remove(profiles / "Acme.json");
fs::remove_all(profiles / "Acme");
PresetBundle bundle;
DynamicPrintConfig config;
std::string error;
REQUIRE(bundle.resolve_system_preset(config, Preset::TYPE_PRINTER, "Acme Printer",
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
CHECK_THAT(config.opt_float("extruder_clearance_dist_to_rod"), Catch::Matchers::WithinAbs(33., 1e-6));
}
TEST_CASE("A system preset no vendor lists is not resolved", "[Preset][Bundle]")
{
ScopedTemporaryDir temp_dir;
ScopedDataDir data(temp_dir.path() / "data");
ScopedResourcesDir resources(temp_dir.path() / "resources");
write_acme_printer_vendor(temp_dir.path() / "resources" / "profiles", 33.);
PresetBundle bundle;
DynamicPrintConfig config;
std::string error;
CHECK_FALSE(bundle.resolve_system_preset(config, Preset::TYPE_PRINTER, "Unknown Printer",
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
CHECK_FALSE(error.empty());
}
namespace {
// A default preset config for type, built the way PresetBundle builds its default presets.
DynamicPrintConfig external_default_config(Preset::Type type)
{
DynamicPrintConfig config;
config.apply_only(static_cast<const PrintRegionConfig &>(FullPrintConfig::defaults()),
type == Preset::TYPE_PRINTER ? Preset::printer_options() : Preset::print_options());
Preset::inherits(config);
return config;
}
// A find_base callback that counts its calls and returns base.
auto base_finder(DynamicPrintConfig *base, int &calls)
{
return [base, &calls](const std::string &) {
++calls;
return base;
};
}
} // namespace
TEST_CASE("A key missing from a project takes the default preset value when there is no base", "[Preset][ExternalPreset]")
{
DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINT);
defaults.set("wall_loops", 7, true);
const DynamicPrintConfig project;
int calls = 0;
const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINT, defaults, project, {"sparse_infill_density"},
base_finder(nullptr, calls));
CHECK(calls == 1);
CHECK(config.opt_int("wall_loops") == 7);
}
TEST_CASE("A key missing from a project takes its base preset value", "[Preset][ExternalPreset]")
{
DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINT);
defaults.set("wall_loops", 7, true);
DynamicPrintConfig base = defaults;
base.set("wall_loops", 5, true);
const DynamicPrintConfig project;
int calls = 0;
const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINT, defaults, project, {"sparse_infill_density"},
base_finder(&base, calls));
CHECK(config.opt_int("wall_loops") == 5);
}
TEST_CASE("A project key listed as different keeps the project value", "[Preset][ExternalPreset]")
{
const DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINT);
DynamicPrintConfig base = defaults;
base.set("wall_loops", 5, true);
DynamicPrintConfig project;
project.set("wall_loops", 4, true);
int calls = 0;
const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINT, defaults, project, {"wall_loops"},
base_finder(&base, calls));
CHECK(config.opt_int("wall_loops") == 4);
}
TEST_CASE("A project key not listed as different is refreshed to the base value", "[Preset][ExternalPreset]")
{
const DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINT);
DynamicPrintConfig base = defaults;
base.set("wall_loops", 5, true);
DynamicPrintConfig project;
project.set("wall_loops", 4, true);
project.set("inherits", "Base Process", true);
std::string inherits;
const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINT, defaults, project, {"sparse_infill_density"},
[&base, &inherits](const std::string &name) {
inherits = name;
return &base;
});
CHECK(inherits == "Base Process");
CHECK(config.opt_int("wall_loops") == 5);
}
TEST_CASE("An empty different settings list keeps the project values without a base", "[Preset][ExternalPreset]")
{
DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINT);
defaults.set("top_shell_layers", 7, true);
DynamicPrintConfig base = defaults;
base.set("wall_loops", 5, true);
base.set("top_shell_layers", 9, true);
DynamicPrintConfig project;
project.set("wall_loops", 4, true);
int calls = 0;
const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINT, defaults, project, {}, base_finder(&base, calls));
CHECK(calls == 0);
CHECK(config.opt_int("wall_loops") == 4);
CHECK(config.opt_int("top_shell_layers") == 7);
}
TEST_CASE("Print-host keys are never taken from a project", "[Preset][ExternalPreset]")
{
DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINTER);
defaults.set("print_host", "", true);
defaults.set("printhost_apikey", "", true);
DynamicPrintConfig project;
project.set("print_host", "http://project-host", true);
project.set("printhost_apikey", "project-key", true);
project.set("printer_notes", "project notes", true);
t_config_option_keys keys;
int calls = 0;
const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINTER, defaults, project, {"printer_notes"},
base_finder(nullptr, calls), &keys);
CHECK(config.opt_string("print_host").empty());
CHECK(config.opt_string("printhost_apikey").empty());
CHECK(config.opt_string("printer_notes") == "project notes");
CHECK_FALSE(contains_key(keys, "print_host"));
CHECK_FALSE(contains_key(keys, "printhost_apikey"));
CHECK(contains_key(keys, "printer_notes"));
}
TEST_CASE("A per-variant project value maps onto its base preset's variant layout", "[Preset][ExternalPreset]")
{
const DynamicPrintConfig defaults = external_default_config(Preset::TYPE_PRINT);
DynamicPrintConfig base = defaults;
base.set_key_value("print_extruder_id", new ConfigOptionInts({1, 1}));
base.set_key_value("print_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard", "Direct Drive High Flow"}));
base.set_key_value("outer_wall_speed", new ConfigOptionFloats({200., 300.}));
base.set_key_value("inner_wall_speed", new ConfigOptionFloats({250., 350.}));
// A project saved before its printer had a High Flow variant.
DynamicPrintConfig project;
project.set_key_value("print_extruder_id", new ConfigOptionInts({1}));
project.set_key_value("print_extruder_variant", new ConfigOptionStrings({"Direct Drive Standard"}));
project.set_key_value("outer_wall_speed", new ConfigOptionFloats({100.}));
project.set_key_value("inner_wall_speed", new ConfigOptionFloats({150.}));
int calls = 0;
const DynamicPrintConfig config = Preset::load_external_config(Preset::TYPE_PRINT, defaults, project, {"outer_wall_speed"},
base_finder(&base, calls));
CHECK(config.option<ConfigOptionStrings>("print_extruder_variant")->values ==
std::vector<std::string>{"Direct Drive Standard", "Direct Drive High Flow"});
// The listed key keeps the project's Standard value and takes High Flow from the base.
check_double_vector(config.option<ConfigOptionFloats>("outer_wall_speed")->values, {100., 300.});
check_double_vector(config.option<ConfigOptionFloats>("inner_wall_speed")->values, {250., 350.});
}
TEST_CASE("A project's different settings always keep the preset bookkeeping keys", "[Preset][ExternalPreset]")
{
const std::set<std::string> keys = PresetBundle::project_different_keys(escape_strings_cstyle({"wall_loops", "top_shell_layers"}));
for (const char *key : {"wall_loops", "top_shell_layers", "inherits", "print_settings_id", "filament_settings_id", "printer_settings_id"})
CHECK(keys.count(key) == 1);
CHECK(PresetBundle::project_different_keys(std::string()).count("inherits") == 1);
}
#endif
-109
View File
@@ -10,8 +10,6 @@
#include <cctype>
#include <fstream>
#include <string>
#include <thread>
#include <system_error>
#ifndef _WIN32
#include <unistd.h> // getuid
@@ -64,113 +62,6 @@ TEST_CASE("per-user temp root is unchanged on Windows, isolated elsewhere", "[ut
#endif
}
TEST_CASE("write_file_atomically replaces the target and leaves no temporary file", "[utils]") {
ScopedTemporaryDir dir;
const boost::filesystem::path target = dir.path() / "preset.json";
REQUIRE_FALSE(write_file_atomically(target.string(), "first"));
REQUIRE_FALSE(write_file_atomically(target.string(), "second"));
std::string content;
load_string_file(target, content);
REQUIRE(content == "second");
size_t entries = 0;
for (auto &entry : boost::filesystem::directory_iterator(dir.path())) {
(void) entry;
++entries;
}
REQUIRE(entries == 1);
}
TEST_CASE("write_file_atomically reports a missing directory and writes nothing", "[utils]") {
ScopedTemporaryDir dir;
const boost::filesystem::path target = dir.path() / "missing" / "preset.json";
const std::error_code ec = write_file_atomically(target.string(), "x");
REQUIRE(ec == std::errc::no_such_file_or_directory);
REQUIRE_FALSE(boost::filesystem::exists(target));
}
#ifndef _WIN32
// The read-only bit on a directory stops file creation only on POSIX.
TEST_CASE("write_file_atomically writes in place when no temporary can be created beside an existing target", "[utils]") {
if (::geteuid() == 0)
SKIP("a read-only directory does not stop root");
ScopedTemporaryDir dir;
const boost::filesystem::path target = dir.path() / "preset.json";
REQUIRE_FALSE(write_file_atomically(target.string(), "first"));
boost::filesystem::permissions(dir.path(), boost::filesystem::owner_read | boost::filesystem::owner_exe);
const std::error_code replaced = write_file_atomically(target.string(), "second");
const std::error_code created = write_file_atomically((dir.path() / "new.json").string(), "x");
// Restored before any assertion, so a failure never leaves an unremovable directory behind.
boost::filesystem::permissions(dir.path(), boost::filesystem::owner_all);
REQUIRE_FALSE(replaced);
REQUIRE(created == std::errc::permission_denied);
std::string content;
load_string_file(target, content);
REQUIRE(content == "second");
}
#endif
TEST_CASE("write_file_atomically keeps bytes intact in binary mode", "[utils]") {
ScopedTemporaryDir dir;
const boost::filesystem::path target = dir.path() / "blob.bin";
const std::string bytes("a\r\nb\0c", 6);
REQUIRE_FALSE(write_file_atomically(target.string(), bytes, /*binary=*/true));
REQUIRE(boost::filesystem::file_size(target) == bytes.size());
}
#ifndef _WIN32
TEST_CASE("write_file_atomically writes through a symlink and keeps the target's permissions", "[utils]") {
ScopedTemporaryDir dir;
const boost::filesystem::path real = dir.path() / "real.json";
const boost::filesystem::path link = dir.path() / "link.json";
REQUIRE_FALSE(write_file_atomically(real.string(), "first"));
boost::filesystem::permissions(real, boost::filesystem::owner_read | boost::filesystem::owner_write);
boost::filesystem::create_symlink(real, link);
REQUIRE_FALSE(write_file_atomically(link.string(), "second"));
REQUIRE(boost::filesystem::is_symlink(boost::filesystem::symlink_status(link)));
std::string content;
load_string_file(real, content);
REQUIRE(content == "second");
REQUIRE_FALSE(write_file_atomically(real.string(), "third"));
const auto perms = boost::filesystem::status(real).permissions() & boost::filesystem::all_all;
REQUIRE(perms == (boost::filesystem::owner_read | boost::filesystem::owner_write));
}
#endif
TEST_CASE("write_file_atomically survives two threads writing one target", "[utils]") {
ScopedTemporaryDir dir;
const boost::filesystem::path target = dir.path() / "shared.json";
const std::string a(20000, 'a'), b(20000, 'b');
std::thread other([&] {
for (int i = 0; i < 50; ++i)
write_file_atomically(target.string(), a);
});
for (int i = 0; i < 50; ++i)
write_file_atomically(target.string(), b);
other.join();
std::string content;
load_string_file(target, content);
const bool whole = content == a || content == b;
REQUIRE(whole);
// No temporary may be left; a scanner on Windows may briefly hold the old
// file under another name, so only the temporaries are counted.
size_t temporaries = 0;
for (auto &entry : boost::filesystem::directory_iterator(dir.path()))
if (entry.path().extension() == ".tmp")
++temporaries;
REQUIRE(temporaries == 0);
}
TEST_CASE("copy_file reports the OS error when the destination cannot be written", "[utils]") {
ScopedTemporaryFile source(".txt");
{
+18
View File
@@ -1804,6 +1804,24 @@ TEST_CASE("a header claiming more body than the file holds is rejected", "[Vendo
REQUIRE_FALSE(bundle.load_vendor_cache(cache, "Bounded", Semver(1, 0, 0)));
}
TEST_CASE("a failed write leaves the previous cache in place", "[VendorCache]")
{
TempDir tmp;
const std::string cache = (tmp.path / "Durable.opc").string();
REQUIRE(save_one_vendor(cache, one_vendor("Durable"), "Durable", "1.0.0"));
const std::string before = slurp(cache);
// A directory where the temp file wants to go: the write cannot complete,
// and must not have destroyed what was already there to find that out.
const fs::path blocker = fs::path(cache + "." + std::to_string(get_current_pid()) + ".tmp");
fs::create_directories(blocker);
REQUIRE_FALSE(save_one_vendor(cache, one_vendor("Durable"), "Durable", "2.0.0"));
CHECK(slurp(cache) == before);
fs::remove_all(blocker);
}
TEST_CASE("a cache written by another build's option ordering still loads", "[VendorCache]")
{
// The regression the fingerprint used to prevent by refusing the file