The Windows helper of a finished installation started only when show_if
returned Action::Close. An app that exited another way (the window's
close button, a quit while unsaved work held the close back, an OS
shutdown) dropped it, so the update never installed and the download
stayed in the cache.
Dropping the Updater now starts a helper that is still waiting, once,
including one from an installation that finished after the last tick.
Updater::start_pending_update does the same for an app that ends the
process in on_exit without dropping the Updater; Action::Close's doc
comment names it.
spawn_detached, run_command and open_url_with_fallback started their
reaper, pipe-reader and opener threads with std::thread::spawn, which
panics when the thread cannot be created. The Windows update helper now
starts from the UI thread when the app closes, so such a panic would end
the app. Each thread now starts through std::thread::Builder: a process
that has already started keeps running and the failed wait is logged,
run_command stops the helper and returns an error, and a failed opener
thread is logged.
On Windows the update helper was started as soon as the download was
ready, then waited one minute for the app to exit. An app that held
unsaved work, or a user who did not close it, outlasted that wait; the
helper then started the old exe again with no arguments, which opened a
second window of the old version and installed nothing.
- self_update::prepare_update does what update did but returns a
Restart. On Windows the Restart records the release as installed and
starts the helper; on macOS and Linux it does nothing, since the
restart is already scheduled. update calls prepare_update and starts
the Restart at once, so its callers see no change.
- Updater starts the Restart when it returns Action::Close. If the
helper can't start, the app stays open and the prompt shows the error.
- The helper does not start any exe when the app is still running after
its wait. A step that fails after the app exited still starts the old
exe, as before.
The max_total and Budget docs now say the budget counts bytes consumed
from gzip, and that unused lookahead from the initial gzip read (chunks
of up to 64 KiB) is not counted.
The recovery stage is now created, opened and, when opening fails, removed through
the retained recovery directory handle. A failed open no longer leaves an empty
stage under .removing, and the removal is not recursive. A rollback failure now
names the recovery stage ID next to the pack's last known path.
Add the linesweeper 0.5.0 package to vendor/ with no file changed, so
that a fix to it can reach the Craft apps through this repository
without waiting for a linesweeper release. The GitHub repository its
manifest names is archived, and its README says development moved off
GitHub.
Leave out the test font (fonts live in craft-fonts), the logo artwork,
the snapshot images and the package's repository tooling. SOURCE.json
lists each with its hash.
Add a workspace exclude, a [patch.crates-io] entry, SOURCE.json with
the archive hash and per-file hashes, NOTICE and ATTRIBUTION.md rows,
and the package's tests in the backend test script. craft-prepress's
flatten feature now builds against the copy, so Cargo.lock drops the
package's registry source and checksum.
A call that timed out left the app's request future running, so an app that releases a request when its future is dropped (the desktop ControlRequest abandons a request nobody waits for) ran the call late, after the page had been told it failed. call() now drives the request future itself and drops it when the timer wins.
The desktop control protocols name the base64 PNG of ui.screenshot differently (pngBase64, png_base64, png, base64), so screenshot() gave the page a different shape in each app. It now also puts the PNG in data and leaves the app's own field where it is.
The timer handle is kept as the environment returned it: Node returns an object, not a number, so a numeric id never cleared the timer and it fired after its closure was dropped.
The deny-list paragraph and the chrono calendar exception now live in the Rust rules. CLAUDE.md imports AGENTS.md, and the workspace-lints requirement stays with the new-crate notes.
* craft-print: a job can be cancelled until it starts reaching the spooler
On Windows a job asks the printer's driver for its capabilities before
anything is sent. A networked printer in power save can hold that query
for a long time, and an app had no way to give up on the job without
risking it printing later (storytold/wordcraft#5710; CodeRabbit on
storytold/wordcraft#5732).
A Cancel token, passed to the new submit_cancellable, moves a job from
pending to cancelled or to sending. The job checks it at the last point
before anything reaches the spooler: on Windows after the driver has
answered, before the job file is written; on macOS and Linux before lp
starts. A cancelled job returns an error and is never sent; a job already
being sent completes. submit and submit_with_progress are unchanged, and
Error keeps its one variant so callers that match on it still compile.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-print: a job turns to sending with the provider held; the job file waits for it
Review: marked as sending before it took the PrintTicket provider, a job
could then wait behind another job's driver call that a sleeping printer
held, and an app that waits only for jobs being sent would wait on it.
The job now turns to sending only once it holds the provider, right
before printing, and writes its file only after that, so a cancelled job
leaves no copy of the document behind.
A test-only hold in the driver call lets the Windows spooler test cancel
a job while its driver doesn't answer: the cancel wins and, once the
driver answers, the job stops instead of reaching the paused queue.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-print: the DEVMODE conversion comes before a job turns to sending
CodeRabbit on #622: winprint converts the ticket to a DEVMODE, a driver
call a sleeping printer can stall, before it creates the native job, so
a job stalled there had already turned to sending and couldn't be
cancelled. The conversion now runs first, with the provider held, while
the job can still be cancelled; the provider stays held until the first
copy's own conversion, so no other job comes between them.
Tests: the Windows spooler test also cancels a job held in that
conversion; the CUPS test gives a spooler that can't start, so any
attempt to start it would change the error. README: cancellation in the
progress section.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-print: clarify cancellation during winprint conversion repeat
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
* craft-font-catalog: run the Unix font-folder tests only on Unix
Five tests build an Os::Unix FontEnv from Unix paths (/home/me, <dir>/sys/fonts</dir>).
Windows doesn't treat those paths as absolute, so the tests failed there. Production code
is unaffected: on Windows FontEnv::from_process picks Os::Windows, whose folders never go
through the Unix path logic. FontEnv's docs now say its paths follow the host's path rules.
The font-store workflow, which already triggers on crates/font-catalog, now also runs
craft-font-catalog's clippy and tests on Linux and Windows.
Refs #619
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-font-catalog: keep the store override and no-store tests on Windows
These assertions don't depend on Unix path rules, so they now sit in their own
ungated test.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* craft-font-catalog: verify store override precedence
Exercise the override with platform defaults present on every OS variant.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
The live spooler test only checked that printer_queues() reports
two_sided as unknown, which would still pass if it called printers() and
cleared the field (CodeRabbit on #618). A test-only per-thread counter in
two_sided(), the one PrintCapabilities query printer discovery makes,
now shows printers() querying the test queue's driver (the control) and
printer_queues() querying none.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On Windows, printers() reads each driver's PrintCapabilities to report
two-sided support before it returns. A networked printer in power save
can hold that query for a long time, so a Print dialog showed no printer
at all, not even the ones that were awake (storytold/wordcraft#5710,
storytold/wordcraft#5645, storytold/wordcraft#5413).
printer_queues() returns the same queues and default from EnumPrinters
and GetDefaultPrinter with two_sided unknown, and makes no driver call.
Submission still checks the job's options with the driver. On other
platforms it is printers(). printers() is unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
zip's `deflate` feature pulls in deflate-flate2-zlib-rs (and zopfli). Cargo
unifies flate2's features across the whole app, so on Windows every flate2
user in an app depending on craft-update switched to zlib-rs, whose level 1
is a static-Huffman quick mode that can produce output larger than its input
(Lightcraft's 16-bit ZIP TIFFs on Windows came out bigger than uncompressed).
Use `deflate-flate2` with flate2's default miniz_oxide backend instead; the
updater only reads Deflate zips, which that covers.
A blank name falls back to the default label only when no letter or
digit is left ("{name} 2" gives "2"); the docs and the rustdoc of
labels::named said a letter. A test pins both cases.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The accessibility section said a group's top border band is never
announced. Where no drag bar is drawn the band is the group's one move
handle and has the node.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Keep both craft-web-api and craft-web-host in the workspace list, the
README and the lockfile. Point the host README at the command API that
landed in #413, and the command API README at these host messages.
Co-authored-by: Cursor <cursoragent@cursor.com>
A page host can report a new dirty state while post_dirty's event is still
dispatching. Remember the state first so that later report wins, and restore
the previous state only when the send fails and nothing else recorded one.
Co-authored-by: Cursor <cursoragent@cursor.com>
A group's drag bar and its top border band were both buttons with
move_group_label, so a screen reader heard every group twice, and every
group and column bar had the same name. The band, which only drags, is
now out of the accessibility tree and keyboard focus (unless no bar is
drawn), and a `{name}` in move_group_label or move_column_label stands
for what the handle moves: the logical label of the panel the group
shows, or the titled panels a column's groups show, joined by ", ". A
blank title leaves the placeholder out with the brackets, quotes or
separator around it. Labels without it, today's defaults included, are
used unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
wasm-bindgen throws "closure invoked recursively" when an FnMut closure
is called while it runs, before its body can queue the message. The
listeners only read their captures, so they are Fn; the handler's state
stays behind the Inbox's RefCell.