mirror of
https://github.com/OrcaSlicer/OrcaSlicer.git
synced 2026-10-05 06:41:10 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
64460d7f85 |
@@ -45,7 +45,7 @@ on:
|
||||
|
||||
|
||||
schedule:
|
||||
- cron: '15 2 * * *' # 10:15 AM Singapore time (UTC+8), when macOS runners are least busy
|
||||
- cron: '0 17 * * *' # run once a day at 1 AM Singapore time (UTC+8)
|
||||
|
||||
workflow_dispatch: # allows for manual dispatch
|
||||
inputs:
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
# Reports the newest push build of main that was not cancelled, for the README badge.
|
||||
name: Main build status
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Build all"]
|
||||
types: [completed]
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
actions: read
|
||||
|
||||
jobs:
|
||||
status:
|
||||
if: github.repository == 'OrcaSlicer/OrcaSlicer'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
for attempt in 1 2 3; do
|
||||
conclusion=$(gh api "repos/${{ github.repository }}/actions/workflows/build_all.yml/runs?branch=main&event=push&status=completed&per_page=100" \
|
||||
--jq '[.workflow_runs[] | select(.conclusion != "cancelled")][0].conclusion') && break
|
||||
sleep 10
|
||||
done
|
||||
echo "Latest finished push build of main: $conclusion"
|
||||
[ "$conclusion" = success ]
|
||||
@@ -6,7 +6,7 @@
|
||||
|
||||
<a href="https://trendshift.io/repositories/15552" target="_blank"><img src="https://trendshift.io/api/badge/repositories/15552" alt="OrcaSlicer%2FOrcaSlicer | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||
[](https://github.com/OrcaSlicer/OrcaSlicer/stargazers) [](https://github.com/OrcaSlicer/OrcaSlicer/actions/workflows/build_all.yml)
|
||||
[](https://github.com/OrcaSlicer/OrcaSlicer/stargazers) [](https://github.com/OrcaSlicer/OrcaSlicer/actions/workflows/build_all.yml)
|
||||
|
||||
OrcaSlicer: an open source Next-Gen Slicing Software for Precision 3D Prints.
|
||||
Optimize your prints with ultra-fast slicing, intelligent support generation, and seamless printer compatibility—engineered for perfection.
|
||||
|
||||
@@ -28,12 +28,6 @@ if (ORCA_TOOLS)
|
||||
target_link_libraries(generate_system_cache libslic3r boost_headeronly)
|
||||
target_compile_definitions(generate_system_cache PRIVATE ${_DEV_DEFS})
|
||||
|
||||
# texture_unwrap_dump: reports the LSCM unwrap of a saved project's texture displacement layers,
|
||||
# chart by chart, so a defect can be reproduced from the project file instead of from a screenshot.
|
||||
add_executable(texture_unwrap_dump texture_unwrap_dump.cpp)
|
||||
target_link_libraries(texture_unwrap_dump libslic3r boost_headeronly nanosvg)
|
||||
target_compile_definitions(texture_unwrap_dump PRIVATE ${_DEV_DEFS})
|
||||
|
||||
# profile_include_dump: prints what included templates contribute to a vendor's presets,
|
||||
# to diff against the same tool built in BambuStudio. Built only on request.
|
||||
add_executable(profile_include_dump EXCLUDE_FROM_ALL profile_include_dump.cpp)
|
||||
|
||||
@@ -1,292 +0,0 @@
|
||||
// Diagnostic for the LSCM unwrap of a texture displacement layer.
|
||||
//
|
||||
// It exists because the defect it hunts only shows up on a real painted patch: the paint mask is built
|
||||
// by TriangleSelector splitting base triangles, so the patch topology cannot be written down by hand,
|
||||
// and reasoning about it from a screenshot of the 3D view had already produced three wrong diagnoses.
|
||||
// This loads a saved project, rebuilds exactly the patch the bake would act on, runs the same unwrap,
|
||||
// and reports what came out - per chart, so a bad one can be pointed at rather than guessed at.
|
||||
//
|
||||
// texture_unwrap_dump <project.3mf>
|
||||
|
||||
// nanosvg is header-only and libslic3r's 3mf import references it without carrying the implementation,
|
||||
// so every executable that links libslic3r has to supply it. Must precede any include that pulls the
|
||||
// header in, or its include guard suppresses the implementation. Same pattern as the other dev tools.
|
||||
#define NANOSVG_IMPLEMENTATION
|
||||
#include "nanosvg/nanosvg.h"
|
||||
#define NANOSVGRAST_IMPLEMENTATION
|
||||
#include "nanosvg/nanosvgrast.h"
|
||||
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <functional>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "libslic3r/Model.hpp"
|
||||
#include "libslic3r/TextureDisplacement.hpp"
|
||||
#include "libslic3r/Format/bbs_3mf.hpp"
|
||||
#include "libslic3r/Utils.hpp"
|
||||
|
||||
#include <boost/filesystem.hpp>
|
||||
|
||||
using namespace Slic3r;
|
||||
|
||||
namespace {
|
||||
|
||||
uint64_t edge_key(int a, int b)
|
||||
{
|
||||
if (a > b)
|
||||
std::swap(a, b);
|
||||
return (uint64_t(uint32_t(a)) << 32) | uint32_t(b);
|
||||
}
|
||||
|
||||
// Boundary loops and the Euler characteristic of a face set, which together say whether a chart is the
|
||||
// topological disk LSCM needs (one loop, V - E + F == 1).
|
||||
void chart_topology(const indexed_triangle_set &mesh, const std::vector<int> &faces, int &loops, int &euler)
|
||||
{
|
||||
std::unordered_map<uint64_t, int> edge_use;
|
||||
std::unordered_map<int, int> local;
|
||||
for (const int f : faces) {
|
||||
const stl_triangle_vertex_indices &t = mesh.indices[size_t(f)];
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
++edge_use[edge_key(t[i], t[(i + 1) % 3])];
|
||||
local.emplace(t[i], int(local.size()));
|
||||
}
|
||||
}
|
||||
euler = int(local.size()) - int(edge_use.size()) + int(faces.size());
|
||||
|
||||
std::unordered_map<int, int> parent;
|
||||
const std::function<int(int)> find = [&](int x) {
|
||||
while (parent[x] != x)
|
||||
x = parent[x] = parent[parent[x]];
|
||||
return x;
|
||||
};
|
||||
for (const auto &[key, uses] : edge_use)
|
||||
if (uses == 1)
|
||||
for (const int v : { int(key >> 32), int(uint32_t(key)) })
|
||||
parent.emplace(v, v);
|
||||
for (const auto &[key, uses] : edge_use)
|
||||
if (uses == 1) {
|
||||
const int a = find(int(key >> 32)), b = find(int(uint32_t(key)));
|
||||
if (a != b)
|
||||
parent[b] = a;
|
||||
}
|
||||
std::unordered_map<int, int> roots;
|
||||
for (const auto &[v, p] : parent)
|
||||
roots[find(v)] = 1;
|
||||
loops = int(roots.size());
|
||||
}
|
||||
|
||||
float signed_area_2d(const Vec2f &a, const Vec2f &b, const Vec2f &c)
|
||||
{
|
||||
return 0.5f * ((b.x() - a.x()) * (c.y() - a.y()) - (c.x() - a.x()) * (b.y() - a.y()));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc < 2) {
|
||||
std::printf("usage: texture_unwrap_dump <project.3mf>\n");
|
||||
return 2;
|
||||
}
|
||||
|
||||
Model model;
|
||||
DynamicPrintConfig config;
|
||||
ConfigSubstitutionContext ctx(ForwardCompatibilitySubstitutionRule::Enable);
|
||||
PlateDataPtrs plate_data;
|
||||
std::vector<Preset *> project_presets;
|
||||
bool is_bbl_3mf = false, is_orca_3mf = false;
|
||||
Semver file_version;
|
||||
// The importer writes a backup copy under the data dir and silently loses objects without one.
|
||||
const boost::filesystem::path tmp = boost::filesystem::temp_directory_path() / "texture_unwrap_dump";
|
||||
boost::filesystem::create_directories(tmp);
|
||||
set_data_dir(tmp.string());
|
||||
|
||||
// LoadModel so the meshes come through; AddDefaultInstances because an object with no instance is
|
||||
// dropped by the plate mapping, which is what "skip this object" in the log means.
|
||||
if (!load_bbs_3mf(argv[1], &config, &ctx, &model, &plate_data, &project_presets, &is_bbl_3mf, &is_orca_3mf,
|
||||
&file_version, nullptr,
|
||||
LoadStrategy::LoadModel | LoadStrategy::LoadConfig | LoadStrategy::AddDefaultInstances |
|
||||
LoadStrategy::Silence)) {
|
||||
std::printf("failed to load %s\n", argv[1]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::printf("loaded: %zu object(s)\n", model.objects.size());
|
||||
|
||||
for (const ModelObject *object : model.objects)
|
||||
for (const ModelVolume *volume : object->volumes) {
|
||||
if (volume->texture_displacement_layers.empty()) {
|
||||
std::printf("volume \"%s\": no texture displacement layers; paint masks per slot:",
|
||||
volume->name.c_str());
|
||||
for (int i = 0; i < int(TEXTURE_DISPLACEMENT_MAX_LAYERS); ++i)
|
||||
std::printf(" %zu", volume->texture_displacement_facet(i).get_data().triangles_to_split.size());
|
||||
std::printf("\n");
|
||||
continue;
|
||||
}
|
||||
std::printf("volume \"%s\": %zu base triangles, %zu layer(s)\n", volume->name.c_str(),
|
||||
volume->mesh().its.indices.size(), volume->texture_displacement_layers.size());
|
||||
|
||||
for (const TextureDisplacementLayer &layer : volume->texture_displacement_layers) {
|
||||
std::printf("\n layer %d \"%s\" mapping=%d seam_angle=%.1f connect=%d islands_stored=%zu\n",
|
||||
layer.slot, layer.name.c_str(), int(layer.projection_method),
|
||||
layer.lscm_seam_angle_deg, int(layer.auto_connect_islands), layer.islands.size());
|
||||
if (layer.projection_method != TextureProjectionMethod::LSCM)
|
||||
continue;
|
||||
|
||||
const indexed_triangle_set patch =
|
||||
extract_painted_patch(volume->mesh().its, volume->texture_displacement_facet(layer.slot).get_data());
|
||||
std::printf(" patch: %zu vertices, %zu triangles\n", patch.vertices.size(), patch.indices.size());
|
||||
if (patch.indices.empty())
|
||||
continue;
|
||||
|
||||
const auto t0 = std::chrono::steady_clock::now();
|
||||
const PatchUnwrap unwrap = compute_patch_unwrap(patch, layer.lscm_seam_angle_deg, 0.f,
|
||||
layer.lscm_seam_edges);
|
||||
const auto t1 = std::chrono::steady_clock::now();
|
||||
std::printf(" TIMING compute_patch_unwrap: %.0f ms\n",
|
||||
std::chrono::duration<double, std::milli>(t1 - t0).count());
|
||||
std::printf(" unwrap: %d charts, %zu unwrapped triangles\n", unwrap.chart_count,
|
||||
unwrap.indices.size());
|
||||
|
||||
// Group the patch's faces by chart so each can be examined on its own.
|
||||
std::vector<std::vector<int>> chart_faces(size_t(std::max(unwrap.chart_count, 0)));
|
||||
for (size_t i = 0; i < unwrap.indices.size(); ++i) {
|
||||
const int chart = unwrap.vertex_chart[size_t(unwrap.indices[i][0])];
|
||||
if (chart >= 0 && size_t(chart) < chart_faces.size())
|
||||
chart_faces[size_t(chart)].push_back(unwrap.source_face[i]);
|
||||
}
|
||||
|
||||
int bad_charts = 0;
|
||||
for (size_t c = 0; c < chart_faces.size(); ++c) {
|
||||
int loops = 0, euler = 0;
|
||||
chart_topology(patch, chart_faces[c], loops, euler);
|
||||
|
||||
// Flipped triangles: the unwrap folded over itself, which is what a planar fallback
|
||||
// does to a chart that is not flat. Measured on the unwrap's own triangles.
|
||||
int pos = 0, neg = 0;
|
||||
for (size_t i = 0; i < unwrap.indices.size(); ++i) {
|
||||
const stl_triangle_vertex_indices &t = unwrap.indices[i];
|
||||
if (unwrap.vertex_chart[size_t(t[0])] != int(c))
|
||||
continue;
|
||||
const float a = signed_area_2d(unwrap.uvs[size_t(t[0])], unwrap.uvs[size_t(t[1])],
|
||||
unwrap.uvs[size_t(t[2])]);
|
||||
if (a > 0.f) ++pos; else if (a < 0.f) ++neg;
|
||||
}
|
||||
const int flipped = std::min(pos, neg);
|
||||
const bool disk = loops == 1 && euler == 1;
|
||||
if (!disk || flipped > 0) {
|
||||
++bad_charts;
|
||||
std::printf(" chart %2zu: %4zu faces loops=%d euler=%d%s flipped=%d/%d%s\n", c,
|
||||
chart_faces[c].size(), loops, euler, disk ? "" : " NOT A DISK", flipped,
|
||||
pos + neg, flipped ? " FOLDED" : "");
|
||||
}
|
||||
}
|
||||
std::printf(" charts with a defect: %d / %d\n", bad_charts, unwrap.chart_count);
|
||||
|
||||
// What the eye actually sees. Every patch edge shared by two charts should carry the same
|
||||
// UV on both sides once the islands are laid out as a connected net; where it does not,
|
||||
// the texture jumps across that seam. Measured through compute_lscm_uvs(), i.e. the exact
|
||||
// coordinates the bake and the checker overlay sample.
|
||||
{
|
||||
const auto n0 = std::chrono::steady_clock::now();
|
||||
const std::vector<TextureIsland> net = compute_connected_net(unwrap);
|
||||
const auto n1 = std::chrono::steady_clock::now();
|
||||
std::printf(" TIMING compute_connected_net: %.0f ms (%zu islands)\n",
|
||||
std::chrono::duration<double, std::milli>(n1 - n0).count(), net.size());
|
||||
}
|
||||
const auto t2 = std::chrono::steady_clock::now();
|
||||
const std::vector<Vec2f> uv = compute_lscm_uvs(patch, layer);
|
||||
const auto t3 = std::chrono::steady_clock::now();
|
||||
std::printf(" TIMING compute_lscm_uvs: %.0f ms (called on every preview, overlay and bake)\n",
|
||||
std::chrono::duration<double, std::milli>(t3 - t2).count());
|
||||
if (uv.size() != patch.vertices.size()) {
|
||||
std::printf(" compute_lscm_uvs returned %zu uvs for %zu vertices\n", uv.size(),
|
||||
patch.vertices.size());
|
||||
continue;
|
||||
}
|
||||
// Per-corner UVs carry each chart's own placement, so an edge shared by two charts shows
|
||||
// the jump directly: the same mesh vertex lands at two different UVs. That is exactly what
|
||||
// the eye reads as the texture breaking.
|
||||
const auto t4 = std::chrono::steady_clock::now();
|
||||
const std::vector<Vec2f> corner = compute_lscm_corner_uvs(patch, layer);
|
||||
const auto t5 = std::chrono::steady_clock::now();
|
||||
std::printf(" TIMING compute_lscm_corner_uvs: %.0f ms\n",
|
||||
std::chrono::duration<double, std::milli>(t5 - t4).count());
|
||||
// Keyed by edge, holding the UV each incident face gives to the edge's *lower-numbered*
|
||||
// endpoint. Comparing that same vertex on both sides is the point: indexing by corner
|
||||
// position instead compares opposite ends of the edge, because the two faces wind it in
|
||||
// opposite directions.
|
||||
std::unordered_map<uint64_t, std::vector<Vec2f>> edge_seen;
|
||||
if (corner.size() == patch.indices.size() * 3)
|
||||
for (size_t f = 0; f < patch.indices.size(); ++f) {
|
||||
const stl_triangle_vertex_indices &t = patch.indices[f];
|
||||
for (int k = 0; k < 3; ++k) {
|
||||
const int a = t[k], b = t[(k + 1) % 3];
|
||||
const int probe = std::min(a, b);
|
||||
const int local = (a == probe) ? k : (k + 1) % 3;
|
||||
edge_seen[edge_key(a, b)].push_back(corner[f * 3 + size_t(local)]);
|
||||
}
|
||||
}
|
||||
// Which chart each patch face belongs to, so a broken edge can be attributed to a pair.
|
||||
std::vector<int> chart_of_face(patch.indices.size(), -1);
|
||||
for (size_t i = 0; i < unwrap.indices.size(); ++i)
|
||||
chart_of_face[size_t(unwrap.source_face[i])] = unwrap.vertex_chart[size_t(unwrap.indices[i][0])];
|
||||
|
||||
std::unordered_map<uint64_t, std::vector<int>> edge_faces;
|
||||
for (size_t f = 0; f < patch.indices.size(); ++f) {
|
||||
const stl_triangle_vertex_indices &t = patch.indices[f];
|
||||
for (int k = 0; k < 3; ++k)
|
||||
edge_faces[edge_key(t[k], t[(k + 1) % 3])].push_back(int(f));
|
||||
}
|
||||
|
||||
int adjacent = 0, broken = 0, broken_same_chart = 0;
|
||||
float worst = 0.f;
|
||||
std::map<std::pair<int, int>, std::pair<int, float>> by_pair;
|
||||
for (const auto &[key, seen] : edge_seen) {
|
||||
if (seen.size() != 2)
|
||||
continue;
|
||||
++adjacent;
|
||||
const float d = (seen[0] - seen[1]).norm();
|
||||
if (d <= 1e-4f)
|
||||
continue;
|
||||
++broken;
|
||||
worst = std::max(worst, d);
|
||||
const auto &faces_here = edge_faces[key];
|
||||
int c1 = -1, c2 = -1;
|
||||
if (faces_here.size() == 2) {
|
||||
c1 = chart_of_face[size_t(faces_here[0])];
|
||||
c2 = chart_of_face[size_t(faces_here[1])];
|
||||
}
|
||||
if (c1 == c2)
|
||||
++broken_same_chart;
|
||||
auto &slot = by_pair[{ std::min(c1, c2), std::max(c1, c2) }];
|
||||
++slot.first;
|
||||
slot.second = std::max(slot.second, d);
|
||||
}
|
||||
std::printf(" broken edges inside a single chart: %d\n", broken_same_chart);
|
||||
std::printf(" broken by chart pair:");
|
||||
for (const auto &[pk, v] : by_pair)
|
||||
std::printf(" (%d,%d)x%d/%.1f", pk.first, pk.second, v.first, v.second);
|
||||
std::printf("\n");
|
||||
// Total length of the seams left broken, in mm: how much visibly torn edge the layout has,
|
||||
// which is what the eye adds up. A count alone hides whether the breaks are hairlines or
|
||||
// whole sides of an island.
|
||||
float seam_mm = 0.f;
|
||||
for (const auto &[key, seen] : edge_seen) {
|
||||
if (seen.size() != 2 || (seen[0] - seen[1]).norm() <= 1e-4f)
|
||||
continue;
|
||||
seam_mm += (patch.vertices[size_t(key >> 32)] - patch.vertices[size_t(uint32_t(key))]).norm();
|
||||
}
|
||||
std::printf(" interior edges: %d, discontinuous: %d, total torn seam: %.2f mm (worst jump %.3f)\n",
|
||||
adjacent, broken, seam_mm, worst);
|
||||
std::printf(" stored islands %zu vs charts %d -> %s\n", layer.islands.size(),
|
||||
unwrap.chart_count,
|
||||
layer.islands.size() == size_t(unwrap.chart_count) ? "stored placements used"
|
||||
: "net rebuilt");
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
+60
-42
@@ -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))
|
||||
m_dirty = false;
|
||||
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,38 +1336,38 @@ 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))
|
||||
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());
|
||||
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 replace is not safe in case of an application crash, there is no atomic "rename file" API
|
||||
// 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.
|
||||
body += appconfig_md5_hash_line(checksum_source);
|
||||
c << appconfig_md5_hash_line(config_str);
|
||||
#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;
|
||||
c.close();
|
||||
if (c.fail()) {
|
||||
BOOST_LOG_TRIVIAL(error) << "Failed to write new configuration to " << path_pid << "; aborting attempt to overwrite original configuration";
|
||||
return;
|
||||
}
|
||||
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();
|
||||
// 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
|
||||
return true;
|
||||
|
||||
// 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::get_variant(const std::string &vendor, const std::string &model, const std::string &variant) const
|
||||
{
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1538,10 +1538,12 @@ 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_LOG_TRIVIAL(info) << __FUNCTION__ << ":" <<__LINE__ << boost::format(", saved config to %1%\n")%file;
|
||||
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;
|
||||
}
|
||||
|
||||
void ConfigBase::save_to_json(std::ostream &os, const std::string &name, const std::string &from, const std::string &version, bool replace_invalid_utf8) const
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
+28
-129
@@ -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));
|
||||
@@ -1749,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,
|
||||
@@ -1776,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);
|
||||
@@ -1891,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;
|
||||
|
||||
@@ -1900,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))
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " added compatible_printers for preset: " << preset.name;
|
||||
else
|
||||
++m_errors; // save() logged why
|
||||
if (!read_only)
|
||||
preset.save(nullptr);
|
||||
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " added compatible_printers for preset: " << preset.name;
|
||||
} catch (const std::runtime_error &err) {
|
||||
++m_errors;
|
||||
BOOST_LOG_TRIVIAL(error) << __FUNCTION__ << " can not write compatible_printers back to " << preset.file << ": " << err.what();
|
||||
@@ -1991,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";
|
||||
@@ -2326,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;
|
||||
}
|
||||
}
|
||||
@@ -4398,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;
|
||||
@@ -4538,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.
|
||||
@@ -4731,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());
|
||||
}
|
||||
boost::nowide::remove(printer.file.c_str());
|
||||
m_printers.erase(it);
|
||||
return true;
|
||||
}
|
||||
@@ -4746,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());
|
||||
}
|
||||
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
|
||||
|
||||
@@ -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.
|
||||
@@ -499,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());
|
||||
|
||||
@@ -914,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
|
||||
@@ -941,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;
|
||||
@@ -1132,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);
|
||||
|
||||
@@ -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"
|
||||
@@ -590,13 +588,15 @@ const PresetBundle *PresetBundle::load_source_vendor(const boost::filesystem::pa
|
||||
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 / (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);
|
||||
// is the same one that resolves the library's own presets. It is only a base, so
|
||||
// it comes from its cache whenever that is all that is installed, even when the
|
||||
// vendor itself is parsed (a vendor updated over the air).
|
||||
const std::string library_file = std::string(ORCA_FILAMENT_LIBRARY);
|
||||
const bool library_json = boost::filesystem::is_regular_file(root_dir / (library_file + ".json"));
|
||||
const bool library_cache_only = !library_json && boost::filesystem::is_regular_file(root_dir / (library_file + ".opc"));
|
||||
const PresetBundle *library = nullptr;
|
||||
if (vendor_id != ORCA_FILAMENT_LIBRARY && (library_json || library_cache_only)) {
|
||||
library = load_source_vendor(root_dir, ORCA_FILAMENT_LIBRARY, compatibility_rule, error, allow_cache || library_cache_only);
|
||||
if (library == nullptr) {
|
||||
error = "OrcaFilamentLibrary contains invalid presets";
|
||||
return nullptr;
|
||||
@@ -1274,13 +1274,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)) {
|
||||
@@ -1294,7 +1287,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();
|
||||
@@ -1329,7 +1322,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();
|
||||
@@ -1663,12 +1656,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();
|
||||
@@ -1680,9 +1671,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";
|
||||
@@ -2290,9 +2278,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)
|
||||
@@ -2540,7 +2529,7 @@ void PresetBundle::clear_printer_hold_aliases()
|
||||
|
||||
//BBS: add json related logic, load system presets from json
|
||||
std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_presets_from_json(
|
||||
ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache)
|
||||
ForwardCompatibilitySubstitutionRule compatibility_rule, bool write_caches)
|
||||
{
|
||||
//BBS: add config related logs
|
||||
BOOST_LOG_TRIVIAL(debug) << __FUNCTION__ << boost::format(" enter, compatibility_rule %1%")%compatibility_rule;
|
||||
@@ -2560,14 +2549,14 @@ std::pair<PresetsConfigSubstitutions, std::string> PresetBundle::load_system_pre
|
||||
// The vendors below are loaded whole and against each other — the filament
|
||||
// library first, then every other vendor with it as the base — so each parse
|
||||
// is complete enough to be worth caching.
|
||||
m_generate_vendor_caches = allow_cache && (m_generate_vendor_caches || !validation_mode);
|
||||
m_generate_vendor_caches = write_caches && (m_generate_vendor_caches || !validation_mode);
|
||||
|
||||
// Sorted, so any duplicate-preset warning comes out in the same order on every run.
|
||||
std::vector<VendorSource> vendors;
|
||||
for (const std::string& name : vendor_names_in(dir))
|
||||
if (name == ORCA_FILAMENT_LIBRARY || !(validation_mode && !vendor_to_validate.empty() && name != vendor_to_validate))
|
||||
vendors.push_back({ name, dir });
|
||||
auto result = this->load_vendors(vendors, compatibility_rule, allow_cache);
|
||||
auto result = this->load_vendors(vendors, compatibility_rule, true);
|
||||
|
||||
this->update_system_maps();
|
||||
|
||||
@@ -8226,13 +8215,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;
|
||||
|
||||
@@ -839,7 +839,9 @@ private:
|
||||
|
||||
//std::pair<PresetsConfigSubstitutions, std::string> load_system_presets(ForwardCompatibilitySubstitutionRule compatibility_rule);
|
||||
//BBS: add json related logic
|
||||
std::pair<PresetsConfigSubstitutions, std::string> load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule, bool allow_cache = true);
|
||||
// Reads each vendor from its preset cache where one covers the profile, as every load does.
|
||||
// write_caches = false keeps a read-only load from writing caches into the data directory.
|
||||
std::pair<PresetsConfigSubstitutions, std::string> load_system_presets_from_json(ForwardCompatibilitySubstitutionRule compatibility_rule, bool write_caches = true);
|
||||
// Update the multicolor information for filaments.
|
||||
void update_filament_multi_color();
|
||||
// Update renamed_from and alias maps of system profiles.
|
||||
|
||||
@@ -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());
|
||||
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();
|
||||
{
|
||||
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();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1236,81 +1236,52 @@ bool triangles_overlap(const Tri2 &a, const Tri2 &b, float eps)
|
||||
struct NetGrid
|
||||
{
|
||||
static constexpr int BIG_SPAN = 16;
|
||||
// Each stored triangle keeps its own bounding box. Overlap testing is dominated by rejects - a cell
|
||||
// holds every triangle whose box touches it, and a candidate meets only a couple of them for real -
|
||||
// so paying six floats per entry to answer most of those rejects with four comparisons, instead of a
|
||||
// full triangle intersection, is what makes the net affordable. Measured on a 42k-triangle patch the
|
||||
// grid ran ~19 million candidate pairs per net, nearly all of them misses, and rejecting them this
|
||||
// way took the net from ~175 ms to ~53 ms.
|
||||
//
|
||||
// The box rides inside the entry rather than in a parallel array: splitting them to scan boxes back
|
||||
// to back was tried and came out slower, because each bucket then grows two vectors instead of one.
|
||||
struct Entry
|
||||
{
|
||||
Tri2 tri;
|
||||
Vec2f lo, hi;
|
||||
};
|
||||
float cell;
|
||||
float eps;
|
||||
std::unordered_map<uint64_t, std::vector<Entry>> cells;
|
||||
std::vector<Entry> big;
|
||||
float cell;
|
||||
float eps;
|
||||
std::unordered_map<uint64_t, std::vector<Tri2>> cells;
|
||||
std::vector<Tri2> big;
|
||||
|
||||
static uint64_t key(int x, int y) { return (uint64_t(uint32_t(x)) << 32) | uint32_t(y); }
|
||||
static Entry entry(const Tri2 &t)
|
||||
{
|
||||
return Entry{ t, t[0].cwiseMin(t[1]).cwiseMin(t[2]), t[0].cwiseMax(t[1]).cwiseMax(t[2]) };
|
||||
}
|
||||
bool range(const Vec2f &lo, const Vec2f &hi, int &x0, int &y0, int &x1, int &y1) const
|
||||
bool range(const Tri2 &t, int &x0, int &y0, int &x1, int &y1) const
|
||||
{
|
||||
const Vec2f lo = t[0].cwiseMin(t[1]).cwiseMin(t[2]), hi = t[0].cwiseMax(t[1]).cwiseMax(t[2]);
|
||||
x0 = int(std::floor(lo.x() / cell));
|
||||
y0 = int(std::floor(lo.y() / cell));
|
||||
x1 = int(std::floor(hi.x() / cell));
|
||||
y1 = int(std::floor(hi.y() / cell));
|
||||
return x1 - x0 <= BIG_SPAN && y1 - y0 <= BIG_SPAN;
|
||||
}
|
||||
// Boxes grown by eps on both sides, to match the tolerance triangles_overlap() itself works to: a
|
||||
// reject here must never discard a pair that test would have called touching.
|
||||
bool boxes_apart(const Entry &a, const Entry &b) const
|
||||
{
|
||||
return a.hi.x() + eps < b.lo.x() || b.hi.x() + eps < a.lo.x() || a.hi.y() + eps < b.lo.y() ||
|
||||
b.hi.y() + eps < a.lo.y();
|
||||
}
|
||||
bool hits(const Entry &q, const std::vector<Entry> &bucket) const
|
||||
{
|
||||
for (const Entry &b : bucket)
|
||||
if (!boxes_apart(q, b) && triangles_overlap(q.tri, b.tri, eps))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
bool overlaps(const Tri2 &t) const
|
||||
{
|
||||
const Entry q = entry(t);
|
||||
if (hits(q, big))
|
||||
return true;
|
||||
for (const Tri2 &b : big)
|
||||
if (triangles_overlap(t, b, eps))
|
||||
return true;
|
||||
int x0, y0, x1, y1;
|
||||
if (!range(q.lo, q.hi, x0, y0, x1, y1)) {
|
||||
for (const auto &[k, bucket] : cells)
|
||||
if (hits(q, bucket))
|
||||
return true;
|
||||
if (!range(t, x0, y0, x1, y1)) {
|
||||
for (const auto &[k, tris] : cells)
|
||||
for (const Tri2 &b : tris)
|
||||
if (triangles_overlap(t, b, eps))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
for (int x = x0; x <= x1; ++x)
|
||||
for (int y = y0; y <= y1; ++y)
|
||||
if (const auto it = cells.find(key(x, y)); it != cells.end() && hits(q, it->second))
|
||||
return true;
|
||||
if (const auto it = cells.find(key(x, y)); it != cells.end())
|
||||
for (const Tri2 &b : it->second)
|
||||
if (triangles_overlap(t, b, eps))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
void insert(const Tri2 &t)
|
||||
{
|
||||
const Entry e = entry(t);
|
||||
int x0, y0, x1, y1;
|
||||
if (!range(e.lo, e.hi, x0, y0, x1, y1)) {
|
||||
big.push_back(e);
|
||||
int x0, y0, x1, y1;
|
||||
if (!range(t, x0, y0, x1, y1)) {
|
||||
big.push_back(t);
|
||||
return;
|
||||
}
|
||||
for (int x = x0; x <= x1; ++x)
|
||||
for (int y = y0; y <= y1; ++y)
|
||||
cells[key(x, y)].push_back(e);
|
||||
cells[key(x, y)].push_back(t);
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
@@ -1322,20 +1293,11 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
|
||||
if (n <= 1)
|
||||
return islands;
|
||||
|
||||
// Chart adjacency, with one representative shared edge per adjacent pair: the fold line the pair is
|
||||
// unfolded about.
|
||||
//
|
||||
// Which edge that is matters, because two charts can touch along more than one run. A chart cut open
|
||||
// to flatten it - a ring opened by segment_into_charts(), say - touches its other half along *both*
|
||||
// sides of the cut. Folding is rigid, so only the run the fold line belongs to comes out matching;
|
||||
// every other run is left mismatched, and a mismatched run is exactly where the texture visibly
|
||||
// jumps. Taking whichever edge the map happened to yield first therefore left the long side broken
|
||||
// about as often as the short one. The fold line is picked from the longest run instead, so what is
|
||||
// left discontinuous is the shortest boundary the pair has.
|
||||
// Chart adjacency, with one representative shared edge per adjacent pair.
|
||||
const auto edges = build_shared_edges(unwrap);
|
||||
struct PairEdge { ChartEdge a, b; };
|
||||
struct SharedEdge { PairEdge fold; int base_lo = -1, base_hi = -1; float length = 0.f; };
|
||||
std::map<std::pair<int, int>, std::vector<SharedEdge>> pair_shared;
|
||||
std::map<std::pair<int, int>, PairEdge> pair_edge;
|
||||
std::vector<std::vector<int>> adj(static_cast<size_t>(n));
|
||||
for (const auto &[base_edge, list] : edges) {
|
||||
for (size_t i = 0; i < list.size(); ++i)
|
||||
for (size_t j = i + 1; j < list.size(); ++j) {
|
||||
@@ -1343,50 +1305,14 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
|
||||
if (c1 == c2 || c1 < 0 || c2 < 0 || c1 >= n || c2 >= n)
|
||||
continue;
|
||||
const std::pair<int, int> pk{ std::min(c1, c2), std::max(c1, c2) };
|
||||
SharedEdge se;
|
||||
se.fold = (c1 < c2) ? PairEdge{ list[i], list[j] } : PairEdge{ list[j], list[i] };
|
||||
se.base_lo = base_edge.first;
|
||||
se.base_hi = base_edge.second;
|
||||
// The unwrap is scaled to true surface area, so a uv distance is a length in mm.
|
||||
se.length = (unwrap.uvs[size_t(se.fold.a.uv_lo)] - unwrap.uvs[size_t(se.fold.a.uv_hi)]).norm();
|
||||
pair_shared[pk].push_back(se);
|
||||
if (pair_edge.count(pk))
|
||||
continue; // keep the first shared edge as the fold line for this pair
|
||||
pair_edge[pk] = (c1 < c2) ? PairEdge{ list[i], list[j] } : PairEdge{ list[j], list[i] };
|
||||
adj[size_t(pk.first)].push_back(pk.second);
|
||||
adj[size_t(pk.second)].push_back(pk.first);
|
||||
}
|
||||
}
|
||||
|
||||
std::map<std::pair<int, int>, PairEdge> pair_edge;
|
||||
std::map<std::pair<int, int>, float> pair_weight; // length of the run each pair folds across
|
||||
std::vector<std::vector<int>> adj(static_cast<size_t>(n));
|
||||
for (const auto &[pk, shared] : pair_shared) {
|
||||
// Group the pair's shared edges into runs - edges joined end to end through a base vertex - and
|
||||
// total each run's length.
|
||||
std::unordered_map<int, int> local;
|
||||
for (const SharedEdge &se : shared)
|
||||
for (const int v : { se.base_lo, se.base_hi })
|
||||
local.emplace(v, int(local.size()));
|
||||
UnionFind runs(local.size());
|
||||
for (const SharedEdge &se : shared)
|
||||
runs.unite(local[se.base_lo], local[se.base_hi]);
|
||||
|
||||
std::unordered_map<int, float> run_length;
|
||||
std::unordered_map<int, size_t> run_first;
|
||||
for (size_t i = 0; i < shared.size(); ++i) {
|
||||
const int root = runs.find(local[shared[i].base_lo]);
|
||||
run_length[root] += shared[i].length;
|
||||
run_first.emplace(root, i);
|
||||
}
|
||||
int best_root = -1;
|
||||
float best_len = -1.f;
|
||||
for (const auto &[root, len] : run_length)
|
||||
if (len > best_len) { best_len = len; best_root = root; }
|
||||
if (best_root < 0)
|
||||
continue;
|
||||
|
||||
pair_edge[pk] = shared[run_first[best_root]].fold;
|
||||
pair_weight[pk] = best_len;
|
||||
adj[size_t(pk.first)].push_back(pk.second);
|
||||
adj[size_t(pk.second)].push_back(pk.first);
|
||||
}
|
||||
|
||||
// Per chart: its vertices, its triangles and its flattened area.
|
||||
std::vector<std::vector<int>> chart_verts(static_cast<size_t>(n)), chart_tris(static_cast<size_t>(n));
|
||||
std::vector<float> chart_area(static_cast<size_t>(n), 0.f);
|
||||
@@ -1440,29 +1366,15 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
|
||||
for (const int t : chart_tris[size_t(root)])
|
||||
grid.insert(placed(m, t));
|
||||
}
|
||||
// Grown strongest-adjacency-first (Prim, not breadth-first): a chart is folded onto whichever
|
||||
// neighbour it shares the longest boundary with, among everything reachable so far. Order matters
|
||||
// because only the fold a chart is actually reached by comes out matching - every other boundary
|
||||
// it has is left to chance. Taking neighbours in breadth-first order, biggest-area first, let a
|
||||
// far-off branch claim a chart across a short boundary before its true neighbour was reached, and
|
||||
// the long boundary they shared then stayed broken. That is the visible seam next to a hole: a
|
||||
// ring is cut into two halves that share a long boundary, and whichever half was reached first
|
||||
// took the other one along some unrelated edge.
|
||||
using Candidate = std::pair<float, std::pair<int, int>>; // weight, (from, to)
|
||||
std::priority_queue<Candidate> q;
|
||||
const auto push_neighbours = [&](int p) {
|
||||
for (const int c : adj[size_t(p)])
|
||||
if (net_of[size_t(c)] < 0 && !chart_tris[size_t(c)].empty()) {
|
||||
const auto w = pair_weight.find({ std::min(p, c), std::max(p, c) });
|
||||
q.push({ w == pair_weight.end() ? 0.f : w->second, { p, c } });
|
||||
}
|
||||
};
|
||||
push_neighbours(root);
|
||||
std::queue<int> q;
|
||||
q.push(root);
|
||||
while (!q.empty()) {
|
||||
const auto [weight, link] = q.top();
|
||||
const int p = q.front();
|
||||
q.pop();
|
||||
const int p = link.first, c = link.second;
|
||||
{
|
||||
std::vector<int> neighbours = adj[size_t(p)];
|
||||
std::stable_sort(neighbours.begin(), neighbours.end(),
|
||||
[&chart_area](int a, int b) { return chart_area[size_t(a)] > chart_area[size_t(b)]; });
|
||||
for (const int c : neighbours) {
|
||||
if (net_of[size_t(c)] >= 0 || chart_tris[size_t(c)].empty())
|
||||
continue;
|
||||
const auto it = pair_edge.find({ std::min(p, c), std::max(p, c) });
|
||||
@@ -1493,7 +1405,7 @@ std::vector<TextureIsland> compute_connected_net(const PatchUnwrap &unwrap)
|
||||
for (const Tri2 &t : tris)
|
||||
grid.insert(t);
|
||||
net_of[size_t(c)] = net;
|
||||
push_neighbours(c);
|
||||
q.push(c);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5129,5 +5041,4 @@ indexed_triangle_set cut_mesh_at_steps(const indexed_triangle_set &mesh, const s
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
} // namespace Slic3r
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
+12
-21
@@ -87,7 +87,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"
|
||||
@@ -3557,7 +3556,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();
|
||||
|
||||
});
|
||||
@@ -7652,11 +7651,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);
|
||||
}
|
||||
boost::system::error_code ec;
|
||||
boost::filesystem::remove_all(bundle_folder, ec);
|
||||
|
||||
preset_bundle->bundles.WriteLock();
|
||||
preset_bundle->bundles.m_bundles.erase(bundle.id);
|
||||
@@ -8948,7 +8944,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;
|
||||
}
|
||||
@@ -9011,19 +9006,15 @@ 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 (!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 .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)
|
||||
|
||||
@@ -276,10 +276,7 @@ void MonitorPanel::select_machine(std::string machine_sn)
|
||||
|
||||
void MonitorPanel::on_timer(wxTimerEvent& event)
|
||||
{
|
||||
// MediaPlayCtrl may yield the event loop while it joins its camera worker
|
||||
// during window teardown. Do not let a queued monitor refresh touch panels
|
||||
// that are already being destroyed.
|
||||
if (!wxGetApp().is_closing() && update_flag) {
|
||||
if (update_flag) {
|
||||
update_all();
|
||||
//Layout();
|
||||
}
|
||||
|
||||
@@ -23,15 +23,14 @@
|
||||
#include <wx/filedlg.h>
|
||||
#include <miniz.h>
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include "Plater.hpp"
|
||||
#include "BitmapCache.hpp"
|
||||
#include "slic3r/GUI/GUI_App.hpp"
|
||||
|
||||
#include "DeviceCore/DevManager.h"
|
||||
#include "DeviceCore/DevStorage.h"
|
||||
#include "../Utils/Http.hpp"
|
||||
#include "md4c/src/md4c-html.h"
|
||||
#include "../Utils/Http.hpp"
|
||||
|
||||
namespace Slic3r { namespace GUI {
|
||||
|
||||
@@ -1778,7 +1777,7 @@ void InputIpAddressDialog::set_machine_obj(MachineObject* obj)
|
||||
auto str_ip = m_input_ip->GetTextCtrl()->GetValue();
|
||||
auto str_access_code = m_input_access_code->GetTextCtrl()->GetValue();
|
||||
// ORCA enabling / disabling buttons with conditions enough to change its style
|
||||
m_button_ok->Enable(isValidEndpoint(str_ip.ToStdString()) &&
|
||||
m_button_ok->Enable(isIp(str_ip.ToStdString()) &&
|
||||
(str_access_code.IsEmpty() || str_access_code.Length() >= 8));
|
||||
|
||||
Layout();
|
||||
@@ -1816,29 +1815,19 @@ void InputIpAddressDialog::update_test_msg(wxString msg,bool connected)
|
||||
Fit();
|
||||
}
|
||||
|
||||
bool InputIpAddressDialog::isValidEndpoint(std::string endpoint)
|
||||
bool InputIpAddressDialog::isIp(std::string ipstr)
|
||||
{
|
||||
if (endpoint.empty() || std::any_of(endpoint.begin(), endpoint.end(), [](unsigned char c) {
|
||||
return std::isspace(c) != 0;
|
||||
}))
|
||||
return false;
|
||||
|
||||
const bool has_http_scheme = endpoint.rfind("http://", 0) == 0;
|
||||
const bool has_https_scheme = endpoint.rfind("https://", 0) == 0;
|
||||
const auto scheme_pos = endpoint.find("://");
|
||||
if (scheme_pos != std::string::npos && !has_http_scheme && !has_https_scheme)
|
||||
return false;
|
||||
|
||||
std::string port;
|
||||
const std::string host = Http::get_host_from_url(endpoint, &port);
|
||||
if (host.empty())
|
||||
return false;
|
||||
|
||||
// get_host_from_url returns its input when libcurl cannot parse it. For a
|
||||
// URL with a scheme, that means a failed parse still needs to be rejected.
|
||||
if (scheme_pos != std::string::npos && host == endpoint)
|
||||
return false;
|
||||
|
||||
istringstream ipstream(ipstr);
|
||||
int num[4];
|
||||
char point[3];
|
||||
string end;
|
||||
ipstream >> num[0] >> point[0] >> num[1] >> point[1] >> num[2] >> point[2] >> num[3] >> end;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
if (num[i] < 0 || num[i]>255) return false;
|
||||
if (point[i] != '.') return false;
|
||||
}
|
||||
if (num[3] < 0 || num[3]>255) return false;
|
||||
if (!end.empty()) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -2141,7 +2130,7 @@ void InputIpAddressDialog::on_text(wxCommandEvent &evt)
|
||||
|
||||
// ORCA enabling / disabling buttons with conditions enough to change its style
|
||||
bool valid_access_code_length = str_access_code.IsEmpty() || str_access_code.Length() >= 8;
|
||||
bool enable_btns = isValidEndpoint(str_ip.ToStdString()) && valid_access_code_length && invalid_access_code;
|
||||
bool enable_btns = isIp(str_ip.ToStdString()) && valid_access_code_length && invalid_access_code;
|
||||
m_button_manual_setup->Enable(enable_btns);
|
||||
m_button_ok->Enable(enable_btns);
|
||||
|
||||
|
||||
@@ -348,7 +348,7 @@ public:
|
||||
void update_title(wxString title);
|
||||
void set_machine_obj(MachineObject* obj);
|
||||
void update_test_msg(wxString msg, bool connected);
|
||||
bool isValidEndpoint(std::string endpoint);
|
||||
bool isIp(std::string ipstr);
|
||||
void check_ip_address_failed(int result);
|
||||
void on_check_ip_address_failed(wxCommandEvent& evt);
|
||||
void on_ok(wxMouseEvent& evt);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "Http.hpp"
|
||||
|
||||
#include <atomic>
|
||||
#include <cstdlib>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
@@ -123,7 +122,7 @@ struct Http::priv
|
||||
std::string error_buffer; // Used for CURLOPT_ERRORBUFFER
|
||||
std::string headers;
|
||||
size_t limit;
|
||||
std::atomic_bool cancel;
|
||||
bool cancel;
|
||||
std::unique_ptr<form_file> putFile;
|
||||
|
||||
std::thread io_thread;
|
||||
@@ -261,9 +260,9 @@ int Http::priv::xfercb(void *userp, curl_off_t dltotal, curl_off_t dlnow, curl_o
|
||||
self->progressfn(progress, cb_cancel);
|
||||
}
|
||||
|
||||
if (cb_cancel) { self->cancel.store(true); }
|
||||
if (cb_cancel) { self->cancel = true; }
|
||||
|
||||
return self->cancel.load();
|
||||
return self->cancel;
|
||||
}
|
||||
|
||||
int Http::priv::xfercb_legacy(void *userp, double dltotal, double dlnow, double ultotal, double ulnow)
|
||||
@@ -474,7 +473,7 @@ void Http::priv::http_perform()
|
||||
|
||||
if (res != CURLE_OK) {
|
||||
if (res == CURLE_ABORTED_BY_CALLBACK) {
|
||||
if (cancel.load()) {
|
||||
if (cancel) {
|
||||
// The abort comes from the request being cancelled programatically
|
||||
Progress dummyprogress(0, 0, 0, 0, std::string());
|
||||
bool cancel = true;
|
||||
@@ -785,7 +784,7 @@ void Http::perform_sync()
|
||||
|
||||
void Http::cancel()
|
||||
{
|
||||
if (p) { p->cancel.store(true); }
|
||||
if (p) { p->cancel = true; }
|
||||
}
|
||||
|
||||
void Http::print() const
|
||||
|
||||
@@ -1478,8 +1478,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()
|
||||
@@ -1568,10 +1575,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
|
||||
stored = true;
|
||||
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();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -57,7 +57,6 @@ add_executable(${_TEST_NAME}_tests
|
||||
# test_png_io.cpp
|
||||
test_indexed_triangle_set.cpp
|
||||
test_texture_displacement.cpp
|
||||
test_instance_lock.cpp
|
||||
../libnest2d/printer_parts.cpp
|
||||
)
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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 {
|
||||
@@ -5793,6 +5788,131 @@ TEST_CASE("A system preset no vendor lists is not resolved", "[Preset][Bundle]")
|
||||
|
||||
namespace {
|
||||
|
||||
// Writes each vendor's preset cache into dir, then deletes its profile JSONs: what a release build installs.
|
||||
void reduce_vendors_to_caches(const fs::path &dir, const std::vector<std::string> &vendor_ids)
|
||||
{
|
||||
const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY);
|
||||
PresetBundle library;
|
||||
if (fs::exists(dir / (lib + ".json"))) {
|
||||
library.set_generate_vendor_caches(true);
|
||||
library.load_vendor_configs_from_json(dir.string(), lib, PresetBundle::LoadSystem,
|
||||
ForwardCompatibilitySubstitutionRule::EnableSilent);
|
||||
}
|
||||
for (const std::string &vendor_id : vendor_ids) {
|
||||
if (vendor_id == lib)
|
||||
continue;
|
||||
PresetBundle writer;
|
||||
writer.set_generate_vendor_caches(true);
|
||||
writer.load_vendor_configs_from_json(dir.string(), vendor_id, PresetBundle::LoadSystem,
|
||||
ForwardCompatibilitySubstitutionRule::EnableSilent, &library);
|
||||
}
|
||||
for (const std::string &vendor_id : vendor_ids) {
|
||||
REQUIRE(fs::exists(dir / (vendor_id + ".opc")));
|
||||
fs::remove(dir / (vendor_id + ".json"));
|
||||
fs::remove_all(dir / vendor_id);
|
||||
}
|
||||
}
|
||||
|
||||
// The filament library with one abstract base filament, and an "Acme" vendor whose one filament inherits it.
|
||||
void write_library_and_acme_filament(const fs::path &root)
|
||||
{
|
||||
const std::string lib(PresetBundle::ORCA_FILAMENT_LIBRARY);
|
||||
fs::create_directories(root / lib / "filament");
|
||||
std::ofstream((root / (lib + ".json")).string())
|
||||
<< R"({"version":"1.0.0","name":")" << lib << R"(",)"
|
||||
<< R"("filament_list":[{"name":"Generic PLA","sub_path":"filament/generic_pla.json"}]})";
|
||||
std::ofstream((root / lib / "filament" / "generic_pla.json").string())
|
||||
<< R"({"type":"filament","name":"Generic PLA","from":"system","instantiation":"false","filament_id":"GFL99","filament_cost":"27"})";
|
||||
fs::create_directories(root / "Acme" / "filament");
|
||||
std::ofstream((root / "Acme.json").string())
|
||||
<< R"({"version":"1.0.0","name":"Acme","filament_list":[{"name":"Acme PLA","sub_path":"filament/pla.json"}]})";
|
||||
std::ofstream((root / "Acme" / "filament" / "pla.json").string())
|
||||
<< R"({"type":"filament","name":"Acme PLA","from":"system","instantiation":"true","inherits":"Generic PLA"})";
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("A read-only load resolves a user preset against vendors installed as their cache alone", "[Preset][Bundle][Regression]")
|
||||
{
|
||||
ScopedTemporaryDir temp_dir;
|
||||
const fs::path data = temp_dir.path() / "data";
|
||||
const fs::path system = data / PRESET_SYSTEM_DIR;
|
||||
ScopedDataDir scoped_data(data);
|
||||
ScopedResourcesDir scoped_resources(temp_dir.path() / "resources");
|
||||
write_acme_printer_vendor(system, 33.);
|
||||
reduce_vendors_to_caches(system, {"Acme"});
|
||||
|
||||
fs::create_directories(data / PRESET_USER_DIR / DEFAULT_USER_FOLDER_NAME / PRESET_PRINTER_NAME);
|
||||
std::ofstream((data / PRESET_USER_DIR / DEFAULT_USER_FOLDER_NAME / PRESET_PRINTER_NAME / "My Acme.json").string())
|
||||
<< R"({"type":"machine","name":"My Acme","from":"User","version":"2.3.0.0","inherits":"Acme Printer","printable_height":"123"})";
|
||||
|
||||
AppConfig app_config;
|
||||
PresetBundle bundle;
|
||||
std::string errors;
|
||||
bundle.load_presets(app_config, ForwardCompatibilitySubstitutionRule::EnableSilent, PresetBundle::PresetPreferences(),
|
||||
&errors, true);
|
||||
CHECK(errors.empty());
|
||||
const Preset *preset = bundle.printers.find_preset("My Acme");
|
||||
REQUIRE(preset != nullptr);
|
||||
CHECK_THAT(preset->config.opt_float("printable_height"), Catch::Matchers::WithinAbs(123., 1e-6));
|
||||
CHECK_THAT(preset->config.opt_float("extruder_clearance_dist_to_rod"), Catch::Matchers::WithinAbs(33., 1e-6));
|
||||
}
|
||||
|
||||
TEST_CASE("A read-only load writes no preset cache", "[Preset][Bundle][Regression]")
|
||||
{
|
||||
ScopedTemporaryDir temp_dir;
|
||||
const fs::path system = temp_dir.path() / "data" / PRESET_SYSTEM_DIR;
|
||||
ScopedDataDir scoped_data(temp_dir.path() / "data");
|
||||
ScopedResourcesDir scoped_resources(temp_dir.path() / "resources");
|
||||
write_acme_printer_vendor(system, 33.);
|
||||
|
||||
AppConfig app_config;
|
||||
PresetBundle bundle;
|
||||
std::string errors;
|
||||
bundle.load_presets(app_config, ForwardCompatibilitySubstitutionRule::EnableSilent, PresetBundle::PresetPreferences(),
|
||||
&errors, true);
|
||||
CHECK(errors.empty());
|
||||
CHECK(bundle.printers.find_preset("Acme Printer") != nullptr);
|
||||
CHECK_FALSE(fs::exists(system / "Acme.opc"));
|
||||
}
|
||||
|
||||
TEST_CASE("A vendor updated over the air resolves against the library installed as its cache alone", "[Preset][Bundle][Regression]")
|
||||
{
|
||||
ScopedTemporaryDir temp_dir;
|
||||
const fs::path system = temp_dir.path() / "data" / PRESET_SYSTEM_DIR;
|
||||
ScopedDataDir scoped_data(temp_dir.path() / "data");
|
||||
ScopedResourcesDir scoped_resources(temp_dir.path() / "resources");
|
||||
// System presets are found by name through the bundled profiles.
|
||||
write_library_and_acme_filament(temp_dir.path() / "resources" / PRESET_PROFILES_DIR);
|
||||
// The release install, then an update that brings Acme back as JSONs while the library stays a cache.
|
||||
write_library_and_acme_filament(system);
|
||||
reduce_vendors_to_caches(system, {PresetBundle::ORCA_FILAMENT_LIBRARY, "Acme"});
|
||||
write_library_and_acme_filament(temp_dir.path() / "update");
|
||||
fs::copy_file(temp_dir.path() / "update" / "Acme.json", system / "Acme.json");
|
||||
fs::create_directories(system / "Acme" / "filament");
|
||||
fs::copy_file(temp_dir.path() / "update" / "Acme" / "filament" / "pla.json", system / "Acme" / "filament" / "pla.json");
|
||||
|
||||
SECTION("by name") {
|
||||
PresetBundle bundle;
|
||||
DynamicPrintConfig config;
|
||||
std::string error;
|
||||
REQUIRE(bundle.resolve_system_preset(config, Preset::TYPE_FILAMENT, "Acme PLA",
|
||||
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
|
||||
CHECK_THAT(config.opt<ConfigOptionFloats>("filament_cost")->values.front(), Catch::Matchers::WithinAbs(27., 1e-6));
|
||||
}
|
||||
SECTION("by its source file") {
|
||||
PresetBundle bundle;
|
||||
DynamicPrintConfig config;
|
||||
config.option<ConfigOptionString>(BBL_JSON_KEY_INHERITS, true)->value = "Generic PLA";
|
||||
std::string error;
|
||||
REQUIRE(bundle.resolve_preset_config(config, Preset::TYPE_FILAMENT, (system / "Acme" / "filament" / "pla.json").string(),
|
||||
ForwardCompatibilitySubstitutionRule::EnableSilent, error));
|
||||
CHECK_THAT(config.opt<ConfigOptionFloats>("filament_cost")->values.front(), Catch::Matchers::WithinAbs(27., 1e-6));
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// A default preset config for type, built the way PresetBundle builds its default presets.
|
||||
DynamicPrintConfig external_default_config(Preset::Type type)
|
||||
{
|
||||
@@ -5961,214 +6081,3 @@ TEST_CASE("A filament's variant index follows the extruder type and nozzle volum
|
||||
// the filament defines no Bowden High Flow variant
|
||||
CHECK(PresetBundle::get_filament_variant_index(filament, printer, 1, nvtHighFlow) == 0);
|
||||
}
|
||||
|
||||
TEST_CASE("A user preset saved over by another instance while its directory loads is read again under the lock", "[Preset][Bundle][InstanceLock]")
|
||||
{
|
||||
ScopedTemporaryDir temp_dir;
|
||||
ScopedDataDir data_dir_scope(temp_dir.path());
|
||||
RenameTestCollection coll;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
#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]")
|
||||
{
|
||||
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 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,
|
||||
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);
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -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");
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user