Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add New API for ProgressBoxEx to Show Progress & Add Progress Display for Plugin Downloading & Improve DownloadUrl Api Function #3170

Open
wants to merge 28 commits into
base: dev
Choose a base branch
from

Conversation

Jack251970
Copy link
Contributor

@Jack251970 Jack251970 commented Jan 5, 2025

Content

  • Add new api of showing progress: IProgressBoxEx ShowProgressBox(string caption, Action forceClosed = null).
  • Add progress displaying & cancellation when downloading plugin

Screenshots

Interface

image

Cancel Download

2025-01-09.16-39-48.mp4

Finish Download

2025-01-09.16-40-07.mp4

Test

  • ProgressBoxEx usage in PluginManager plugin (progress displaying & cancellation)

@prlabeler prlabeler bot added the enhancement New feature or request label Jan 5, 2025

This comment has been minimized.

Copy link

gitstream-cm bot commented Jan 5, 2025

🥷 Code experts: jjw24

jjw24 has most 🧠 knowledge in the files.

See details

Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs

Knowledge based on git-blame:
jjw24: 18%

Flow.Launcher/PublicAPIInstance.cs

Knowledge based on git-blame:
jjw24: 9%

Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs

Knowledge based on git-blame:
jjw24: 56%

Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs

Knowledge based on git-blame:
jjw24: 16%

To learn more about /:\ gitStream - Visit our Docs

Copy link

gitstream-cm bot commented Jan 5, 2025

Be a legend 🏆 by adding a before and after screenshot of the changes you made, especially if they are around UI/UX.

Copy link
Contributor

coderabbitai bot commented Jan 5, 2025

📝 Walkthrough

Walkthrough

This pull request introduces a new method, ShowProgressBoxAsync, to the IPublicAPI interface, allowing plugins to display a standardized asynchronous progress box with optional cancellation. The changes include updates to the HttpDownloadAsync method for progress reporting, modifications to several classes to implement asynchronous operations, and the introduction of a new ProgressBoxEx window for user feedback during long-running tasks. The enhancements improve the plugin system's capability to manage user interactions during downloads and installations.

Changes

File Change Summary
Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs Added ShowProgressBoxAsync method; updated HttpDownloadAsync to include reportProgress parameter.
Flow.Launcher/PublicAPIInstance.cs Implemented ShowProgressBoxAsync method; updated HttpDownloadAsync to include reportProgress parameter.
Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs Updated QueryAsync to be asynchronous; renamed RequestInstallOrUpdate to RequestInstallOrUpdateAsync.
Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs Enhanced InstallOrUpdateAsync method; changed zip visibility; added exception handling and progress reporting.
Flow.Launcher/ProgressBoxEx.xaml Introduced new XAML window for progress box with cancel functionality.
Flow.Launcher/ProgressBoxEx.xaml.cs Added ProgressBoxEx class with asynchronous capabilities and progress reporting.
Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs Updated HttpDownloadAsync to include reportProgress parameter.
Flow.Launcher.Infrastructure/Http/Http.cs Modified DownloadAsync to support progress reporting during downloads.

Possibly related PRs

Suggested labels

bug, 1 min review

Suggested reviewers

  • jjw24
  • taooceros

Poem

🐰 A Progress Box Appears
Async magic in the air,
A rabbit's progress tracker with flair,
Cancellable, smooth, and bright,
Plugins dancing with delight!
Hop, hop, progress goes free! 🚀


Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai generate unit testing code for this file.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and generate unit testing code.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR. (Beta)
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (15)
Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs (10)

145-146: Consider using a CancellationTokenSource for download cancellation.
Currently, downloadCancelled is a simple boolean. If future requirements call for more complex scenarios (like timeouts or multiple parallel downloads), a CancellationTokenSource may be more robust. For now, this approach is straightforward and acceptable.


154-156: Add timeouts or retry policy to the HttpClient usage.
Using HttpClient without an explicit timeout or retry logic might cause the application to hang indefinitely if the server fails to respond. Adding a timeout or retry policy could improve reliability.


157-157: Log failed responses.
response.EnsureSuccessStatusCode() is good. However, if it fails, consider logging relevant headers or status codes for better diagnostics.


159-161: Support indefinite progress when content length is unknown.
If ContentLength is -1, no progress reporting occurs. Consider a fallback indefinite progress bar for such situations to signal ongoing activity to the user.


170-189: Check chunk size and iteration performance.
The usage of an 8KB buffer inside a while loop is typical. For very large downloads, you might consider measuring performance or adjusting the buffer size to reduce overhead. Also, consider verifying cancellation more frequently if extremely large files must be handled.


191-201: Combine download methods to reduce duplication.
If canReportProgress is false, the code defaults to Http.DownloadAsync. That is fine, but you might unify logic for progress/no-progress scenarios to maintain consistent error handling or support partial progress.


211-213: Consider post-cancellation cleanup.
You return immediately if downloadCancelled is true, skipping installation. If partial files exist at filePath, consider cleaning them up to avoid leaving orphaned files.


219-234: Show additional diagnostics on HttpRequestException.
Currently, the code logs a general error and displays a generic message. Consider including the HTTP status code or partial stack trace to help with troubleshooting.


