From c59fb24933bf8c0793d65bd27c2f03446956bde0 Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Mon, 19 Oct 2026 18:31:52 +0800 Subject: [PATCH 01/13] fix: make backend update downloads cancellable --- racket/taskly/updater.rkt | 39 +++++++++++++++++++++++++++++++++------ 1 file changed, 33 insertions(+), 6 deletions(-) diff --git a/racket/taskly/updater.rkt b/racket/taskly/updater.rkt index 0657a8c..974f898 100644 --- a/racket/taskly/updater.rkt +++ b/racket/taskly/updater.rkt @@ -73,6 +73,7 @@ set-update-error! set-update-candidate! update-worker-thread + cancel-download! perform-check! start-download! rollout-bucket) @@ -134,6 +135,7 @@ (define candidate-box (box #f)) (define worker-thread-box (box #f)) +(define cancel-requested-box (box #f)) ;; Test seams: install a checked candidate without a live network check, ;; and reach the running worker so a test can break it the way the @@ -144,6 +146,20 @@ (define (update-worker-thread) (unbox worker-thread-box)) +(define (cancel-download!) + (define worker (unbox worker-thread-box)) + (cond + [(and worker (thread-running? worker)) + (set-box! cancel-requested-box #t) + (break-thread worker) + ;; The watchdog already relies on break-thread interrupting a blocked + ;; network read. Wait for the worker cleanup arm so a retry cannot race + ;; the previous transfer or inherit its partial file. + (sync (thread-dead-evt worker)) + (set-box! worker-thread-box #f) + #t] + [else #f])) + ;; Download watchdog bounds. The copy loop waits on read-bytes-avail! with ;; no timeout, so a connection that stalls without RST/FIN parks the worker ;; thread forever — visibly "stuck at 100%" (all bytes had arrived; the @@ -388,7 +404,7 @@ (appimage-artifact-urls candidate)) (define-values (self temporary) (appimage-download-target)) (define expected (fetch-sidecar-sha256! sidecar-url)) - (with-handlers ([exn:fail? + (with-handlers ([(lambda (e) (or (exn:fail? e) (exn:break? e))) (lambda (e) (when (file-exists? temporary) (delete-file temporary)) @@ -507,6 +523,7 @@ (define candidate (unbox candidate-box)) (unless candidate (error 'start-download! "no update is available; run a check first")) + (set-box! cancel-requested-box #f) (state-set! 'phase "downloading") (state-set! 'percent 0) (state-set! 'message #f) @@ -544,11 +561,21 @@ (define temporary (path-add-extension destination #".partial")) (when (file-exists? temporary) (delete-file temporary)) - (state-set! 'phase "error") - (state-set! 'message - (if (exn:break? e) - "update download stalled or timed out" - (exn-message e))))]) + (define cancelled? + (and (exn:break? e) (unbox cancel-requested-box))) + (if cancelled? + (begin + (state-set! 'phase "idle") + (state-set! 'percent 0) + (state-set! 'message #f) + (state-set! 'downloadedPath #f)) + (begin + (state-set! 'phase "error") + (state-set! 'message + (if (exn:break? e) + "update download stalled or timed out" + (exn-message e))))) + (set-box! cancel-requested-box #f))]) (define path (cond [(msi-flavor?) From 3e3945539cf4ee02237f19e055edffa8b6764429 Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Mon, 19 Oct 2026 18:32:10 +0800 Subject: [PATCH 02/13] fix: let hosts cancel the active update transfer --- racket/taskly/backend.rkt | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/racket/taskly/backend.rkt b/racket/taskly/backend.rkt index fba0d57..3066f58 100644 --- a/racket/taskly/backend.rkt +++ b/racket/taskly/backend.rkt @@ -271,11 +271,16 @@ (update-check->record (perform-check!)))) ;; Runs on a backend worker thread; the host follows progress via -;; update_state. Never raises: failures surface through the state's phase. +;; update_state. A second call while a transfer is active is the cancellation +;; signal used by the generated clients (keeps the RPC schema backward +;; compatible while making the existing Cancel controls real). (define-rpc (start_download : Void) (with-handlers ([exn:fail? (lambda (e) (set-update-error! (exn-message e)))]) - (start-download! (taskly-directory))) + (define worker (update-worker-thread)) + (if (and worker (thread-running? worker)) + (cancel-download!) + (start-download! (taskly-directory)))) (void)) (define-rpc (update_state : UpdateState) From c5ea707121547656702d83bc7f50b1a38d14c87b Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Mon, 19 Oct 2026 18:32:18 +0800 Subject: [PATCH 03/13] linux: cancel backend update downloads --- linux/src/main.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/linux/src/main.cpp b/linux/src/main.cpp index 98caf62..ea90512 100644 --- a/linux/src/main.cpp +++ b/linux/src/main.cpp @@ -1370,11 +1370,17 @@ void start_update_download() { auto* label = gtk_label_new(tr("updateDownloading").c_str()); gtk_box_append(GTK_BOX(content), label); - // Canceling only detaches the progress UI — the backend keeps downloading - // (family parity: there is no cancel RPC). + // start_download is intentionally a start/cancel toggle while a transfer + // is active, so the existing generated client can stop the backend worker + // without a schema-only regeneration. g_signal_connect( dialog, "response", - G_CALLBACK(+[](GtkDialog* dialog, gint, gpointer) { + G_CALLBACK(+[](GtkDialog* dialog, gint response, gpointer) { + if (response == GTK_RESPONSE_CANCEL && g_state.api != nullptr && + g_update.download_in_flight) { + g_state.api->start_download_async( + [](rivet_app::Result) {}); + } stop_update_poll(); g_update.progress_dialog = nullptr; g_update.progress_bar = nullptr; From c90f04d19bd21f05e51d410de398f745d1abf131 Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Mon, 19 Oct 2026 18:32:38 +0800 Subject: [PATCH 04/13] windows: declare update cancel action --- windows/MainWindow.xaml.h | 3 +++ 1 file changed, 3 insertions(+) diff --git a/windows/MainWindow.xaml.h b/windows/MainWindow.xaml.h index abf1429..2aae19e 100644 --- a/windows/MainWindow.xaml.h +++ b/windows/MainWindow.xaml.h @@ -150,6 +150,9 @@ struct MainWindow : MainWindowT { std::string const& failure, bool silent); void RecordUpdateCheck(); void StartDownload(); + void OnCancelUpdateDownload( + winrt::Windows::Foundation::IInspectable const&, + Microsoft::UI::Xaml::RoutedEventArgs const&); void PollUpdateState( std::shared_ptr const& backend); void HandleUpdatePoll(rivet_app::Result const& result); From ee3d884c2e85116fa5db4763d8b1bd8cc99162e1 Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Mon, 19 Oct 2026 18:32:40 +0800 Subject: [PATCH 05/13] windows: expose update download cancel button --- windows/MainWindow.xaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/windows/MainWindow.xaml b/windows/MainWindow.xaml index ffefa75..eca5d31 100644 --- a/windows/MainWindow.xaml +++ b/windows/MainWindow.xaml @@ -90,6 +90,9 @@ IsOpen="False" IsClosable="False" Severity="Informational"> + +