Compare commits

..

10 Commits

Author SHA1 Message Date
Hanif Koh
9a219b414a ci: 2 shards instead of 8, from measured cost
The 8-way matrix was sized against a model that over-estimated slice cost by
~10x. Run 33877144840 measured 25.8 min of sweep work in total and a 4.8 min
longest shard against a predicted 52.9, so the sweep fits the 60-minute
timeout with no sharding at all.

2 shards keeps feedback at ~12.5 min instead of ~24, for one extra repeat of
the merge and G-code stages. Eight repeats plus eight AppImage downloads bought
nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSynfTLvSnHBbVkRUm56nY
2026-09-04 21:32:23 +08:00
Hanif Koh
d888cc5702 ci: print the override sweep's stage summaries in the job log
pytest captures stdout on passing tests, so the per-stage summaries the sweep
prints never reached the log and the job summary said "no stage summaries" on
every green run. Combined with continue-on-error, that meant a shard reporting
zero effective options looked identical to a healthy one unless you downloaded
the artifact and read the JSON.

-rA prints them, which matters more now that the effect stage reports a
per-fixture breakdown and a no_baseline bucket for a fixture that would not
slice.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSynfTLvSnHBbVkRUm56nY
2026-09-04 21:19:26 +08:00
Hanif Koh
454967ebd7 ci: shard the CLI override sweep across 8 jobs
The effect stage re-slices every landed option one at a time and cannot batch,
so it is the entire cost of this job. Today it slices a 20mm cube, where most
options change nothing because the fixture has no supports, no second filament
and no geometry to act on; once each option is routed to a fixture that can
actually show its effect, --effect-full is ~6 h on a 4-vCPU runner and blows
the 60-minute timeout.

orca-test-repo's parity/effect_routing.json groups options by the cheapest
capable fixture and balances the groups by measured slice cost. 8 shards puts
the longest at ~53 min. Shards are disjoint and cover every landed option
exactly once, including options with no recorded fixture, which are spread
round-robin so a newly added one is never dropped from every shard.