239-254: Consolidate code in exception handlers.
The logic to close the progress box and display an error is repeated between HttpRequestException and the general Exception block. Consider extracting this into a helper method to reduce duplication.


Line range hint 420-492: Add overall progress for “Update All” operation.
When updating multiple plugins, it may be helpful to display a combined progress indicator. This would align the user experience with single plugin updates by showing progress for each or overall.

Flow.Launcher.Core/ProgressBoxEx.xaml.cs (3)

14-18: Constructor approach is straightforward.
Invoking InitializeComponent() inside the constructor is standard. Ensure that _forceClosed is not null-checked here if you plan to handle edge cases later in ForceClose.


44-65: Dispatcher invocation in ReportProgress.
Propagating method calls to the main UI thread is essential for WPF UI changes. The value clamping to 0 or 100 is correct. If you anticipate extremely large increments, consider floating-point rounding.


78-82: Esc key closure is user-friendly.
Binding the Escape key is a nice usability feature. Ensure you communicate this behavior to users in any relevant documentation, if needed.

Flow.Launcher.Core/ProgressBoxEx.xaml (2)

1-16: Window sizing and theming.
The Width="420" with SizeToContent="Height" and dynamic resources for background/foreground ensure a flexible UI that can adapt to theming changes. Confirm that 420px is sufficient for localized text.


26-62: Title bar close button approach.
The custom path and styling for the close button is user-friendly and matches typical “x” icons. Check that the path dimensions remain consistent on high-DPI systems.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a34136f and c907c29.

📒 Files selected for processing (7)
  • Flow.Launcher.Core/ProgressBoxEx.xaml (1 hunks)
  • Flow.Launcher.Core/ProgressBoxEx.xaml.cs (1 hunks)
  • Flow.Launcher.Plugin/IProgressBoxEx.cs (1 hunks)
  • Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs (2 hunks)
  • Flow.Launcher/PublicAPIInstance.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs (6 hunks)
🔇 Additional comments (15)
Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs (3)

162-168: Validate usage of the cancellation callback.
The cancellation callback sets downloadCancelled and prgBox = null. Ensure that any subsequent logic that references prgBox handles the null state gracefully.


407-407: Ensure the user is notified on failed tasks.
Using ContinueWith(..., TaskContinuationOptions.OnlyOnFaulted, ...) is valid for cleanup or logging. Confirm that the user receives a clear error message or UI feedback if an update fails in a background task.


628-628: Method name is consistent and clear.
Renaming RequestInstallOrUpdate to RequestInstallOrUpdateAsync clarifies its asynchronous nature, aligning with best practices.

Flow.Launcher.Plugin/IProgressBoxEx.cs (1)

1-20: Interface design appears cohesive.
Defining ReportProgress(double progress) and Close() suffices for most simple progress-box use cases. In future expansions, consider adding asynchronous variants, advanced cancellation, or status text updates if needed.

Plugins/Flow.Launcher.Plugin.PluginsManager/Main.cs (1)

54-54: Good adaptation of asynchronous API usage.
Replacing RequestInstallOrUpdate with RequestInstallOrUpdateAsync ensures consistency and proper async handling in the query method. No issues noted.

Flow.Launcher.Core/ProgressBoxEx.xaml.cs (5)

9-13: Interface implementation and field usage.
ProgressBoxEx correctly implements IProgressBoxEx. The _forceClosed field is stored for a callback, and _isClosed ensures we don't close multiple times. This is a well-structured approach.


20-42: Robust UI-thread checks & error handling in Show method.
Calling Show via the dispatcher ensures thread safety. The try-catch block logs exceptions without crashing the application. This approach is commendable for user-facing components.


67-77: Safe override of Close method.
The _isClosed guard ensures the window is not double-closed, preventing potential exceptions. This pattern is good for UI stability.


83-91: Cancel button event.
ForceClose is consistently called for both the “Cancel” button and the close button, avoiding duplicated logic. This helps reduce complexity.


93-104: ForceClose handles the callback.
Invoking _forceClosed?.Invoke() after verifying _isClosed is a good pattern. This ensures that external cleanup or cancellation logic is triggered exactly once.

Flow.Launcher.Core/ProgressBoxEx.xaml (3)

17-25: WindowChrome and keyboard shortcuts.
Defining WindowChrome with CaptionHeight="32" provides a modern look. Using KeyBinding for EscapeClose is an excellent usability enhancement.


63-86: Title and progress bar arrangement.
Placing the TextBlock and ProgressBar in separate rows is a clean layout. The margins are well-balanced. If a very long title is shown, ensure the text wrapping doesn't overlap the progress bar.


87-105: Footer container and Cancel button.
Using a Border for the footer with a separate button region is a nice separation of concerns in the UI. The use of dynamic resources for background/border color ensures theming consistency.

Flow.Launcher/PublicAPIInstance.cs (1)

327-328: Consider thread-safety measures and explicit documentation.

