From 8b0c6d0f1772b16b26819ebc3d8bec248ab208dc Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Mon, 19 Oct 2026 19:44:26 +0800 Subject: [PATCH 1/8] fix: make backend update downloads cancellable --- racket/taskly/updater.rkt | 46 ++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 6 deletions(-) diff --git a/racket/taskly/updater.rkt b/racket/taskly/updater.rkt index fc596c5..e8bdaa6 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! update-install-kind start-download! @@ -135,6 +136,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 @@ -145,6 +147,26 @@ (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) + ;; Wait for termination before publishing idle. A very early cancel can + ;; land before the worker enters its with-handlers body, so the caller + ;; owns the final state normalization instead of relying solely on the + ;; worker cleanup arm. + (sync (thread-dead-evt worker)) + (set-box! worker-thread-box #f) + (state-set! 'phase "idle") + (state-set! 'percent 0) + (state-set! 'message #f) + (state-set! 'downloadedPath #f) + (set-box! cancel-requested-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 @@ -164,6 +186,7 @@ (define (reset-update-state!) (set-box! candidate-box #f) + (set-box! cancel-requested-box #f) (set-box! update-state (hasheq 'phase "idle" 'percent 0 @@ -390,7 +413,7 @@ (define (download-appimage-with-progress! config candidate) (define-values (self temporary) (appimage-download-target)) - (with-handlers ([exn:fail? + (with-handlers ([(lambda (e) (or (exn:fail? e) (exn:break? e))) (lambda (e) (when (file-exists? temporary) (delete-file temporary)) @@ -426,6 +449,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) @@ -463,11 +487,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 [(appimage-flavor?) From 55a36e9dcdff84f1f747f5d9123525e95230dc1f Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Mon, 19 Oct 2026 19:44:36 +0800 Subject: [PATCH 2/8] 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 9f516401596b654bf9ab1f7bb557c5836b4cd682 Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Mon, 19 Oct 2026 19:44:45 +0800 Subject: [PATCH 3/8] 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 0428b2d86755b85caefec8c81116ef3facafd925 Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Mon, 19 Oct 2026 19:44:55 +0800 Subject: [PATCH 4/8] windows: expose update cancel action --- windows/MainWindow.xaml.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/windows/MainWindow.xaml.h b/windows/MainWindow.xaml.h index abf1429..25acf31 100644 --- a/windows/MainWindow.xaml.h +++ b/windows/MainWindow.xaml.h @@ -74,6 +74,12 @@ struct MainWindow : MainWindowT { void OnNewTaskKeyDown(winrt::Windows::Foundation::IInspectable const&, Microsoft::UI::Xaml::Input::KeyRoutedEventArgs const&); + // Update download action wired from XAML; event handlers must be public + // because generated Connect() binds them through MainWindowT. + void OnCancelUpdateDownload( + winrt::Windows::Foundation::IInspectable const&, + Microsoft::UI::Xaml::RoutedEventArgs const&); + private: enum class ViewKind { Today, Planned, All, Completed, List }; From eb38a1b7f7534ec2372c89ccd6e1e85778f6f9bf Mon Sep 17 00:00:00 2001 From: TuringLambdaAI Date: Mon, 19 Oct 2026 19:45:06 +0800 Subject: [PATCH 5/8] windows: add update 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"> + +