fail-fast is off so one shard's failure does not cancel the rest, and the
report artifact is named per shard so all 8 upload.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TSynfTLvSnHBbVkRUm56nY
2026-09-04 18:32:38 +08:00
Hanif Koh
3744b14ab8 ci: run the parity suite with 2 GUI workers
--gui-workers 2: two fixtures at once, each on its own display. Measured
~1.5 cores peak / ~1.9 GB on the 4-vCPU / 16 GB runner (3+ contends); each
fixture is fully isolated so this stays correctness-neutral. Roughly halves
the GUI-bound wall time.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XqSEqBGY9RYF96J8aCgxGy
2026-09-03 03:10:27 +08:00
Hanif Koh
76b6d94d5c ci: add CLI override sweep workflow (full effect stage)
Runs orca-test-repo's test_cli_overrides.py --effect-full against the
latest successful Linux AppImage build: every CLI option verified to land
in the merged config and the G-code CONFIG_BLOCK, then re-sliced
individually and classified effective/inert. Headless - no display stack;
reuses parity.yml's artifact discovery/extraction steps and its temporary
push trigger for pre-merge branch testing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XqSEqBGY9RYF96J8aCgxGy
2026-09-02 17:57:14 +08:00
Hanif Koh
8df242ab30 ci: default lane C to flattened presets
Run 33517761842 proved the lanes work on CI and measured the real
unresolved-inherits divergence of upstream builds (leaf-only
--load-settings -> built-in defaults for parent keys; #14718 family):
engine comparisons identical, pipeline at 17% similarity, 126 settings
reverted. Default CI to --cli-presets flat for a clean parity signal
downstream of preset loading; raw stays available as a dispatch input to
measure the inherits behavior deliberately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XqSEqBGY9RYF96J8aCgxGy
2026-09-01 22:21:51 +08:00
Hanif Koh
ecb0f22935 ci: install WebKitGTK 4.1 host runtime for the AppImage
Second of the two orca-slicer-env preflight checks (the first was
libOpenGL.so.0).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XqSEqBGY9RYF96J8aCgxGy
2026-09-01 22:09:23 +08:00
Hanif Koh
55b44d22cc ci: install host GL runtime libs for the AppImage
AppRun refuses to start without libOpenGL.so.0 (bare runners lack
libopengl0/libglu1-mesa; desktops have them, which is why local testing
passed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XqSEqBGY9RYF96J8aCgxGy
2026-09-01 21:57:11 +08:00
Hanif Koh
7347663e89 ci: temporary push trigger for pre-merge parity workflow testing
workflow_dispatch cannot target a workflow that has never existed on the
default branch; a push trigger scoped to this branch runs it from the
pushed ref instead. Also point the suite-ref fallback at the testing
branch while inputs are unavailable on push events. Both marked for
removal when merging.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XqSEqBGY9RYF96J8aCgxGy
2026-09-01 21:45:43 +08:00
Hanif Koh
7da1a188ac ci: add non-gating GUI-vs-CLI parity workflow
Nightly + manual dispatch. Downloads the latest successful build_all Linux
AppImage artifact (runs it via AppRun - the bare binary misses bundled
libs), checks out sources at the artifact's headSha for resources/profiles
(the AppImage ships only packed .opc preset caches, no profile JSONs), and
runs the parity harness from OrcaSlicer/orca-test-repo (parity/). Metrics
only: continue-on-error, report.md as job summary, scorecard uploaded as an
artifact. Dispatch inputs select fixtures, the suite ref, and which
branch's build artifact to slice with.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XqSEqBGY9RYF96J8aCgxGy
2026-09-01 21:44:30 +08:00
10 changed files with 367 additions and 625 deletions

145
.github/workflows/cli_overrides.yml vendored Normal file
View File

@@ -0,0 +1,145 @@
# CLI override sweep (orca-test-repo test_cli_overrides.py) against the
# latest successful Linux AppImage build of this repo. Verifies every CLI
# config option lands in the merged config and the sliced G-code's
# CONFIG_BLOCK, then re-slices each landed option individually to classify
# it effective/inert (--effect-full). Headless CLI only - no display stack.
name: CLI Override Sweep
on:
# temporary, for pre-merge branch testing: workflow_dispatch only works once
# the workflow exists on the default branch, but push events run the
# workflow from the pushed ref. Remove this trigger when merging to main.
push:
branches: ["hanif/parity-harness"]
workflow_dispatch:
inputs:
test_repo_ref:
description: "orca-test-repo ref to run"
required: false
default: "cli-test-suite"
build_branch:
description: "branch whose latest successful build_all artifact to test"
required: false
default: "main"
permissions:
contents: read
actions: read
jobs:
overrides:
runs-on: ubuntu-24.04
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
# The effect stage re-slices every landed option one at a time and
# cannot batch, so it is the whole cost of this job.
# parity/effect_routing.json groups options by the cheapest fixture that
# can show their effect and splits them into balanced, disjoint shards
# covering every landed option exactly once. Measured at ~12.5 min a
# shard; un-sharded the sweep is ~24 min, so this is about feedback time
# rather than fitting the timeout.
shard: [0, 1]
steps:
- name: Find the latest successful Linux build
id: build
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
gh run list --workflow build_all.yml \
--branch "${{ github.event.inputs.build_branch || 'main' }}" \
--status success --limit 1 --json databaseId,headSha \
--jq '"run_id=\(.[0].databaseId)\nhead_sha=\(.[0].headSha)"' \
>> "$GITHUB_OUTPUT"
cat "$GITHUB_OUTPUT"
- name: Check out the test suite
uses: actions/checkout@v5
with:
repository: OrcaSlicer/orca-test-repo
# temporary fallback for push-triggered branch testing (inputs are
# empty on push): restore to 'cli-test-suite' when merging to main
ref: ${{ github.event.inputs.test_repo_ref || 'hanif/parity-harness' }}
path: orca-test-repo
# the AppImage ships only packed .opc preset caches, no profile JSONs,
# so resources/ (and the option-surface source) must come from the
# source tree at the SAME sha the artifact was built from
- name: Check out OrcaSlicer sources at the build's sha
uses: actions/checkout@v5
with:
ref: ${{ steps.build.outputs.head_sha }}
path: slicer
- name: Download and extract the Linux AppImage
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
gh run download "${{ steps.build.outputs.run_id }}" --dir appimage \
--pattern "OrcaSlicer_Linux_ubuntu_2404*"
appimage=$(find appimage -name "*.AppImage" ! -name "*aarch64*" | head -1)
[ -n "$appimage" ] || { echo "no AppImage found"; ls -R appimage; exit 1; }
chmod +x "$appimage"
"$appimage" --appimage-extract > /dev/null
[ -x squashfs-root/AppRun ] || { echo "no AppRun in AppImage"; exit 1; }
echo "ORCA_BIN=$PWD/squashfs-root/AppRun" >> "$GITHUB_ENV"
echo "ORCA_SOURCE=$PWD/slicer" >> "$GITHUB_ENV"
- name: Install the AppImage's host runtime dependencies
run: |
# orca-slicer-env's two preflight gates: OpenGL and WebKitGTK 4.1
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libopengl0 libglu1-mesa libgl1 libegl1 libwebkit2gtk-4.1-0
- name: Install suite dependencies
run: pip install -r orca-test-repo/requirements.txt
- name: Run the override sweep (full effect stage)
id: run
continue-on-error: true
working-directory: orca-test-repo
run: |
# -rA prints the per-stage summaries: pytest captures stdout on
# passing tests, so without it every "grep override sweep" below
# finds nothing and the job summary says "no stage summaries"
python -m pytest test_cli_overrides.py -c pytest.ini -v -rA --effect-full \
--effect-shard ${{ matrix.shard }}/2 \
--orca-bin "$ORCA_BIN" --orca-source "$ORCA_SOURCE" \
2>&1 | tee ../sweep.log
- name: Publish job summary
if: always()
run: |
{
echo "## CLI override sweep - shard ${{ matrix.shard }}/2"
grep -E "\[override sweep" sweep.log || echo "no stage summaries (see log)"
echo
grep -E "^(=+ )?[0-9]+ (passed|failed)" sweep.log | tail -1 || true
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload the override report
if: always()
uses: actions/upload-artifact@v7
with:
name: override-report-${{ github.run_id }}-shard${{ matrix.shard }}
path: |
orca-test-repo/.pytest_cache/override_report.json
sweep.log
if-no-files-found: warn
retention-days: 30
# The sweep step is continue-on-error so the summary and artifact steps
# above still run on a failure - without this the job would then report
# success, and a shard that measured nothing would look identical to a
# healthy one.
- name: Fail the job if the sweep failed
if: steps.run.outcome == 'failure'
run: |
echo "the override sweep failed - see the job summary and the uploaded report" >&2
exit 1

144
.github/workflows/parity.yml vendored Normal file
View File

@@ -0,0 +1,144 @@
# GUI-vs-CLI parity check. Runs the parity harness from
# OrcaSlicer/orca-test-repo (parity/) against the latest successful Linux
# AppImage build of this repo. Metrics-only: the job never gates - its
# product is scorecard.json / report.md, with the count of NEW divergences
# (differences not in the harness's known-differences ledger) as the
# headline number in the job summary.
name: GUI-CLI Parity
on:
# temporary, for pre-merge branch testing: workflow_dispatch only works once
# the workflow exists on the default branch, but push events run the
# workflow from the pushed ref. Remove this trigger when merging to main.
push:
branches: ["hanif/parity-harness"]
schedule:
- cron: "30 18 * * *"
workflow_dispatch:
inputs:
fixtures:
description: "space-separated fixture ids (empty = all)"
required: false
default: ""
test_repo_ref:
description: "orca-test-repo ref to run"
required: false
default: "cli-test-suite"
build_branch:
description: "branch whose latest successful build_all artifact to slice with"
required: false
default: "main"
cli_presets:
description: "lane C preset handling: flat = flatten inherits (clean parity), raw = leaf as-is (measures the CLI's unresolved-inherits behavior, expect large divergence on unfixed builds)"
required: false
default: "flat"
permissions:
contents: read
actions: read
jobs:
parity:
runs-on: ubuntu-24.04
timeout-minutes: 180
steps:
- name: Find the latest successful Linux build
id: build
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
gh run list --workflow build_all.yml \
--branch "${{ github.event.inputs.build_branch || 'main' }}" \
--status success --limit 1 --json databaseId,headSha \
--jq '"run_id=\(.[0].databaseId)\nhead_sha=\(.[0].headSha)"' \
>> "$GITHUB_OUTPUT"
cat "$GITHUB_OUTPUT"
- name: Check out the parity test suite
uses: actions/checkout@v5
with:
repository: OrcaSlicer/orca-test-repo
# temporary fallback for push-triggered branch testing (inputs are
# empty on push): restore to 'cli-test-suite' when merging to main
ref: ${{ github.event.inputs.test_repo_ref || 'hanif/parity-harness' }}
path: orca-test-repo
# the AppImage ships only packed .opc preset caches, no profile JSONs,
# so resources/profiles must come from the source tree at the SAME sha
# the artifact was built from
- name: Check out OrcaSlicer sources at the build's sha
uses: actions/checkout@v5
with:
ref: ${{ steps.build.outputs.head_sha }}
path: slicer
- name: Download and extract the Linux AppImage
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
set -euo pipefail
gh run download "${{ steps.build.outputs.run_id }}" --dir appimage \
--pattern "OrcaSlicer_Linux_ubuntu_2404*"
# prefer the x86_64 artifact (amd64 is historically unsuffixed)
appimage=$(find appimage -name "*.AppImage" ! -name "*aarch64*" | head -1)
[ -n "$appimage" ] || { echo "no AppImage found"; ls -R appimage; exit 1; }
chmod +x "$appimage"
"$appimage" --appimage-extract > /dev/null
# the bare binary misses bundled libs (libavcodec etc.) - use AppRun,
# which sets up the AppImage's library paths and forwards arguments
[ -x squashfs-root/AppRun ] || { echo "no AppRun in AppImage"; exit 1; }
echo "ORCA_BIN=$PWD/squashfs-root/AppRun" >> "$GITHUB_ENV"
echo "ORCA_SLICER_ROOT=$PWD/slicer" >> "$GITHUB_ENV"
- name: Install display tooling for the GUI lanes
run: |
sudo apt-get update
# the AppImage's orca-slicer-env preflight requires host OpenGL and
# WebKitGTK 4.1 runtimes; everything else ships in the AppImage
sudo apt-get install -y --no-install-recommends \
xvfb xdotool imagemagick openbox mesa-utils \
libopengl0 libglu1-mesa libgl1 libegl1 libwebkit2gtk-4.1-0
- name: Run the parity harness
id: run
continue-on-error: true
run: |
set -euo pipefail
fixtures=""
for f in ${{ github.event.inputs.fixtures || '' }}; do
fixtures="$fixtures --fixture $f"
done
# 2 concurrent GUI displays: measured ~1.5 cores peak / ~1.9 GB on
# this 4-vCPU / 16 GB runner (3+ would contend); each fixture is
# fully isolated so this is correctness-neutral
python3 orca-test-repo/parity/run_parity.py \
--slicer-root "$ORCA_SLICER_ROOT" --bin "$ORCA_BIN" \
--cli-presets "${{ github.event.inputs.cli_presets || 'flat' }}" \
--gui-workers 2 \
--out "$PWD/parity-out" $fixtures
- name: Publish job summary
if: always()
run: |
if [ -f parity-out/report.md ]; then
cat parity-out/report.md >> "$GITHUB_STEP_SUMMARY"
else
echo "harness produced no report (see logs)" >> "$GITHUB_STEP_SUMMARY"
fi
- name: Prune bulky per-lane state before upload
if: always()
run: |
rm -rf parity-out/*/seed parity-out/*/datadir-* || true
- name: Upload scorecard and evidence
if: always()
uses: actions/upload-artifact@v7
with:
name: parity-scorecard-${{ github.run_id }}
path: parity-out/
if-no-files-found: warn
retention-days: 30

View File

@@ -415,9 +415,7 @@ img.ModelThumbnail {
border-width: 1px;
border-style: solid;
border-radius: 4px;
border-color: var(--border-color);
background-color: var(--bg-color);
color: var(--fg-color-text);
background-color: inherit;
position: absolute;
left: 50%;
top: 200px;
@@ -425,17 +423,13 @@ img.ModelThumbnail {
}
#NoticeBar {
background-color: var(--main-color);
background-color:#00f0d8;
height: 40px;
line-height: 40px;
color: #fff;
text-align: center;
}
#NoticeBar.notice-error {
background-color: var(--button-bg-alert);
}
#NoticeContent {
padding: 4mm 10mm;
}

View File

@@ -37,34 +37,6 @@ function HandleStudio( pVal )
{
HandleModelList(pVal['response']);
}
else if(strCmd=='check_new_printers_result')
{
let button = document.getElementById("CheckNewPrintersBtn");
if (button) {
button.style.pointerEvents = "auto";
button.style.opacity = "1";
}
let noticeBar = document.getElementById("NoticeBar");
let noticeText = document.getElementById("NoticeText");
let hasError = pVal.hasOwnProperty("error");
noticeBar.classList.toggle("notice-error", hasError);
if (hasError) {
noticeBar.textContent = "Error";
noticeText.textContent = pVal["error"];
} else if (pVal["vendors"] && pVal["vendors"].length > 0) {
noticeBar.textContent = "New printers found";
noticeText.textContent = "New printer vendors installed: " + pVal["vendors"].join(", ");
} else if (pVal["declined"]) {
noticeBar.textContent = "Information";
noticeText.textContent = "New printer vendors were found, but installation was cancelled.";
} else {
noticeBar.textContent = "Information";
noticeText.textContent = "No new printers found.";
}
ShowNotice(1);
}
}
function HandleModelList( pVal )
@@ -106,13 +78,9 @@ function HandleModelList( pVal )
}
//Update Nozzel Html Append
// ORCA: HandleModelList can now be called more than once per dialog (e.g. after a new vendor
// is installed via "check for new printers"). pModel always holds the full, current list, so
// clear each vendor's printer area before repopulating instead of appending on top of a
// previous render.
for( let key in ModelHtml )
{
$(".OneVendorBlock[vendor='"+key+"'] .PrinterArea").empty().append( ModelHtml[key] );
$(".OneVendorBlock[vendor='"+key+"'] .PrinterArea").append( ModelHtml[key] );
}
//Update Checkbox
@@ -375,9 +343,6 @@ function OnExit()
let nTotal=ModelSelect.length;
if( nTotal==0 ) {
let noticeBar = document.getElementById("NoticeBar");
noticeBar.classList.add("notice-error");
noticeBar.textContent = "Error";
ShowNotice(1);
return 0;
}

