diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs index ffba6ce61..e3efcd296 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs @@ -1,33 +1,27 @@ -using Accessibility; -using Flow.Launcher.Core.Resource; +using Flow.Launcher.Core.Resource; using Flow.Launcher.Infrastructure; using System; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; -using System.Reflection; +using System.Text; using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; -using ICSharpCode.SharpZipLib.Zip; -using JetBrains.Annotations; using Microsoft.IO; -using System.Text.Json.Serialization; using System.Windows; using System.Windows.Controls; using YamlDotNet.Serialization; using YamlDotNet.Serialization.NamingConventions; using CheckBox = System.Windows.Controls.CheckBox; using Control = System.Windows.Controls.Control; -using Label = System.Windows.Controls.Label; using Orientation = System.Windows.Controls.Orientation; using TextBox = System.Windows.Controls.TextBox; using UserControl = System.Windows.Controls.UserControl; -using System.Windows.Data; namespace Flow.Launcher.Core.Plugin { @@ -69,7 +63,7 @@ namespace Flow.Launcher.Core.Plugin private static readonly JsonSerializerOptions options = new() { PropertyNameCaseInsensitive = true, -#pragma warning disable SYSLIB0020 +#pragma warning disable SYSLIB0020 // IgnoreNullValues is obsolete, but the replacement JsonIgnoreCondition.WhenWritingNull still // deserializes null, instead of ignoring it and leaving the default (empty list). We can change the behaviour // to accept null and fallback to a default etc, or just keep IgnoreNullValues for now @@ -92,12 +86,15 @@ namespace Flow.Launcher.Core.Plugin private async Task> DeserializedResultAsync(Stream output) { - if (output == Stream.Null) return null; + await using (output) + { + if (output == Stream.Null) return null; - var queryResponseModel = - await JsonSerializer.DeserializeAsync(output, options); + var queryResponseModel = + await JsonSerializer.DeserializeAsync(output, options); - return ParseResults(queryResponseModel); + return ParseResults(queryResponseModel); + } } private List DeserializedResult(string output) @@ -139,7 +136,7 @@ namespace Flow.Launcher.Core.Plugin } else { - var actionResponse = await RequestAsync(result.JsonRPCAction); + await using var actionResponse = await RequestAsync(result.JsonRPCAction); if (actionResponse.Length == 0) { @@ -247,74 +244,55 @@ namespace Flow.Launcher.Core.Plugin protected async Task ExecuteAsync(ProcessStartInfo startInfo, CancellationToken token = default) { - Process process = null; - using var exitTokenSource = new CancellationTokenSource(); + using var process = Process.Start(startInfo); + if (process == null) + { + Log.Error("|JsonRPCPlugin.ExecuteAsync|Can't start new process"); + return Stream.Null; + } + + var sourceBuffer = BufferManager.GetStream(); + using var errorBuffer = BufferManager.GetStream(); + + var sourceCopyTask = process.StandardOutput.BaseStream.CopyToAsync(sourceBuffer, token); + var errorCopyTask = process.StandardError.BaseStream.CopyToAsync(errorBuffer, token); + + await using var registeredEvent = token.Register(() => + { + if (!process.HasExited) + process.Kill(); + sourceBuffer.Dispose(); + }); + try { - process = Process.Start(startInfo); - if (process == null) - { - Log.Error("|JsonRPCPlugin.ExecuteAsync|Can't start new process"); - return Stream.Null; - } - - - await using var source = process.StandardOutput.BaseStream; - - var buffer = BufferManager.GetStream(); - - token.Register(() => - { - // ReSharper disable once AccessToModifiedClosure - // Manually Check whether disposed - if (!exitTokenSource.IsCancellationRequested && !process.HasExited) - process.Kill(); - }); - - try - { - // token expire won't instantly trigger the exception, - // manually kill process at before - await source.CopyToAsync(buffer, token); - } - catch (OperationCanceledException) - { - await buffer.DisposeAsync(); - return Stream.Null; - } - - buffer.Seek(0, SeekOrigin.Begin); - - token.ThrowIfCancellationRequested(); - - if (buffer.Length == 0) - { - var errorMessage = process.StandardError.EndOfStream ? - "Empty JSONRPC Response" : - await process.StandardError.ReadToEndAsync(); - throw new InvalidDataException($"{context.CurrentPluginMetadata.Name}|{errorMessage}"); - } - - if (!process.StandardError.EndOfStream) - { - using var standardError = process.StandardError; - var error = await standardError.ReadToEndAsync(); - - if (!string.IsNullOrEmpty(error)) - { - Log.Error($"|{context.CurrentPluginMetadata.Name}.{nameof(ExecuteAsync)}|{error}"); - } - } - - return buffer; + // token expire won't instantly trigger the exception, + // manually kill process at before + await process.WaitForExitAsync(token); + await Task.WhenAll(sourceCopyTask, errorCopyTask); } - finally + catch (OperationCanceledException) { - exitTokenSource.Cancel(); - process?.Dispose(); + await sourceBuffer.DisposeAsync(); + return Stream.Null; } + + switch (sourceBuffer.Length, errorBuffer.Length) + { + case (0, 0): + const string errorMessage = "Empty JSON-RPC Response."; + Log.Warn($"|{nameof(JsonRPCPlugin)}.{nameof(ExecuteAsync)}|{errorMessage}"); + break; + case (_, not 0): + throw new InvalidDataException(Encoding.UTF8.GetString(errorBuffer.ToArray())); // The process has exited with an error message + } + + sourceBuffer.Seek(0, SeekOrigin.Begin); + + return sourceBuffer; } + public async Task> QueryAsync(Query query, CancellationToken token) { var request = new JsonRPCRequestModel @@ -366,6 +344,7 @@ namespace Flow.Launcher.Core.Plugin private static readonly Thickness settingPanelMargin = new(15, 20, 15, 20); private static readonly Thickness settingTextBlockMargin = new(10, 4, 10, 4); private JsonRpcConfigurationModel _settingsTemplate; + public Control CreateSettingPanel() { if (Settings == null) @@ -397,84 +376,84 @@ namespace Flow.Launcher.Core.Plugin switch (type) { case "textBlock": + { + contentControl = new TextBlock { - contentControl = new TextBlock - { - Text = attribute.Description.Replace("\\r\\n", "\r\n"), - Margin = settingTextBlockMargin, - MaxWidth = 500, - TextWrapping = TextWrapping.WrapWithOverflow - }; - break; - } + Text = attribute.Description.Replace("\\r\\n", "\r\n"), + Margin = settingTextBlockMargin, + MaxWidth = 500, + TextWrapping = TextWrapping.WrapWithOverflow + }; + break; + } case "input": + { + var textBox = new TextBox() { - var textBox = new TextBox() - { - Width = 300, - Text = Settings[attribute.Name] as string ?? string.Empty, - Margin = settingControlMargin, - ToolTip = attribute.Description - }; - textBox.TextChanged += (_, _) => - { - Settings[attribute.Name] = textBox.Text; - }; - contentControl = textBox; - break; - } + Width = 300, + Text = Settings[attribute.Name] as string ?? string.Empty, + Margin = settingControlMargin, + ToolTip = attribute.Description + }; + textBox.TextChanged += (_, _) => + { + Settings[attribute.Name] = textBox.Text; + }; + contentControl = textBox; + break; + } case "textarea": + { + var textBox = new TextBox() { - var textBox = new TextBox() - { - Width = 300, - Height = 120, - Margin = settingControlMargin, - TextWrapping = TextWrapping.WrapWithOverflow, - AcceptsReturn = true, - Text = Settings[attribute.Name] as string ?? string.Empty, - ToolTip = attribute.Description - }; - textBox.TextChanged += (sender, _) => - { - Settings[attribute.Name] = ((TextBox)sender).Text; - }; - contentControl = textBox; - break; - } + Width = 300, + Height = 120, + Margin = settingControlMargin, + TextWrapping = TextWrapping.WrapWithOverflow, + AcceptsReturn = true, + Text = Settings[attribute.Name] as string ?? string.Empty, + ToolTip = attribute.Description + }; + textBox.TextChanged += (sender, _) => + { + Settings[attribute.Name] = ((TextBox)sender).Text; + }; + contentControl = textBox; + break; + } case "passwordBox": + { + var passwordBox = new PasswordBox() { - var passwordBox = new PasswordBox() - { - Width = 300, - Margin = settingControlMargin, - Password = Settings[attribute.Name] as string ?? string.Empty, - PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar, - ToolTip = attribute.Description - }; - passwordBox.PasswordChanged += (sender, _) => - { - Settings[attribute.Name] = ((PasswordBox)sender).Password; - }; - contentControl = passwordBox; - break; - } + Width = 300, + Margin = settingControlMargin, + Password = Settings[attribute.Name] as string ?? string.Empty, + PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar, + ToolTip = attribute.Description + }; + passwordBox.PasswordChanged += (sender, _) => + { + Settings[attribute.Name] = ((PasswordBox)sender).Password; + }; + contentControl = passwordBox; + break; + } case "dropdown": + { + var comboBox = new ComboBox() { - var comboBox = new ComboBox() - { - ItemsSource = attribute.Options, - SelectedItem = Settings[attribute.Name], - Margin = settingControlMargin, - ToolTip = attribute.Description - }; - comboBox.SelectionChanged += (sender, _) => - { - Settings[attribute.Name] = (string)((ComboBox)sender).SelectedItem; - }; - contentControl = comboBox; - break; - } + ItemsSource = attribute.Options, + SelectedItem = Settings[attribute.Name], + Margin = settingControlMargin, + ToolTip = attribute.Description + }; + comboBox.SelectionChanged += (sender, _) => + { + Settings[attribute.Name] = (string)((ComboBox)sender).SelectedItem; + }; + contentControl = comboBox; + break; + } case "checkbox": var checkBox = new CheckBox { @@ -499,6 +478,7 @@ namespace Flow.Launcher.Core.Plugin } return settingWindow; } + public void Save() { if (Settings != null) @@ -541,4 +521,5 @@ namespace Flow.Launcher.Core.Plugin } } } + } diff --git a/Flow.Launcher.Plugin/Result.cs b/Flow.Launcher.Plugin/Result.cs index 35c491d35..136395467 100644 --- a/Flow.Launcher.Plugin/Result.cs +++ b/Flow.Launcher.Plugin/Result.cs @@ -197,5 +197,16 @@ namespace Flow.Launcher.Plugin { return AsyncAction?.Invoke(context) ?? ValueTask.FromResult(Action?.Invoke(context) ?? false); } + + /// + /// Progress bar display. Providing an int value between 0-100 will trigger the progress bar to be displayed on the result + /// + public int? ProgressBar { get; set; } + + /// + /// Optionally set the color of the progress bar + /// + /// #26a0da (blue) + public string ProgressBarColor { get; set; } = "#26a0da"; } } diff --git a/Flow.Launcher/ReportWindow.xaml.cs b/Flow.Launcher/ReportWindow.xaml.cs index 72c69d74d..4899edc14 100644 --- a/Flow.Launcher/ReportWindow.xaml.cs +++ b/Flow.Launcher/ReportWindow.xaml.cs @@ -77,7 +77,6 @@ namespace Flow.Launcher }; link.Inlines.Add(url); link.NavigateUri = new Uri(url); - link.RequestNavigate += (s, e) => SearchWeb.OpenInBrowserTab(e.Uri.ToString()); link.Click += (s, e) => SearchWeb.OpenInBrowserTab(url); paragraph.Inlines.Add(textBeforeUrl); diff --git a/Flow.Launcher/ResultListBox.xaml b/Flow.Launcher/ResultListBox.xaml index 2a7f9c494..2c78f9bab 100644 --- a/Flow.Launcher/ResultListBox.xaml +++ b/Flow.Launcher/ResultListBox.xaml @@ -30,7 +30,7 @@ -