This new method is straightforward, but confirm whether it's safe to call on threads other than the UI thread. If the call must be confined to the UI thread, explicitly mention that in documentation or throw an appropriate exception when invoked from a background thread. This approach can help prevent race conditions and GUI operation errors.

Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs (1)

319-327: Ensure consistency and clarify null return conditions.

The new ShowProgressBox method aligns well with the existing "ShowX" naming style. However, the documentation mentions a possible null return; verify if calling this method in certain edge conditions or from non-UI threads might produce a null result. If so, add clarifications to help plugin developers handle this safely.

This comment has been minimized.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🔭 Outside diff range comments (1)
Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs (1)

Line range hint 424-496: Add progress reporting and cancellation support for batch updates.

The "Update All" functionality lacks progress reporting and cancellation support, which are available for single plugin updates.

Consider implementing:

  1. Progress reporting for batch updates
  2. Cancellation support using the provided CancellationToken
  3. Parallel download limits to prevent overwhelming the server

Example implementation available upon request.

🧹 Nitpick comments (1)
Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs (1)

162-172: Simplify progress box initialization and disposal.

The progress box initialization and disposal logic can be simplified using pattern matching and null-coalescing operators.

-    if (canReportProgress && 
-        (prgBox = Context.API.ShowProgressBox(prgBoxTitle, () =>
-        {
-            if (prgBox != null)
-            {
-                httpClient.CancelPendingRequests();
-                downloadCancelled = true;
-                prgBox = null;
-            }
-        })) != null)
+    if (canReportProgress && 
+        Context.API.ShowProgressBox(prgBoxTitle, () =>
+        {
+            httpClient.CancelPendingRequests();
+            downloadCancelled = true;
+        }) is IProgressBoxEx progress)
+    {
+        prgBox = progress;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c907c29 and 0fabe31.

📒 Files selected for processing (1)
  • Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs (6 hunks)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: gitStream.cm
🔇 Additional comments (2)
Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs (2)

632-632: LGTM! Method naming follows async conventions.

The method rename to include the 'Async' suffix follows C# naming conventions for asynchronous methods.


181-193: 🛠️ Refactor suggestion

Add ConfigureAwait(false) to async operations.

Missing ConfigureAwait(false) in async operations can lead to potential deadlocks in UI applications.

-            while ((read = await contentStream.ReadAsync(buffer).ConfigureAwait(false)) > 0)
+            while ((read = await contentStream.ReadAsync(buffer).ConfigureAwait(false)) > 0)
             {
-                await fileStream.WriteAsync(buffer.AsMemory(0, read)).ConfigureAwait(false);
+                await fileStream.WriteAsync(buffer.AsMemory(0, read)).ConfigureAwait(false);
                 totalRead += read;
 
                 var progressValue = totalRead * 100 / totalBytes;

Likely invalid or redundant comment.

This comment has been minimized.

This comment has been minimized.

@taooceros
Copy link
Member

Mind sharing some screenshot (or a short video) to demonstrate the behavior?

@Jack251970
Copy link
Contributor Author

Mind sharing some screenshot (or a short video) to demonstrate the behavior?

Sure, please check updated desc.

This comment has been minimized.

This comment has been minimized.

@Jack251970
Copy link
Contributor Author

I feel like the code related to progress report is way to complicated and should requires some encapsulation. Would you mind do something like this https://stackoverflow.com/a/46497896 (and maybe use the IProgress<T> interface instead of creating a new one).

I have considered to use delegate to report the progress. (This indeed can remove the IProgressBoxEx)

But in PluginManager plugin, it can cause the issue that the progress window cannot be closed if catching exception in

catch (HttpRequestException e)
{
    // force close progress box
    await prgBox?.CloseAsync();

    // show error message
    Context.API.ShowMsgError(
        string.Format(Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"), plugin.Name),
        Context.API.GetTranslation("plugin_pluginsmanager_download_error"));
    Log.Exception("PluginsManager", "An error occurred while downloading plugin", e);

    return;
}
catch (Exception e)
{
    // force close progress box
    await prgBox?.CloseAsync();

    // show error message
    Context.API.ShowMsgError(Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"),
        string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"),
            plugin.Name));
    Log.Exception("PluginsManager", "An error occurred while downloading plugin", e);

    return;
}

So I need to get the progress window in the exception sentence, like using await prgBox?.CloseAsync(); to force the FL to close progress box as you can see above.

@taooceros
Copy link
Member

I don't really understand sorry. Why does delegate cannot handle that? I think we can use some special value (like -1) to represent error on the delegate so I don't really think that's a problem?

This comment has been minimized.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
Flow.Launcher/ProgressBoxEx.xaml.cs (1)

111-113: ⚠️ Potential issue

Avoid hiding base class method Close()

The method private void Close() hides the inherited Close() method from the Window class. Hiding methods can lead to confusion and maintenance issues.

As per previous discussions and learned guidelines, consider renaming the method to avoid hiding the base class method:

-private void Close()
+private void ForceCloseWindow()
 {
     Close();
     _forceClosed?.Invoke();
 }

Update all references to Close() within this class to ForceCloseWindow().

🧹 Nitpick comments (4)
Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs (2)

319-332: Ensure consistent documentation and method ordering

The documentation for ShowMsgBox and ShowProgressBoxAsync should be consistent in style and detail. Additionally, consider placing the new ShowProgressBoxAsync method appropriately within the interface for better readability.

  • Align the documentation style with existing methods.
  • Place the new method after related message box methods.

330-331: Clarify the return value in the documentation

The summary for ShowProgressBoxAsync mentions that it returns a progress box interface, but the method returns a Task. Clarify the return type and its purpose.

Update the method documentation to accurately reflect the return value.

 /// <returns>A task that represents the asynchronous operation.</returns>
Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs (2)

222-237: Consolidate exception handling and logging

The exception handling in the catch blocks is similar. To reduce code duplication and improve clarity, consider consolidating them.

Extract common code into a method or adjust the catch blocks accordingly.


Line range hint 417-475: Consider using MessageBoxResult.Yes for consistency

In the AsyncAction for the "Update All" result, you check for MessageBoxResult.No, whereas in other places you check for MessageBoxResult.Yes. This inconsistency can lead to confusion.

Modify the condition to check for MessageBoxResult.Yes to align with other parts of the code.

 if (Context.API.ShowMsgBox(message,
         Context.API.GetTranslation("plugin_pluginsmanager_update_title"),
-        MessageBoxButton.YesNo) == MessageBoxResult.No)
+        MessageBoxButton.YesNo) != MessageBoxResult.Yes)
 {
     return false;
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ed7265d and 297d191.

📒 Files selected for processing (4)
  • Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs (2 hunks)
  • Flow.Launcher/ProgressBoxEx.xaml.cs (1 hunks)
  • Flow.Launcher/PublicAPIInstance.cs (1 hunks)
  • Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • Flow.Launcher/PublicAPIInstance.cs
🧰 Additional context used
📓 Learnings (1)
Flow.Launcher/ProgressBoxEx.xaml.cs (1)
Learnt from: taooceros
PR: Flow-Launcher/Flow.Launcher#3170
File: Flow.Launcher/ProgressBoxEx.xaml.cs:79-88
Timestamp: 2025-01-10T04:58:20.225Z
Learning: In Flow.Launcher, avoid hiding base class methods with the `new` keyword. Instead, use event handlers, method overriding (where the base method is virtual), or create new methods with different names to extend functionality.
🔇 Additional comments (5)
Flow.Launcher/ProgressBoxEx.xaml.cs (2)

13-17: 🛠️ Refactor suggestion

Call InitializeComponent() before other initialization

It's a good practice to call InitializeComponent() at the beginning of the constructor to ensure that all UI components are initialized before being accessed.

Apply this diff to reorder the constructor:

 private ProgressBoxEx(Action forceClosed)
 {
+    InitializeComponent();
     _forceClosed = forceClosed;
-    InitializeComponent();
 }

Likely invalid or redundant comment.


60-81: Prevent multiple progress reports after closure

If ReportProgress is called after the progress box is closed, it may result in exceptions.

Ensure that ReportProgress cannot be called after the progress box has been closed. Consider adding a guard condition to check if the window is still open before updating the progress.

 private void ReportProgress(double progress)
 {
     if (!Application.Current.Dispatcher.CheckAccess())
     {
         Application.Current.Dispatcher.Invoke(() => ReportProgress(progress));
         return;
     }
 
+    if (!IsVisible)
+        return;
+
     if (progress < 0)
     {
         ProgressBar.Value = 0;
     }
     else if (progress >= 100)
     {
         ProgressBar.Value = 100;
         Close();
     }
     else
     {
         ProgressBar.Value = progress;
     }
 }
✅ Verification successful

The suggested guard condition is necessary and correct

The IsVisible check is important because:

  • The progress window is used in async operations that may continue reporting progress after the window is closed
  • Without the guard, there's a risk of accessing disposed UI controls when progress updates arrive after closure
  • The solution follows WPF best practices for handling window lifecycle in async scenarios

The suggested implementation should be applied as proposed in the review comment.

🏁 Scripts executed

The following scripts were executed for the analysis:

Error: No shell scripts found


Script:

#!/bin/bash
# Find usages of ProgressBoxEx
rg "ProgressBoxEx" -l

# Look for instantiation and usage patterns
rg "new ProgressBoxEx" -A 5

# Find similar progress reporting patterns
ast-grep --pattern 'ReportProgress($$$)'

Length of output: 802


Script:

#!/bin/bash
# Check PublicAPIInstance.cs content
cat Flow.Launcher/PublicAPIInstance.cs

# Look for async patterns around ProgressBox
rg -B 3 -A 10 "ProgressBoxEx" --type cs

Length of output: 17378

Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs (3)

20-20: Reuse HttpClient carefully to avoid socket exhaustion

Using a static HttpClient instance is generally recommended, but care must be taken to manage its lifetime and avoid unintended consequences.

Ensure that the static HttpClient is properly configured and that default settings like Timeout are appropriate for all use cases.


191-194: ⚠️ Potential issue

Ensure progress box is closed upon cancellation

When downloadCancelled is true, the method returns without closing the progress box, which may leave it open.

Modify the code to close the progress box before returning:

 if (downloadCancelled)
 {
+    await Context.API.HideProgressBoxAsync();
     return;
 }

Assuming there is a method to hide or close the progress box. If not, ensure the progress box is properly closed in this scenario.

Likely invalid or redundant comment.


147-203: 🛠️ Refactor suggestion

Handle cancellation tokens properly during download

The cancellation token cts.Token should be passed to all asynchronous methods that support it, including ReadAsync and WriteAsync, to ensure that the operation can be cancelled promptly.

Apply this diff to include the cancellation token:

 while ((read = await contentStream.ReadAsync(buffer, cts.Token).ConfigureAwait(false)) > 0)
 {
     await fileStream.WriteAsync(buffer.AsMemory(0, read), cts.Token).ConfigureAwait(false);
     totalRead += read;

     var progressValue = totalRead * 100 / totalBytes;

     if (downloadCancelled)
         return;
     else
         reportProgress(progressValue);
 }

Likely invalid or redundant comment.

Flow.Launcher/ProgressBoxEx.xaml.cs Show resolved Hide resolved
Flow.Launcher/ProgressBoxEx.xaml.cs Show resolved Hide resolved
Flow.Launcher/ProgressBoxEx.xaml.cs Outdated Show resolved Hide resolved

This comment has been minimized.

@Jack251970
Copy link
Contributor Author

I don't really understand sorry. Why does delegate cannot handle that? I think we can use some special value (like -1) to represent error on the delegate so I don't really think that's a problem?

@taooceros Sorry, it seems that I encountered the thread issue last time. Now update and please check.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs (4)

20-21: Use HttpClientFactory instead of static HttpClient.

While using a static HttpClient is better than creating new instances per request, consider injecting IHttpClientFactory for better lifecycle management and resilience.

-private static readonly HttpClient HttpClient = new();
+private readonly IHttpClientFactory _httpClientFactory;

+public PluginsManager(PluginInitContext context, Settings settings, IHttpClientFactory httpClientFactory)
+{
+    Context = context;
+    Settings = settings;
+    _httpClientFactory = httpClientFactory;
+}

170-176: Fix typo in comment.

There's a typo in the comment: "expcetion" should be "exception".

-                                    // when reportProgress is null, it means there is expcetion with the progress box
+                                    // when reportProgress is null, it means there is exception with the progress box

207-211: Consider using exponential backoff for redownload.

When redownloading after an exception, consider implementing exponential backoff to handle temporary network issues.

if (exceptionHappened && (!downloadCancelled))
-    await Http.DownloadAsync(plugin.UrlDownload, filePath).ConfigureAwait(false);
+{
+    var policy = Policy
+        .Handle<HttpRequestException>()
+        .WaitAndRetryAsync(3, retryAttempt => 
+            TimeSpan.FromSeconds(Math.Pow(2, retryAttempt)));
+    
+    await policy.ExecuteAsync(async () =>
+        await Http.DownloadAsync(plugin.UrlDownload, filePath).ConfigureAwait(false));
+}

Line range hint 411-483: Consider chunking parallel updates.

When updating multiple plugins in parallel, consider chunking them into smaller batches to avoid overwhelming the system.

-await Task.WhenAll(resultsForUpdate.Select(async plugin =>
+const int batchSize = 3;
+var pluginBatches = resultsForUpdate
+    .Select((plugin, index) => new { plugin, index })
+    .GroupBy(x => x.index / batchSize)
+    .Select(g => g.Select(x => x.plugin));
+
+foreach (var batch in pluginBatches)
+{
+    await Task.WhenAll(batch.Select(async plugin =>
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 297d191 and cc921c7.

📒 Files selected for processing (1)
  • Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs (7 hunks)
🔇 Additional comments (5)
Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs (5)

147-148: LGTM: Proper cancellation handling.

Good implementation of cancellation using CancellationTokenSource and download cancellation flag.

Also applies to: 156-157


161-163: LGTM: Robust content length handling.

Good practice to handle cases where content length is not available (-1).


222-226: LGTM: Proper cancellation check before installation.

Good practice to check for cancellation before proceeding with installation.


619-619: LGTM: Method renamed to reflect async nature.

Good practice to append 'Async' suffix to asynchronous methods.


186-189: 🛠️ Refactor suggestion

Pass CancellationToken to async operations.

ReadAsync and WriteAsync operations should use the cancellation token for proper cancellation support.

-while ((read = await contentStream.ReadAsync(buffer).ConfigureAwait(false)) > 0)
-{
-    await fileStream.WriteAsync(buffer.AsMemory(0, read)).ConfigureAwait(false);
+while ((read = await contentStream.ReadAsync(buffer, cts.Token).ConfigureAwait(false)) > 0)
+{
+    await fileStream.WriteAsync(buffer.AsMemory(0, read), cts.Token).ConfigureAwait(false);

Likely invalid or redundant comment.

@Jack251970 Jack251970 requested a review from taooceros January 10, 2025 11:25
@taooceros
Copy link
Member

I don't really understand sorry. Why does delegate cannot handle that? I think we can use some special value (like -1) to represent error on the delegate so I don't really think that's a problem?

@taooceros Sorry, it seems that I encountered the thread issue last time. Now update and please check.

Totally understand the thread issue. Maybe you can make the ProgressBar MVVM so we didn't modify the UI state directly but rather updating the view via the property (which by WPF spec is thread safe)?

@taooceros
Copy link
Member

As the stackoverflow indicated, could you make the ProgressReport part an extension method of Http (maybe inside the Http class). That would also allow other parts of code to utilize this feature if they wants. Then interact the progress report with the ProgressWindow via delegate or something instead of handling the stream itself, which makes this part of code too complicated.

This comment has been minimized.

This comment has been minimized.

@Jack251970
Copy link
Contributor Author

As the stackoverflow indicated, could you make the ProgressReport part an extension method of Http (maybe inside the Http class). That would also allow other parts of code to utilize this feature if they wants. Then interact the progress report with the ProgressWindow via delegate or something instead of handling the stream itself, which makes this part of code too complicated.

I see. I improve the DownloadUrl api funcion. Please check.

@Jack251970 Jack251970 changed the title Add New API for ProgressBoxEx to Show Progress & Add Progress Display for Plugin Downloading Add New API for ProgressBoxEx to Show Progress & Add Progress Display for Plugin Downloading & Improve DownloadUrl Api Function Jan 11, 2025
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (7)
Flow.Launcher.Infrastructure/Http/Http.cs (5)

102-106: Consider using a larger buffer size for better performance.

The current 8KB buffer size might not be optimal for large downloads. Consider increasing it to 81920 (80KB) which is commonly used for file operations.

-        var buffer = new byte[8192];
+        var buffer = new byte[81920];

114-120: Potential for progress reporting optimization.

The current implementation reports progress on every chunk read, which could be excessive for fast downloads. Consider adding a throttle to reduce UI updates.

+            const double progressThreshold = 1.0; // Report only 1% changes
             progressValue = totalRead * 100.0 / totalBytes;
+            var newProgress = Math.Floor(progressValue);
+            if (Math.Abs(newProgress - lastReportedProgress) >= progressThreshold)
+            {
                 if (token.IsCancellationRequested)
                     return;
                 else
-                    reportProgress(progressValue);
+                {
+                    reportProgress(newProgress);
+                    lastReportedProgress = newProgress;
+                }
+            }

122-123: Ensure final progress is reported only if not already at 100%.

The current implementation might unnecessarily report 100% progress even if it was already reported in the last chunk.

-        if (progressValue < 100)
+        if (Math.Floor(progressValue) < 100)
             reportProgress(100);

127-128: Add buffer size parameter to CopyToAsync for consistency.

When falling back to the non-progress path, specify the same buffer size for consistency with the progress reporting path.

-        await response.Content.CopyToAsync(fileStream, token);
+        await response.Content.CopyToAsync(fileStream, token, 81920);

86-129: Consider implementing download resume capability.

For large plugin downloads, it would be beneficial to support resuming interrupted downloads. This could be implemented by:

  1. Checking for existing partial downloads
  2. Using HTTP Range headers
  3. Appending to existing files when possible

Would you like me to provide a detailed implementation for this feature?

Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs (2)

172-175: Consider adding a retry limit.

The current implementation retries the download once if an exception occurs. Consider adding a retry limit and exponential backoff to prevent infinite retries.

+private const int MaxRetries = 3;
+private static readonly TimeSpan InitialRetryDelay = TimeSpan.FromSeconds(1);

 // if exception happened while downloading and user does not cancel downloading,
 // we need to redownload the plugin
-if (exceptionHappened && (!cts.IsCancellationRequested))
-    await Http.DownloadAsync(plugin.UrlDownload, filePath, null, cts.Token).ConfigureAwait(false);
+if (exceptionHappened && (!cts.IsCancellationRequested))
+{
+    for (int retry = 0; retry < MaxRetries; retry++)
+    {
+        try
+        {
+            await Task.Delay(TimeSpan.FromSeconds(Math.Pow(2, retry)) * InitialRetryDelay, cts.Token);
+            await Http.DownloadAsync(plugin.UrlDownload, filePath, null, cts.Token).ConfigureAwait(false);
+            break;
+        }
+        catch (Exception) when (retry < MaxRetries - 1)
+        {
+            continue;
+        }
+    }
+}

190-207: Consider adding more specific exception handling.

The current implementation catches general exceptions which might mask specific issues. Consider adding separate catch blocks for common exceptions like IOException, UnauthorizedAccessException, etc.

 catch (HttpRequestException e)
 {
     Context.API.ShowMsgError(
         string.Format(Context.API.GetTranslation("plugin_pluginsmanager_downloading_plugin"), plugin.Name),
         Context.API.GetTranslation("plugin_pluginsmanager_download_error"));
     Log.Exception("PluginsManager", "An error occurred while downloading plugin", e);
     return;
 }
+catch (IOException e)
+{
+    Context.API.ShowMsgError(
+        Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"),
+        string.Format(Context.API.GetTranslation("plugin_pluginsmanager_io_error"), plugin.Name));
+    Log.Exception("PluginsManager", "An I/O error occurred while handling plugin", e);
+    return;
+}
+catch (UnauthorizedAccessException e)
+{
+    Context.API.ShowMsgError(
+        Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"),
+        string.Format(Context.API.GetTranslation("plugin_pluginsmanager_access_error"), plugin.Name));
+    Log.Exception("PluginsManager", "Access denied while handling plugin", e);
+    return;
+}
 catch (Exception e)
 {
     Context.API.ShowMsgError(
         Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"),
         string.Format(Context.API.GetTranslation("plugin_pluginsmanager_install_error_subtitle"), plugin.Name));
     Log.Exception("PluginsManager", "An error occurred while downloading plugin", e);
     return;
 }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between df3cb58 and 8eb5a4d.

📒 Files selected for processing (6)
  • Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs (1 hunks)
  • Flow.Launcher.Infrastructure/Http/Http.cs (1 hunks)
  • Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs (3 hunks)
  • Flow.Launcher/ProgressBoxEx.xaml.cs (1 hunks)
  • Flow.Launcher/PublicAPIInstance.cs (2 hunks)
  • Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs (7 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • Flow.Launcher/PublicAPIInstance.cs
  • Flow.Launcher.Plugin/Interfaces/IPublicAPI.cs
  • Flow.Launcher/ProgressBoxEx.xaml.cs
🔇 Additional comments (4)
Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs (1)

123-126: LGTM! Clean implementation of progress reporting.

The updated HttpDownloadAsync method signature properly integrates progress reporting while maintaining backward compatibility with the optional reportProgress parameter.

Flow.Launcher.Infrastructure/Http/Http.cs (1)

94-96: Good practice checking content length availability.

The check for content length availability before attempting progress reporting helps prevent issues with servers that don't provide this information.

Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs (2)

20-20: LGTM! Improved encapsulation.

Making the zip constant private properly restricts its access scope to this class.


579-579: Verify all callers of the renamed method.

The method has been renamed from RequestInstallOrUpdate to RequestInstallOrUpdateAsync to better reflect its asynchronous nature. This is a breaking change that requires verification of all callers.

✅ Verification successful

Method rename verification completed successfully

All callers have been updated to use the new async method name. The method is internal and has no interface or inheritance dependencies.

🏁 Scripts executed

The following scripts were executed for the analysis:

Script:

#!/bin/bash
# Search for any remaining calls to the old method name
rg "RequestInstallOrUpdate[^A]" --type cs

Length of output: 43


Script:

#!/bin/bash
# Search for the new async method name usage
rg "RequestInstallOrUpdateAsync" --type cs -A 2

# Check if this is part of an interface or has derived classes
ast-grep --pattern 'interface $_ { $$$ RequestInstallOrUpdate$$$ }'

# Look for class inheritance
ast-grep --pattern 'class $_ : PluginsManager { $$$ }'

Length of output: 1062

Flow.Launcher.Infrastructure/Http/Http.cs Outdated Show resolved Hide resolved

This comment has been minimized.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
Flow.Launcher.Infrastructure/Http/Http.cs (2)

86-95: Consider using IProgress<T> instead of Action<double>.

While Action<double> works, IProgress<T> is the recommended pattern for progress reporting in .NET as it:

  • Handles thread synchronization automatically
  • Provides better error handling
  • Supports cancellation more elegantly
  • Is more testable
-        public static async Task DownloadAsync([NotNull] string url, [NotNull] string filePath, Action<double> reportProgress = null, CancellationToken token = default)
+        public static async Task DownloadAsync([NotNull] string url, [NotNull] string filePath, IProgress<double> progress = null, CancellationToken token = default)

97-124: Optimize progress reporting frequency.

The current implementation reports progress after every chunk, which could lead to excessive UI updates for large files. Consider implementing a throttling mechanism to reduce the frequency of progress updates.

         if (canReportProgress && reportProgress != null)
         {
             await using var contentStream = await response.Content.ReadAsStreamAsync(token);
             await using var fileStream = new FileStream(filePath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 8192, true);

             var buffer = new byte[8192];
             long totalRead = 0;
             int read;
             double progressValue = 0;
+            double lastReportedProgress = 0;
+            const double progressThreshold = 1.0; // Report only when progress changes by 1%

             reportProgress(0);

             while ((read = await contentStream.ReadAsync(buffer, token)) > 0)
             {
                 await fileStream.WriteAsync(buffer.AsMemory(0, read), token);
                 totalRead += read;

                 progressValue = totalRead * 100.0 / totalBytes;

                 if (token.IsCancellationRequested)
                     return;
-                else
+                else if (progressValue - lastReportedProgress >= progressThreshold)
+                {
                     reportProgress(progressValue);
+                    lastReportedProgress = progressValue;
+                }
             }

             if (progressValue < 100)
                 reportProgress(100);
         }
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8eb5a4d and 1bf045f.

📒 Files selected for processing (1)
  • Flow.Launcher.Infrastructure/Http/Http.cs (1 hunks)
🔇 Additional comments (1)
Flow.Launcher.Infrastructure/Http/Http.cs (1)

125-129: LGTM! Consistent file handling between progress and non-progress paths.

The implementation correctly uses FileMode.CreateNew in both paths, addressing the previous review comment about FileMode inconsistency.

Flow.Launcher.Infrastructure/Http/Http.cs Show resolved Hide resolved

This comment has been minimized.

Copy link

@check-spelling-bot Report

🔴 Please review

See the 📂 files view, the 📜action log, or 📝 job summary for details.

❌ Errors Count
❌ forbidden-pattern 22
⚠️ ignored-expect-variant 1
⚠️ non-alpha-in-dictionary 19

See ❌ Event descriptions for more information.

If the flagged items are 🤯 false positives

If items relate to a ...

  • binary file (or some other file you wouldn't want to check at all).

    Please add a file path to the excludes.txt file matching the containing file.

    File paths are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your files.

    ^ refers to the file's path from the root of the repository, so ^README\.md$ would exclude README.md (on whichever branch you're using).

  • well-formed pattern.

    If you can write a pattern that would match it,
    try adding it to the patterns.txt file.

    Patterns are Perl 5 Regular Expressions - you can test yours before committing to verify it will match your lines.

    Note that patterns can't match multiline strings.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs (4)

147-164: Enhance cancellation handling with proper cleanup.

While the cancellation token implementation is good, we should ensure proper cleanup of resources when cancellation is requested.

 if (cts.IsCancellationRequested)
+{
+    if (File.Exists(filePath))
+        File.Delete(filePath);
     return;
+}

168-183: Improve error handling with specific exception types.

The error handling is good but could be more specific. Consider catching UnauthorizedAccessException separately for file system operations.

+catch (UnauthorizedAccessException e)
+{
+    Context.API.ShowMsgError(
+        Context.API.GetTranslation("plugin_pluginsmanager_install_error_title"),
+        Context.API.GetTranslation("plugin_pluginsmanager_install_error_access_denied"));
+    Log.Exception("PluginsManager", "Access denied while downloading plugin", e);
+    return;
+}
 catch (Exception e)

202-228: Consider retry mechanism for failed downloads.

The method handles exceptions well but could benefit from a retry mechanism for transient network issues.

 private async Task DeleteFileAndDownloadMsgBoxAsync(string prgBoxTitle, string downloadUrl, string filePath, CancellationTokenSource cts)
 {
+    const int maxRetries = 3;
+    const int delayMs = 1000;
+    
     if (File.Exists(filePath))
         File.Delete(filePath);

     var exceptionHappened = false;
+    var retryCount = 0;
+    
+    while (retryCount < maxRetries)
+    {
         await Context.API.ShowProgressBoxAsync(prgBoxTitle,
             async (reportProgress) =>
             {
                 if (reportProgress == null)
                 {
                     exceptionHappened = true;
                     return;
                 }
                 else
                 {
                     await Context.API.HttpDownloadAsync(downloadUrl, filePath, reportProgress, cts.Token).ConfigureAwait(false);
                 }
             }, cts.Cancel);

         if (!exceptionHappened || cts.IsCancellationRequested)
             break;
             
+        retryCount++;
+        if (retryCount < maxRetries)
+            await Task.Delay(delayMs * retryCount, cts.Token);
+    }
-    if (exceptionHappened && (!cts.IsCancellationRequested))
-        await Context.API.HttpDownloadAsync(downloadUrl, filePath).ConfigureAwait(false);
 }

419-430: Consider parallel download limit for update all.

When updating multiple plugins simultaneously, consider implementing a semaphore to limit parallel downloads.

+private static SemaphoreSlim _downloadSemaphore = new SemaphoreSlim(3); // Limit to 3 concurrent downloads

 await Task.WhenAll(resultsForUpdate.Select(async plugin =>
 {
     var downloadToFilePath = Path.Combine(Path.GetTempPath(),
         $"{plugin.Name}-{plugin.NewVersion}.zip");

     try
     {
+        await _downloadSemaphore.WaitAsync();
         using var cts = new CancellationTokenSource();
         await DeleteFileAndDownloadMsgBoxAsync(...);
+        _downloadSemaphore.Release();
         
         if (cts.IsCancellationRequested)
             return;
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1bf045f and c32435f.

📒 Files selected for processing (1)
  • Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs (10 hunks)
🔇 Additional comments (3)
Plugins/Flow.Launcher.Plugin.PluginsManager/PluginsManager.cs (3)

20-20: LGTM! Access modifier change improves encapsulation.

The change from const to private const for the zip variable appropriately restricts access.


317-358: LGTM! Good implementation of progress reporting and cancellation.

The update functionality properly handles progress reporting and cancellation.


369-369: ⚠️ Potential issue

Handle potential null exception in continuation task.

The continuation task assumes t.Exception.InnerException is not null, which might not always be true.

-Log.Exception("PluginsManager", $"Update failed for {x.Name}", t.Exception.InnerException);
+Log.Exception("PluginsManager", $"Update failed for {x.Name}", t.Exception?.InnerException ?? t.Exception);

Likely invalid or redundant comment.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
20 min review enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants