Two review findings on the one-prompt session:
- The results file is bound to its session by a random token, written as
the file's first line and carried in the held manifest; a leftover or
planted file in the downloads folder can no longer pass for a
session's report. The session's name is only taken once its results
and process-id files are free too.
- The waiting side falls back to one-by-one installs only when the
elevated session is shown to be over (its process waited out, or its
process id gone). A session that was launched but cannot be seen to
finish - a slow antivirus start-up, a process that will not open -
now reports an error per app instead of racing a second installer.
The Linux gate never compiles this test, and Windows clippy stops at the
first hard error, so the field came to light only once the errors before
it were fixed.
Clippy on Windows (never compiled on the Linux gate) asked for two
things: run_msi now keeps to its caller's module, the visibility its
Package argument already has, and the session's cleanup-on-error uses
inspect_err, which is what removing a pid file on the way out is.
A job whose run_install ends before the installer is reached (a
repository-managed Flatpak, a missing release, the overwrite question, a
paused download) dropped its batch's gather without leaving it, so the
rest of the batch waited out the full ten-minute cap before their shared
prompt. The backend now holds the place with the same BatchPlace guard
install_at uses, so every way out of a job gives the place back.
* fix(macos): respect disabled Gatekeeper assessments
* fix(macos): read disabled assessment status on exit 1
* fix(native): allow the macOS-only exit details on other targets
Linux and Windows clippy (-D warnings) flagged RunOutput::code and the test
constructors as unused there. Same change as #1177.
Co-authored-by: AxoIsAxo <133877250+AxoIsAxo@users.noreply.github.com>
---------
Co-authored-by: AxoIsAxo <133877250+AxoIsAxo@users.noreply.github.com>
- Support ArtCraft Studio's published MSI and DMG asset formats in parse_asset_name while preserving existing asset naming
- Add real artcraft-release.json fixture and test asset selection across desktop platforms
- Add runs_on to CraftApp and filter out ArtCraft Studio on FreeBSD across backend and UI
- List ArtCraft Studio as app #01 in the README Craft App Suite table and xtask APPS array
The Flatpak check took any entry with this app's X-Flatpak and a flatpak run
Exec as on, also one a startup manager had hidden (Hidden=true, or GNOME's
X-GNOME-Autostart-enabled=false) or whose Exec started a different app.
It now reads the [Desktop Entry] keys as desktop-entry parsers do and
counts the entry only when it isn't hidden and its Exec is exactly the line
this launcher writes for its own app id.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
After switching from the host build to the Flatpak, the old host entry has
the same file name, so the setting showed on while its Exec still named the
host program. In the Flatpak only the Flatpak form (X-Flatpak=<id> and an
Exec of flatpak run) counts as on, and turning it on rewrites an old entry.
Both manifests grant xdg-config/autostart:create. Flatpak resolves that
on the host and binds the folder at its own path (common/flatpak-context.c,
common/flatpak-exports.c), so the entry can be written there also when
$XDG_CONFIG_HOME is outside home, and the folder is made on a fresh account.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Recompute the current revision, options and download plan when report_download rejects stale work. Add a real publication-boundary regression that fails with a missing plan before the fix and checks the accepted size plus Paused/Ready events. Local validation: serial native suite 279 passed, 3 existing ignored; Windows MSI integration 1 passed; strict all-target Clippy and repository formatting passed. The first parallel suite hit two unchanged HTTP fixture failures; both pass in the complete serial run.
Move the existing plan report after detected-state publication so post-job invalidation does not erase it. Strengthen the real-worker JobFinished observer regression: it fails with a missing plan before the fix. Full native validation passes 278 unit tests (3 existing ignored), the Windows MSI integration test, strict all-target Clippy and scoped formatting.
Observe the background Configure result before exercising stale-report rejection. Keep the existing plan and installed-format assertions, while excluding unrelated apps from the no-stale-events check. Full native validation: 278 tests passed, 3 existing ignored, plus the Windows MSI integration test; strict all-target Clippy passed.
Inside the Flatpak the autostart entry went into the sandbox's own config
folder (~/.var/app/<id>/config/autostart), which the host session never
reads, and its Exec line named the in-sandbox /app/bin path. When
/.flatpak-info exists the entry now goes into the host's autostart folder
($HOST_XDG_CONFIG_HOME, else ~/.config), is named after the Flatpak app id,
and starts the app with `flatpak run --command=artcraft-launcher <id>
--hidden` plus X-Flatpak, as the XDG Background portal writes it. Outside a
Flatpak the path and the entry are unchanged. Both manifests note that the
entry relies on --filesystem=home.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- When a copy appears between a publisher looking and taking reporting,
the answer uses the view its detection asked for (last_system_kinds)
instead of probing there. A copy only enters installed through
publish_detected, which asks first.
- Event::Installable's doc covers both emissions: in the batch with the
Latest or Detected it depends on, and alone when only the plan changes.
- Tests: no probe under the lock (expired view kept), a plan report's
answer follows the current settings, a concurrent poll never splits a
pair.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
run_install updates a repository-managed Flatpak with flatpak update (on
Linux, on the stable track) before any download is planned, but the
answer treated it as managed elsewhere and said no, so the new Reinstall
gate hid the launcher's only route to that update (Jeremy8776).
repository_update now holds that rule, and both run_install and the
answer ask it. The system_kinds doc says when it runs under a lock.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stages the race deterministically: a report in progress holds the
planning lock, a worker started before the settings change waits behind
it, the preferred format changes, and the worker's report is checked to
be the new one. Capturing settings at the worker's start would fail it.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Detection, an install's end and a check's end reported kept downloads
with settings captured when their thread started, so one could land
after the report of a newer settings change and show a stale Resume or
Retry. report_partial now reads the settings itself under a planning
lock, so whichever report comes last was worked out last.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Configure recomputed every app's planned download on the UI thread, and
inside the Flatpak sandbox each asks the host (up to 3 s). That now runs
on a thread, one at a time, each with the settings as they are when it
starts, so the newest settings have the last word. Installs and planned
downloads still ask the system afresh.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- The answer reads the backend options when it is worked out, so a job
that started before a settings change no longer answers with the old
version track (Jeremy8776).
- What the system can install is asked before the reporting lock is
taken and kept for a minute: in the Flatpak sandbox asking runs
flatpak-spawn on the host (up to 3 s). Installs still ask afresh
(Jeremy8776, CodeRabbit).
- No answer, and nothing asked, for an app that isn't installed: the UI
only uses it for an installed copy. Startup's cached replay, before
detection, no longer asks at all.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
install_plan takes how a copy managed elsewhere is explained. Installs
keep the full message (with pacman -Si naming the repository); the
installability answer, which runs on every check under the reporting
lock, only needs yes or no.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A copy installed outside the launcher, such as a FreeBSD system binary
in /usr/local/bin, was reported installable when a newer release had a
matching tarball, because only the download choice was asked. The install
then refused it as installed outside the launcher. install_plan now holds
every check an install makes before downloading (a copy managed
elsewhere, the platform, the release and its version track, the package),
and both run_install and Event::Installable use it.
Refs review by FredApps on #1252.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A poll could fall between Installable and the Latest or Detected after it,
and an answer worked out from older state could land after a newer one.
Each change to an app's latest release or installed copy is now recorded,
answered and queued under one lock, and the pair is queued together.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The UI guessed from any download this platform takes, but an install keeps
the way a copy was installed (an AppImage with an AppImage, an MSI with an
MSI), so an AppImage copy with a .deb-only release still showed Update,
was announced and started automatically, and then failed. The backend now
sends Event::Installable, from the same plan an install makes, before each
Latest and Detected it depends on and whenever the plan changes. The UI
uses it for installed copies and keeps its own check where there is no
answer (the web build).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Choose version, and the launcher's own version list in Settings, showed
only the tag. Each row now adds, at its right, the day the release came
out and the size of the package this machine would download, in the
launcher's usual date and size formats. Either is left out when unknown;
the row keeps the tag as its name and carries them as its description.
On the selected row they take the row's text colour, not the muted grey.
The size follows the backend's choice of package. An app's installed
copy keeps its format (an MSI stays an MSI, the launcher's Windows copy
is its portable ZIP, an AppImage stays an AppImage), else the preferred
formats among those it installs here; FreeBSD always takes the tarball.
Where that depends on package managers the UI can't see (other Linux
installs), the row gives the date alone. The launcher's own rows size
what its self-update downloads, the format the running copy was
installed as, which the backend now reports once it knows; until then
the manual download, except on Linux.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Each installed app's menu gains "Launch options…": flags typed there are
saved in Settings.launch_flags (app.ron, keyed by catalog id) and added to
the app's command line on Open. core::args::split turns the text into
arguments (whitespace, "…"/'…' quoting, backslashes kept for Windows
paths) and refuses an unclosed quote, both in the dialog and at launch.
Flatpak, launcher-unpacked, plain binary and macOS .app installs all pass
the flags through; strings are translated in every complete catalog.
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* refactor(native): one PATH lookup for detect and process
process.rs (`which_on_path`) and two places in detect.rs's Linux module
each searched PATH the same way. They call `process::which` now.
These all ask what the user can run, so PATH is the right place to look.
install.rs is left alone: programs run as root come from the root-owned
SYSTEM_BIN_DIRS, never from PATH.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Document the Linux detect, find_binary and binary_version functions
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Say every case where binary_version gives up
The 5-second limit matches the code. The doc also missed that the binary
can fail to start or exit with an error, and that it is killed on timeout.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* Timestamps saved to disk never wrap the schedule arithmetic (#774)
A saved feed-throttle `not_before` outside the sane range — the far-future
check missed anything at or below zero — survived `load`, and `wait`'s
`at - now` wrapped: near `i64::MIN` it panicked a debug build and, wrapped
in release, kept the feed from ever being asked again. `load` now drops
any wait outside 0..=now + 2 * MAX_THROTTLE_BACKOFF and the subtraction
saturates.
The update-check schedule had the same shape: a `last_check` in the future
(an edited app.ron, a clock that ran fast and was later corrected)
postponed checks until real time caught up, and i64::MIN panicked. The
condition is now `update_check_due`, which treats a future time as due and
saturates; the two rescan comparisons that share the pattern saturate too.
Co-Authored-By: GLM-5.3 <noreply@z.ai>
* Test the saved throttle state at its limits (#774)
Covers the review's remaining cases: a saved `not_before` of i64::MAX is
dropped while a sane one (now + 600) stands, and `failures`/`throttles`
saved at u32::MAX cap the next backoff at its maximum without overflowing
(`doubled` caps the doublings at 16 and the counts add saturating).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* Merge schedule tests into existing test module
* Preserve long Retry-After waits
---------
Co-authored-by: GLM-5.3 <noreply@z.ai>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* fix(windows): read an MSI's install context without starting PowerShell
The update path ran a hidden PowerShell that asked the WindowsInstaller COM
object which context a product was installed in. That is the pattern
behavioural antivirus flagged in #1045. The context now comes from
Windows Installer's own registration keys, read the way ProductsEx reads
them: this account's managed and unmanaged products and the machine's,
under the product code's packed name. A key the launcher may not read is
still a lookup that didn't run, so the uninstall-record fallback is
unchanged. Nothing in an update starts PowerShell any more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(windows): declare winsafe's advapi feature and avoid a panicking split
From review:
- current_user_sid uses HACCESSTOKEN and TokenInfo, which need winsafe's advapi
feature. It built only because craft-update enables it; now declared here.
- packed_code uses split_at_checked, like the .get() calls after it.
- The incident note lists the replaced lookup under "What changed", not "Still open".
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* test: accept any well-formed account SID, not only S-1-5-
A Microsoft Entra ID account's SID starts S-1-12-1-, so the test failed on
those machines although the lookup works. It now checks the SID's shape.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Command::Replace carried only the app. Between the "already exists"
question and the answer, the release can change (a new version, another
asset name, another folder), and the install then recomputed its
destination and wrote over a different existing file with replace=true.
Replace now carries the path from Event::Occupied. The backend checks
what is in the way again: if it is exactly that path the install
overwrites it; if something else is in the way it asks again; if nothing
is, it installs without leave to overwrite.
* fix(single-instance): exit only for a launcher that proved itself
A second start exited (Instance::Secondary) whenever the port's holder
answered with a version at least as new as its own, or failed the token
proof. Any local listener on 47413 could therefore keep the launcher from
starting without proving it belongs to the same account.
The running launcher now challenges every newcomer that offers a nonce,
whatever its version, and still grants Replace only to a newer one that
proves itself. The newcomer defers only to an authenticated answer; an
unproven or failed one is treated as an unrelated program and it starts
anyway, as it already did for a port held by something else.
* fix(single-instance): re-prove the port holder before exiting after a stalled takeover
* fix(single-instance): prove a stalled holder without the 0.0.0 probe
Launchers from 0.8.1 to 0.14.x only challenge a newer newcomer, so a 0.0.0
probe of a busy older launcher that stayed after a takeover was never proven
and the new start ran beside it. Greet with our own version, check the
holder's proof, and never answer the challenge, so the probe can only show
the window and never grant Replace.
Review of #1147:
- A failed OCI migration precheck (no version in the remote's AppStream data, polkit refusing
remote-add, offline) now falls back to the bundle update instead of failing it; only errors
once the reinstall starts are reported. The ref must list exactly the selected version.
- `flatpak list` names a named installation `system (<name>)`; the parser keeps `<name>`, and
one packages::flatpak_scope builds the --user/--system/--installation= argument everywhere.
- A repository-managed Flatpak on a pinned or nightly track gets the "update with flatpak"
hint instead of a plain `flatpak update`, and status no longer offers a GitHub version for it.
- The migration refuses while the app runs; the origin check after the reinstall can't be
cancelled; is_flatpark_origin matches only `flatpark`; settings_install keeps its doc; the
no-op oci_migration Cargo features are gone.
Signed bundles (storytold/craft-common-actions#129): a signed bundle's origin has the release
key but no signature lookaside, which `flatpak build-bundle` can't record, so its first update
from the ArtCraft OCI repository fails with "no valid signatures". The updater then sets
--signature-lookaside=https://getartcraft.com/signatures on that origin once and retries;
any other origin or error is left alone.
An all-users downgrade removed, under one elevated prompt, every product
code whose uninstall record carried the app's DisplayName and Publisher,
the current account's own HKCU records included. Those are writable
without elevation, so a program running as the user could plant one that
names the app and an unrelated all-users product's code, and have that
product removed under the app's permission prompt.
The elevated session now takes its removal targets only from the
machine's uninstall records (HKLM), which only administrators can write.
An all-users downgrade replaces an all-users copy, which a per-user
record never names.
Add the artcraft-oci remote with --if-not-exists so concurrent migrations
do not fail, let the --reinstall step finish once started because killing
flatpak-spawn does not stop the host transaction, and when the reinstall
fails after removing the ref, point to the .flatpak bundle on the releases
page.