View File

@@ -36,22 +36,6 @@ function ConfirmSelect()
}
}
function CheckForNewPrinters()
{
var tSend={};
tSend['sequence_id']=Math.round(new Date() / 1000);
tSend['command']="check_for_new_printers";
tSend['data']={};
var button = document.getElementById("CheckNewPrintersBtn");
if (button) {
button.style.pointerEvents = "none";
button.style.opacity = "0.6";
}
SendWXMessage( JSON.stringify(tSend) );
}
function CreateNewPrinter()
{
var tSend={};

View File

@@ -82,8 +82,7 @@
</div>
<div id="AcceptArea">
<div class="ButtonStyleRegular ButtonTypeChoice trans" id="CreateBtn" onclick="CreateNewPrinter()">Create</div>
<div class="ButtonStyleRegular ButtonTypeChoice" id="CheckNewPrintersBtn" onclick="CheckForNewPrinters()">Check for new printers</div>
<div class="ButtonStyleConfirm ButtonTypeChoice trans" tid="t39" id="AcceptBtn" onclick="ConfirmSelect()">Confirm</div>
<div class="ButtonStyleConfirm ButtonTypeChoice trans" tid="t39" id="AcceptBtn" onclick="ConfirmSelect()">Confirm</div>
<div class="ButtonStyleRegular ButtonTypeChoice trans" tid="t38" id="PreBtn" onclick="CancelSelect()">Cancel</div>
</div>

View File

@@ -42,9 +42,6 @@ namespace Slic3r {
static const std::string VERSION_CHECK_URL = "https://check-version.orcaslicer.com/latest";
static const std::string PROFILE_UPDATE_URL = "https://check-version.orcaslicer.com/profile";
constexpr const char* CONFIG_ORCA_UPDATER_URL = "orca_updater_url";
static const std::string MODELS_STR = "models";
const std::string AppConfig::SECTION_FILAMENTS = "filaments";
@@ -638,11 +635,6 @@ void AppConfig::set_defaults()
set_bool("use_printer_agents", false);
}
if (get("enable_ota").empty())
{
set_bool("enable_ota", false);
}
// Remove legacy window positions/sizes
erase("app", "main_frame_maximized");
erase("app", "main_frame_pos");
@@ -1822,10 +1814,7 @@ std::string AppConfig::version_check_url() const
std::string AppConfig::profile_update_url() const
{
std::string orca_updater_url = get(CONFIG_ORCA_UPDATER_URL);
if (orca_updater_url.empty())
return PROFILE_UPDATE_URL;
return orca_updater_url;
return PROFILE_UPDATE_URL;
}
bool AppConfig::exists()

View File

@@ -23,7 +23,6 @@
#include <wx/textdlg.h>
#include <wx/wx.h>
#include <wx/weakref.h>
#include <wx/display.h>
#include <wx/fileconf.h>
#include <wx/file.h>
@@ -563,78 +562,6 @@ void GuideFrame::OnScriptMessage(wxWebViewEvent &evt)
m_ProfileJson["filament"][fName]["selected"] = 1;
}
}
else if (strCmd == "check_for_new_printers") {
json response = json::object();
response["command"] = "check_new_printers_result";
// Guide pages currently send sequence_id as a number, while older
// pages may send it as a string. Preserve the value without
// forcing either representation.
if (j.contains("sequence_id"))
response["sequence_id"] = j["sequence_id"];
else
response["sequence_id"] = "";
if (!m_MainPtr->preset_updater) {
response["error"] = "Printer update service is unavailable.";
wxString strJS = wxString::Format("HandleStudio(%s)", response.dump(-1, ' ', true));
wxGetApp().CallAfter([this, strJS] { RunScript(strJS); });
} else {
// Orca: enumerate vendors directly from disk rather than from m_ProfileJson["model"]
// — a vendor with no machine models (e.g. a filament-only bundle, or a test fixture
// like "test123" with an empty machine_model_list) never gets a "vendor" entry
// pushed into "model" by LoadProfileFamily(), so it would be invisible to the
// request body and get endlessly re-offered by the server. Scan both the system dir
// (already-installed vendors) and the bundled resources dir (shipped-but-not-yet-
// installed vendors), same as LoadProfileData() does when building loaded_vendors.
std::set<std::string> system_vendors;
for (const auto& dir : {vendor_dir, rsrc_vendor_dir}) {
if (!boost::filesystem::exists(dir))
continue;
for (const auto& entry : boost::filesystem::directory_iterator(dir)) {
if (!boost::filesystem::is_directory(entry) && boost::iequals(entry.path().extension().string(), ".json"))
system_vendors.insert(entry.path().stem().string());
}
}
// Orca: check_new_vendors() is async (network + confirmation dialog + download
// all happen off the calling thread apart from the dialog itself); guard against
// this dialog being closed before the callback fires.
wxWeakRef<GuideFrame> weak_this(this);
try {
m_MainPtr->preset_updater->check_new_vendors(
system_vendors, [weak_this, response](std::vector<std::string> installed_vendors, bool declined) mutable {
if (!weak_this)
return;
// Orca: append the newly installed vendor(s) into the in-memory
// profile data (instead of a full LoadProfileData() rescan of every
// vendor) and push the refreshed list to the webview, the same way
// request_userguide_profile does, so the printer list picks them up
// without needing to reopen the guide.
for (const auto& vendor_id : installed_vendors) {
weak_this->LoadProfileFamily(vendor_id, (weak_this->vendor_dir / (vendor_id + ".json")).string());
}
if (!installed_vendors.empty()) {
json profile_response = json::object();
profile_response["command"] = "response_userguide_profile";
profile_response["sequence_id"] = "10001";
profile_response["response"] = weak_this->m_ProfileJson;
wxString profileJS = wxString::Format("HandleStudio(%s)", profile_response.dump(-1, ' ', true));
weak_this->RunScript(profileJS);
}
response["vendors"] = installed_vendors;
response["declined"] = declined;
wxString strJS = wxString::Format("HandleStudio(%s)", response.dump(-1, ' ', true));
weak_this->RunScript(strJS);
});
} catch (const std::exception &e) {
BOOST_LOG_TRIVIAL(warning) << "Failed to check for new printers: " << e.what();
response["error"] = "Failed to check for new printers.";
wxString strJS = wxString::Format("HandleStudio(%s)", response.dump(-1, ' ', true));
wxGetApp().CallAfter([this, strJS] { RunScript(strJS); });
}
}
}
else if (strCmd == "user_guide_finish") {
SaveProfile();
@@ -1717,15 +1644,13 @@ int GuideFrame::LoadProfileFamily(std::string strVendor, std::string strFilePath
OneModel["materials"] = pm["default_materials"];
// wxString strCoverPath = wxString::Format("%s\\%s\\%s_cover.png", strFolder, strVendor, std::string(s1.mb_str()));
std::string cover_file = s1 + "_cover.png";
boost::filesystem::path cover_path = boost::filesystem::absolute(vendor_dir / cover_file).make_preferred();
BOOST_LOG_TRIVIAL(info) << "[WebGuideDialog] " << cover_path;
std::string cover_file = s1 + "_cover.png";
boost::filesystem::path cover_path = boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/profiles/" / strVendor / cover_file).make_preferred();
if (!boost::filesystem::exists(cover_path)) {
cover_path = boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/profiles/" / strVendor / cover_file)
.make_preferred();
if (!boost::filesystem::exists(cover_path))
cover_path = (boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/web/image/printer/") / cover_file)
.make_preferred();
cover_path =
(boost::filesystem::absolute(boost::filesystem::path(resources_dir()) / "/web/image/printer/") /
cover_file)
.make_preferred();
}
OneModel["cover"] = cover_path.string();

View File

@@ -10,7 +10,6 @@
#include <set>
#include <string>
#include <thread>
#include <mutex>
#include <unordered_map>
#include <ostream>
#include <utility>
@@ -30,7 +29,6 @@
#include "libslic3r/format.hpp"
#include "libslic3r/Utils.hpp"
#include "libslic3r/PresetBundle.hpp"
#include "libslic3r/PresetCacheFormat.hpp"
#include "libslic3r_version.h"
#include "slic3r/GUI/GUI.hpp"
#include "slic3r/GUI/GUI_App.hpp"
@@ -98,8 +96,6 @@ struct Update
bool forced_update;
//BBS: add directory support
bool is_directory {false};
// Orca: a vendor update may be the cache-only form.
bool is_opc {false};
Update() {}
//BBS: add directory support
@@ -130,24 +126,13 @@ struct Update
//BBS: add directory support
void install() const
{
if (is_directory) {
if (is_directory) {
copy_directory_recursively(source, target, file_filter);
} else {
copy_file_fix(source, target);
// A vendor must be installed in exactly one form. Remove the
// representation that would otherwise be stale or shadow this one.
boost::system::error_code ec;
if (is_opc) {
fs::remove(target.parent_path() / (vendor + ".json"), ec);
ec.clear();
fs::remove_all(target.parent_path() / vendor, ec);
}
else {
fs::remove(target.parent_path() / (vendor + ".opc"), ec);
}
}
}
else {
copy_file_fix(source, target);
}
}
friend std::ostream& operator<<(std::ostream& os, const Update &self)
{
@@ -195,8 +180,6 @@ struct Updates
std::vector<Update> updates;
};
static bool reload_configs_update_gui();
wxDEFINE_EVENT(EVT_SLIC3R_VERSION_ONLINE, wxCommandEvent);
wxDEFINE_EVENT(EVT_SLIC3R_EXPERIMENTAL_VERSION_ONLINE, wxCommandEvent);
@@ -224,10 +207,6 @@ struct PresetUpdater::priv
// Per-vendor update checking
std::set<std::string> checked_vendors;
// Orca (PR #130): changelog text for each vendor, captured in memory during
// sync_vendor_config()/check_new_vendors() instead of written beside the cache.
std::unordered_map<std::string, std::string> vendor_changelogs;
mutable std::mutex vendor_changelogs_mutex;
std::vector<std::thread> vendor_check_threads;
std::atomic<bool> vendor_check_cancel{false};
@@ -252,8 +231,6 @@ struct PresetUpdater::priv
void parse_version_string(const std::string& body) const;
void sync_resources(std::string http_url, std::map<std::string, Resource> &resources, bool check_patch = false, std::string current_version="", std::string changelog_file="");
void sync_vendor_config(const std::string& vendor_id);
void check_new_vendors(const std::set<std::string>& system_vendors,
std::function<void(std::vector<std::string>, bool)> callback);
void sync_tooltip(std::string http_url, std::string language);
void sync_plugins(std::string http_url, std::string plugin_version);
void sync_printer_config(std::string http_url);
@@ -291,7 +268,7 @@ void PresetUpdater::priv::set_download_prefs(AppConfig *app_config)
version_check_url = app_config->version_check_url();
auto profile_update_url = app_config->profile_update_url();
if (!profile_update_url.empty() && app_config->get_bool("enable_ota"))
if (!profile_update_url.empty())
enabled_config_update = true;
else
enabled_config_update = false;
@@ -694,9 +671,6 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id)
std::string online_version_str; // this represents the PROFILE VERSION, not ORCA VERSION
std::string download_url_str;
std::string changelog;
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] fetching vendor update status from " << url;
Http::get(url)
.timeout_connect(5)
@@ -709,12 +683,9 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id)
if (http_status != 200) return;
try {
json j = json::parse(body);
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] url: " << url << " returned:" << body;
if (j.contains("vendor_version") && j.contains("download_url")) {
online_version_str = j["vendor_version"].get<std::string>();
download_url_str = j["download_url"].get<std::string>();
changelog = j.value("changelog", std::string());
}
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] vendor check JSON parse failed: " << e.what();
@@ -736,10 +707,6 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id)
boost::system::error_code ec;
fs::remove_all(cache_profile_path / vendor_id, ec);
fs::remove(cache_profile_path / (vendor_id + ".json"), ec);
// Orca: the OPC cache is the vendor's whole installation in one file; clear it too.
fs::remove(cache_profile_path / (vendor_id + ".opc"), ec);
// Best-effort cleanup of the legacy on-disk changelog written by older builds
// (changelogs are now kept in memory - see vendor_changelogs).
fs::remove(cache_profile_path / (vendor_id + ".changelog"), ec);
// Download the zip
@@ -768,7 +735,7 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id)
if (!download_ok || cancel || vendor_check_cancel) return;
// Extract vendor profile bundles under ota/profiles. The downloaded zip contains
// either the vendor json/folder or the vendor cache at its root.
// the vendor json/folder at its root.
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] extracting update for " << vendor_id;
if (!extract_file(download_file, cache_profile_path)) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] extraction failed for " << vendor_id;
@@ -778,27 +745,6 @@ void PresetUpdater::priv::sync_vendor_config(const std::string& vendor_id)
if (cancel || vendor_check_cancel) return;
const fs::path cached_vendor_json = cache_profile_path / (vendor_id + ".json");
const fs::path cached_vendor_folder = cache_profile_path / vendor_id;
const fs::path cached_vendor_opc = cache_profile_path / (vendor_id + ".opc");
bool is_json_update = fs::is_regular_file(cached_vendor_json) && fs::is_directory(cached_vendor_folder) && !fs::is_empty(cached_vendor_folder);
bool is_opc_update = fs::is_regular_file(cached_vendor_opc);
if (!is_json_update && !is_opc_update) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] rejected update for " << vendor_id
<< ": expected " << vendor_id << ".json and a non-empty "
<< vendor_id << " directory, or OPC update format.";
fs::remove_all(cached_vendor_folder, ec);
fs::remove(cached_vendor_json, ec);
return;
}
{
std::lock_guard<std::mutex> lock(vendor_changelogs_mutex);
vendor_changelogs[vendor_id] = std::move(changelog);
}
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] vendor " << vendor_id << " update cached, notifying UI";
GUI::wxGetApp().CallAfter([] {
GUI::wxGetApp().check_config_updates_from_updater();
@@ -1093,7 +1039,6 @@ void PresetUpdater::priv::check_installed_vendor_profiles() const
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:Checking whether the profile from resource is newer";
AppConfig *app_config = GUI::wxGetApp().app_config;
const auto enabled_vendors = app_config->vendors();
std::set<std::string> bundles;
@@ -1202,110 +1147,68 @@ Updates PresetUpdater::priv::get_config_updates(const Semver &old_slic3r_version
if (!fs::exists(cache_profile_path))
return updates;
// Orca (PR #130): vendor changelogs are captured in memory during
// sync_vendor_config()/check_new_vendors(), not written beside the cache.
std::unordered_map<std::string, std::string> changelogs;
{
std::lock_guard<std::mutex> lock(vendor_changelogs_mutex);
changelogs = vendor_changelogs;
}
for (auto &dir_entry : boost::filesystem::directory_iterator(cache_profile_path)) {
const auto &path = dir_entry.path();
std::string file_path = path.string();
if (is_json_file(file_path)) {
const auto path_in_vendor = vendor_path / path.filename();
std::string vendor_name = path.filename().string();
// Remove the .json suffix.
vendor_name.erase(vendor_name.size() - 5);
auto print_in_cache = (cache_profile_path / vendor_name / PRESET_PRINT_NAME);
auto filament_in_cache = (cache_profile_path / vendor_name / PRESET_FILAMENT_NAME);
auto machine_in_cache = (cache_profile_path / vendor_name / PRESET_PRINTER_NAME);
for (auto &dir_entry : boost::filesystem::directory_iterator(cache_profile_path)) {
const auto &path = dir_entry.path();
std::string file_path = path.string();
const bool is_opc_file = boost::iequals(path.extension().string(), ".opc");
if (!is_json_file(file_path) && !is_opc_file)
continue;
if (is_vendor_installed(vendor_name)
|| fs::exists(print_in_cache)
|| fs::exists(filament_in_cache)
|| fs::exists(machine_in_cache)) {
// Orca: a vendor installed as a preset cache carries its version there.
Semver vendor_ver = installed_vendor_version(vendor_name);
const std::string vendor_name = path.stem().string();
auto print_in_cache = (cache_profile_path / vendor_name / PRESET_PRINT_NAME);
auto filament_in_cache = (cache_profile_path / vendor_name / PRESET_FILAMENT_NAME);
auto machine_in_cache = (cache_profile_path / vendor_name / PRESET_PRINTER_NAME);
std::map<std::string, std::string> key_values;
std::vector<std::string> keys(3);
Semver cache_ver;
keys[0] = BBL_JSON_KEY_VERSION;
keys[1] = BBL_JSON_KEY_DESCRIPTION;
keys[2] = BBL_JSON_KEY_FORCE_UPDATE;
get_values_from_json(file_path, keys, key_values);
std::string description = key_values[BBL_JSON_KEY_DESCRIPTION];
bool force_update = false;
if (key_values.find(BBL_JSON_KEY_FORCE_UPDATE) != key_values.end())
force_update = (key_values[BBL_JSON_KEY_FORCE_UPDATE] == "1")?true:false;
auto config_version = Semver::parse(key_values[BBL_JSON_KEY_VERSION]);
if (config_version)
cache_ver = *config_version;
// Orca (PR #130): a JSON cache entry is only meaningful next to a non-empty
// <vendor>/ preset directory; a stray or half-downloaded <vendor>.json is
// skipped. An .opc cache is a single self-contained file (validated below),
// so this check does not apply to it.
if (!is_opc_file) {
const auto vendor_folder_in_cache = cache_profile_path / vendor_name;
if (!fs::is_regular_file(path) || !fs::is_directory(vendor_folder_in_cache) ||
fs::is_empty(vendor_folder_in_cache)) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]:ignoring invalid cached update for "
<< vendor_name << ": expected " << vendor_name
<< ".json and a non-empty " << vendor_name << " directory";
continue;
}
}
std::string changelog;
std::string changelog_file = (cache_profile_path / (vendor_name + ".changelog")).string();
boost::nowide::ifstream ifs(changelog_file);
if (ifs) {
std::ostringstream oss;
oss<< ifs.rdbuf();
changelog = oss.str();
ifs.close();
}
if (is_vendor_installed(vendor_name)
|| is_opc_file
|| fs::exists(print_in_cache)
|| fs::exists(filament_in_cache)
|| fs::exists(machine_in_cache)) {
// Orca: a vendor installed as a preset cache carries its version there.
Semver vendor_ver = installed_vendor_version(vendor_name);
Semver cache_ver;
std::string description;
bool force_update = false;
if (is_opc_file) {
cache_ver = VendorCacheFile::usable_version(file_path, vendor_name);
if (!cache_ver.valid()) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater]:ignoring unreadable vendor cache " << file_path;
continue;
}
}
else {
std::map<std::string, std::string> key_values;
std::vector<std::string> keys(3);
keys[0] = BBL_JSON_KEY_VERSION;
keys[1] = BBL_JSON_KEY_DESCRIPTION;
keys[2] = BBL_JSON_KEY_FORCE_UPDATE;
get_values_from_json(file_path, keys, key_values);
description = key_values[BBL_JSON_KEY_DESCRIPTION];
if (key_values.find(BBL_JSON_KEY_FORCE_UPDATE) != key_values.end())
force_update = (key_values[BBL_JSON_KEY_FORCE_UPDATE] == "1")?true:false;
auto config_version = Semver::parse(key_values[BBL_JSON_KEY_VERSION]);
if (config_version)
cache_ver = *config_version;
}
// Orca (PR #130): changelog for this vendor was captured in memory at sync time.
const auto changelog_it = changelogs.find(vendor_name);
std::string changelog = changelog_it != changelogs.end() ? changelog_it->second : std::string();
if (vendor_ver < cache_ver) {
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:need to update settings from " << vendor_ver.to_string()
<< " to newer version " << cache_ver.to_string() << ", app version " << SLIC3R_VERSION;
Version version;
version.config_version = cache_ver;
version.comment = description;
if (is_opc_file) {
// A cache contains the vendor profile and all presets.
// Install it directly; Update::install removes any
// superseded JSON representation.
auto &update = updates.updates.emplace_back(
std::move(file_path), vendor_path / (vendor_name + ".opc"),
std::move(version), vendor_name, changelog, "", force_update, false);
update.is_opc = true;
}
else {
// JSON profile and its preset directory are installed
// separately, as before.
updates.updates.emplace_back(std::move(file_path),
vendor_path / (vendor_name + ".json"), std::move(version),
vendor_name, changelog, "", force_update, false);
updates.updates.emplace_back(cache_profile_path / vendor_name,
vendor_path / vendor_name, Version(), vendor_name,
"", "", force_update, true);
}
} else {
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:cached settings for " << vendor_name
<< " are not newer than installed version, installed " << vendor_ver.to_string()
<< ", cached " << cache_ver.to_string();
}
}
}
if (vendor_ver < cache_ver) {
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:need to update settings from " << vendor_ver.to_string()
<< " to newer version " << cache_ver.to_string() << ", app version " << SLIC3R_VERSION;
Version version;
version.config_version = cache_ver;
version.comment = description;
// Orca: update vendor.json
updates.updates.emplace_back(std::move(file_path), path_in_vendor.string(), std::move(version), vendor_name, changelog, "", force_update, false);
//Orca: update vendor folder
updates.updates.emplace_back(cache_profile_path / vendor_name, vendor_path / vendor_name, Version(), vendor_name, "", "", force_update, true);
} else {
BOOST_LOG_TRIVIAL(info) << "[Orca Updater]:cached settings for " << vendor_name
<< " are not newer than installed version, installed " << vendor_ver.to_string()
<< ", cached " << cache_ver.to_string();
}
}
}
}
return updates;
}
@@ -1414,9 +1317,6 @@ void PresetUpdater::sync(std::string http_url, std::string language, std::string
// after the startup printer preset has been restored.
this->p->sync_plugins(http_url, plugin_version);
this->p->sync_printer_config(http_url);
// Orca (PR #130): the filament library is always installed, so refresh it
// from the updater on every startup sync rather than deferring to check_vendor_update().
this->p->sync_vendor_config(PresetBundle::ORCA_FILAMENT_LIBRARY);
//if (p->cancel)
// return;
//remove the tooltip currently
@@ -1449,302 +1349,6 @@ void PresetUpdater::check_vendor_update(const std::string& vendor_id)
});
}
// Orca: ask the server which vendors from `system_vendors` have a profile bundle available that
// isn't installed yet (or is newer than what's installed). Request body maps vendor id -> currently
// installed profile version (unknown/not-yet-installed vendors report "0.0.0"). Response maps
// vendor id -> {version, download_url, changelog} for each vendor the server has an update for.
// Any such vendor is downloaded and cached under ota/profiles the same way sync_vendor_config()
// does; the caller's check_config_updates_from_updater() -> get_config_updates()/perform_updates()
// flow then installs the cached profiles into data_dir()/system.
//
// Mirrors check_vendor_update()/sync_vendor_config(): the network query and the download/extract
// work run on a background thread (vendor_check_threads), never on the calling (UI) thread. Only
// the confirmation dialog (which must run on the UI thread) and the final callback are marshaled
// back via CallAfter().
void PresetUpdater::priv::check_new_vendors(const std::set<std::string>& system_vendors,
std::function<void(std::vector<std::string>, bool)> callback)
{
vendor_check_threads.emplace_back([this, system_vendors, callback]() {
AppConfig* app_config = GUI::wxGetApp().app_config;
std::string url = app_config->profile_update_url() + "/new?orcaslicer_version=" + Http::url_encode(SoftFever_VERSION);
auto check_cancel = [this](Http::Progress, bool& cancel_http) {
if (cancel || vendor_check_cancel)
cancel_http = true;
};
json request_body = json::object();
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] checking new vendors for:";
for (const auto& vendor_id : system_vendors) {
// Orca: installed_vendor_version() reads whichever form the vendor is
// installed as - the .json profile or the .opc preset cache stamp -
// so a cache-only vendor is not reported as version 0.0.0 and then
// endlessly re-offered by the server.
Semver installed_ver = installed_vendor_version(vendor_id);
request_body[vendor_id] = installed_ver.to_string();
BOOST_LOG_TRIVIAL(info) << vendor_id << " (installed version " << installed_ver.to_string() << ")";
}
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] new vendor check request url: " << url;
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] new vendor check request body: " << request_body.dump(2);
json response_json;
bool got_response = false;
auto post = Http::post(url);
post.timeout_connect(5);
post.on_progress(check_cancel);
post.header("Content-Type", "application/json");
post.on_error([](std::string body, std::string error, unsigned http_status) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] new vendor check HTTP error: " << error;
})
.on_complete([&response_json, &got_response](std::string body, unsigned http_status) {
if (http_status != 200)
return;
try {
json j = json::parse(body);
if (j.is_object()) {
response_json = std::move(j);
got_response = true;
}
} catch (const std::exception& e) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] new vendor check JSON parse failed: " << e.what();
}
});
post.set_post_body(request_body.dump());
post.perform_sync();
if (cancel || vendor_check_cancel)
return;
if (!got_response) {
GUI::wxGetApp().CallAfter([callback]() { callback({}, false); });
return;
}
// Collect candidates before touching the filesystem or network again.
struct NewVendorCandidate
{
std::string vendor_id;
Semver version;
std::string download_url;
std::string changelog;
};
std::vector<NewVendorCandidate> candidates;
for (auto it = response_json.begin(); it != response_json.end(); ++it) {
const json& entry = it.value();
if (!entry.is_object())
continue;
std::string download_url_str = entry.value("download_url", std::string());
if (download_url_str.empty())
continue;
NewVendorCandidate candidate;
candidate.vendor_id = it.key();
auto parsed_ver = Semver::parse(entry.value("version", std::string()));
candidate.version = parsed_ver ? *parsed_ver : Semver();
candidate.download_url = std::move(download_url_str);
candidate.changelog = entry.value("changelog", std::string());
candidates.push_back(std::move(candidate));
}
if (candidates.empty()) {
GUI::wxGetApp().CallAfter([callback]() { callback({}, false); });
return;
}
// Orca: the confirmation dialog must run on the UI thread; if confirmed, the actual
// download/install work is dispatched back onto a new background thread from there,
// same as check_vendor_update() does for a single vendor.
GUI::wxGetApp().CallAfter([this, candidates, callback]() {
std::vector<GUI::MsgUpdateConfig::Update> updates_msg;
for (const auto& candidate : candidates)
updates_msg.emplace_back(candidate.vendor_id, candidate.version, std::string(), candidate.changelog);
GUI::MsgUpdateConfig dlg(updates_msg);
if (dlg.ShowModal() != wxID_OK) {
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] user declined installing new vendors";
callback({}, true);
return;
}
// Orca: the actual download runs on a background thread below (so it doesn't block the
// UI), but that also means nothing visibly happens for the several seconds it can take
// (longer still if a retry kicks in) — push a notification so it's clear work is
// ongoing rather than looking hung.
{
std::string vendor_list;
for (const auto& candidate : candidates) {
if (!vendor_list.empty())
vendor_list += ", ";
vendor_list += candidate.vendor_id;
}
GUI::wxGetApp().plater()->get_notification_manager()->push_notification(
_u8L("Downloading new vendor profile(s): ") + vendor_list + _u8L("..."));
}
vendor_check_threads.emplace_back([this, candidates, callback]() {
auto check_cancel = [this](Http::Progress, bool& cancel_http) {
if (cancel || vendor_check_cancel)
cancel_http = true;
};
std::vector<std::string> new_vendor_ids;
std::vector<std::string> failed_vendor_ids;
auto cache_profile_path = cache_path / "profiles";
fs::create_directories(cache_profile_path);
boost::system::error_code ec;
for (const auto& candidate : candidates) {
if (cancel || vendor_check_cancel)
break;
const std::string& vendor_id = candidate.vendor_id;
const std::string& download_url_str = candidate.download_url;
std::string changelog = candidate.changelog;
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] downloading new vendor " << vendor_id << " version " << candidate.version.to_string();
// Clear only this vendor's cached data, same as sync_vendor_config().
fs::remove_all(cache_profile_path / vendor_id, ec);
fs::remove(cache_profile_path / (vendor_id + ".json"), ec);
fs::path download_file = cache_path / (vendor_id + TMP_EXTENSION);
bool download_ok = false;
// Orca: same retry pattern as Plater.cpp's project download — a single-shot
// 5s connect timeout against GitHub's redirect chain is prone to transient
// failures (DNS/connect hiccups) that succeed a moment later, so retry a few
// times before giving up rather than failing the whole vendor on one blip.
int retry_count = 0;
const int max_retries = 3;
bool keep_trying = true;
while (keep_trying && retry_count < max_retries) {
retry_count++;
Http::get(download_url_str)
.timeout_connect(5)
.on_progress(check_cancel)
.on_error([&vendor_id, &retry_count, max_retries](std::string body, std::string error, unsigned http_status) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] download failed for new vendor " << vendor_id
<< " (attempt " << retry_count << "/" << max_retries << "): " << error;
})
.on_complete([&](std::string body, unsigned http_status) {
if (http_status != 200)
return;
fs::fstream file(download_file, std::ios::out | std::ios::binary | std::ios::trunc);
if (!file.good())
return;
file.write(body.c_str(), body.size());
file.close();
if (file.good())
download_ok = true;
})
.perform_sync();
keep_trying = !download_ok && !(cancel || vendor_check_cancel);
}
if (!download_ok || cancel || vendor_check_cancel) {
if (!download_ok)
failed_vendor_ids.push_back(vendor_id);
continue;
}
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] extracting new vendor " << vendor_id;
if (!extract_file(download_file, cache_profile_path)) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] extraction failed for new vendor " << vendor_id;
fs::remove(download_file, ec);
failed_vendor_ids.push_back(vendor_id);
continue;
}
fs::remove(download_file, ec);
const fs::path cached_vendor_json = cache_profile_path / (vendor_id + ".json");
const fs::path cached_vendor_folder = cache_profile_path / vendor_id;
if (!fs::is_regular_file(cached_vendor_json) || !fs::is_directory(cached_vendor_folder) ||
fs::is_empty(cached_vendor_folder)) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] rejected new vendor " << vendor_id << ": expected " << vendor_id
<< ".json and a non-empty " << vendor_id << " directory";
fs::remove_all(cached_vendor_folder, ec);
fs::remove(cached_vendor_json, ec);
failed_vendor_ids.push_back(vendor_id);
continue;
}
{
std::lock_guard<std::mutex> lock(vendor_changelogs_mutex);
vendor_changelogs[vendor_id] = std::move(changelog);
}
new_vendor_ids.push_back(vendor_id);
}
if (!new_vendor_ids.empty()) {
// Orca: the user already confirmed via the dialog above, so install right away
// instead of routing through check_config_updates_from_updater(), which only
// queues a passive notification (meant for the silent background per-vendor
// check) requiring yet another click + confirmation before anything is copied
// into data_dir()/system.
GUI::wxGetApp().CallAfter([this, new_vendor_ids] {
AppConfig* app_config = GUI::wxGetApp().app_config;
Updates updates = get_config_updates(app_config->orig_version());
// Only install the vendors just confirmed; leave any other unrelated
// pending cached update (from a background sync_vendor_config()) alone,
// still gated behind its own notification/confirmation.
std::set<std::string> confirmed(new_vendor_ids.begin(), new_vendor_ids.end());
Updates filtered;
for (auto& update : updates.updates)
if (confirmed.count(update.vendor))
filtered.updates.push_back(std::move(update));
if (filtered.updates.empty()) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] new vendors cached but no updates detected";
return;
}
if (!perform_updates(std::move(filtered)) || !reload_configs_update_gui()) {
BOOST_LOG_TRIVIAL(warning) << "[Orca Updater] failed to install new vendors";
return;
}
BOOST_LOG_TRIVIAL(info) << "[Orca Updater] new vendors installed";
for (const auto& vendor_id : new_vendor_ids) {
Semver cur_ver = GUI::wxGetApp().preset_bundle->get_vendor_profile_version(vendor_id);
GUI::wxGetApp().plater()->get_notification_manager()->push_notification(
GUI::NotificationType::PresetUpdateFinished,
GUI::NotificationManager::NotificationLevel::ImportantNotificationLevel,
_u8L("Configuration package: ") + vendor_id + _u8L(" updated to ") + cur_ver.to_string());
}
});
}
if (!failed_vendor_ids.empty()) {
GUI::wxGetApp().CallAfter([failed_vendor_ids] {
std::string vendor_list;
for (const auto& vendor_id : failed_vendor_ids) {
if (!vendor_list.empty())
vendor_list += ", ";
vendor_list += vendor_id;
}
GUI::wxGetApp().plater()->get_notification_manager()->push_notification(
_u8L("Failed to download vendor profile(s): ") + vendor_list);
});
}
GUI::wxGetApp().CallAfter([callback, new_vendor_ids]() { callback(new_vendor_ids, false); });
});
});
});
}
void PresetUpdater::check_new_vendors(const std::set<std::string>& system_vendors,
std::function<void(std::vector<std::string>, bool)> callback)
{
p->check_new_vendors(system_vendors, std::move(callback));
}
void PresetUpdater::slic3r_update_notify()
{
if (! p->enabled_version_check)
@@ -1766,9 +1370,10 @@ static bool reload_configs_update_gui()
GUI::wxGetApp().load_current_presets();
GUI::wxGetApp().plater()->set_bed_shape();
return true;
return true;
}
PresetUpdater::UpdateResult PresetUpdater::config_update(const Semver& old_slic3r_version, UpdateParams params) const
{
if (! p->enabled_config_update) { return R_NOOP; }

View File

@@ -1,9 +1,7 @@
#ifndef slic3r_PresetUpdate_hpp_
#define slic3r_PresetUpdate_hpp_
#include <functional>
#include <memory>
#include <set>
#include <vector>
#include <wx/event.h>
@@ -61,12 +59,6 @@ public:
void on_update_notification_confirm();
void do_printer_config_update();
void check_vendor_update(const std::string& vendor_id);
// Orca: async, mirrors check_vendor_update()/sync_vendor_config() — the network query and any
// download/install work happen on a background thread; only the confirmation dialog runs on
// the UI thread. `callback` is invoked on the UI thread with the ids of vendors that were
// installed (empty if none were found, or the user declined) and whether the user declined.
void check_new_vendors(const std::set<std::string>& system_vendors,
std::function<void(std::vector<std::string> installed_vendors, bool declined)> callback);
bool version_check_enabled() const;