diff --git a/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs b/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs index 6a96a94b7..13b6ac968 100644 --- a/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs +++ b/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs @@ -1,5 +1,8 @@ using System; using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; using Flow.Launcher.Plugin; namespace Flow.Launcher.Core.Plugin @@ -21,17 +24,17 @@ namespace Flow.Launcher.Core.Plugin }; } - protected override string ExecuteQuery(Query query) + protected override Task ExecuteQueryAsync(Query query, CancellationToken token) { JsonRPCServerRequestModel request = new JsonRPCServerRequestModel { Method = "query", - Parameters = new object[] { query.Search }, + Parameters = new object[] {query.Search}, }; _startInfo.Arguments = $"\"{request}\""; - return Execute(_startInfo); + return ExecuteAsync(_startInfo, token); } protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest) @@ -40,10 +43,12 @@ namespace Flow.Launcher.Core.Plugin return Execute(_startInfo); } - protected override string ExecuteContextMenu(Result selectedResult) { - JsonRPCServerRequestModel request = new JsonRPCServerRequestModel { + protected override string ExecuteContextMenu(Result selectedResult) + { + JsonRPCServerRequestModel request = new JsonRPCServerRequestModel + { Method = "contextmenu", - Parameters = new object[] { selectedResult.ContextData }, + Parameters = new object[] {selectedResult.ContextData}, }; _startInfo.Arguments = $"\"{request}\""; diff --git a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs index 247fe1889..b1c8ff6ea 100644 --- a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs +++ b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs @@ -45,6 +45,8 @@ namespace Flow.Launcher.Core.Plugin { [JsonPropertyName("result")] public new List Result { get; set; } + + public string DebugMessage { get; set; } } public class JsonRPCRequestModel : JsonRPCModelBase @@ -58,13 +60,12 @@ namespace Flow.Launcher.Core.Plugin string rpc = string.Empty; if (Parameters != null && Parameters.Length > 0) { - string parameters = Parameters.Aggregate("[", (current, o) => current + (GetParameterByType(o) + ",")); - parameters = parameters.Substring(0, parameters.Length - 1) + "]"; - rpc = string.Format(@"{{\""method\"":\""{0}\"",\""parameters\"":{1}", Method, parameters); + string parameters = $"[{string.Join(',', Parameters.Select(GetParameterByType))}]"; + rpc = $@"{{\""method\"":\""{Method}\"",\""parameters\"":{parameters}"; } else { - rpc = string.Format(@"{{\""method\"":\""{0}\"",\""parameters\"":[]", Method); + rpc = $@"{{\""method\"":\""{Method}\"",\""parameters\"":[]"; } return rpc; @@ -72,26 +73,16 @@ namespace Flow.Launcher.Core.Plugin } private string GetParameterByType(object parameter) + => parameter switch { - if (parameter == null) { - return "null"; - } - if (parameter is string) - { - return string.Format(@"\""{0}\""", ReplaceEscapes(parameter.ToString())); - } - if (parameter is int || parameter is float || parameter is double) - { - return string.Format(@"{0}", parameter); - } - if (parameter is bool) - { - return string.Format(@"{0}", parameter.ToString().ToLower()); - } - return parameter.ToString(); - } + null => "null", + string _ => $@"\""{ReplaceEscapes(parameter.ToString())}\""", + bool _ => $@"{parameter.ToString().ToLower()}", + _ => parameter.ToString() + }; - private string ReplaceEscapes(string str) + + private string ReplaceEscapes(string str) { return str.Replace(@"\", @"\\") //Escapes in ProcessStartInfo .Replace(@"\", @"\\") //Escapes itself when passed to client diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs index c7ad70391..7a088bd09 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs @@ -7,9 +7,9 @@ using System.Text.Json; using System.Threading; using System.Threading.Tasks; using System.Windows.Forms; -using Flow.Launcher.Infrastructure.Exception; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Plugin; +using JetBrains.Annotations; namespace Flow.Launcher.Core.Plugin { @@ -17,7 +17,7 @@ namespace Flow.Launcher.Core.Plugin /// Represent the plugin that using JsonPRC /// every JsonRPC plugin should has its own plugin instance /// - internal abstract class JsonRPCPlugin : IPlugin, IContextMenu + internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu { protected PluginInitContext context; public const string JsonRPC = "JsonRPC"; @@ -27,24 +27,10 @@ namespace Flow.Launcher.Core.Plugin /// public abstract string SupportedLanguage { get; set; } - protected abstract string ExecuteQuery(Query query); + protected abstract Task ExecuteQueryAsync(Query query, CancellationToken token); protected abstract string ExecuteCallback(JsonRPCRequestModel rpcRequest); protected abstract string ExecuteContextMenu(Result selectedResult); - public List Query(Query query) - { - string output = ExecuteQuery(query); - try - { - return DeserializedResult(output); - } - catch (Exception e) - { - Log.Exception($"|JsonRPCPlugin.Query|Exception when query <{query}>", e); - return null; - } - } - public List LoadContextMenus(Result selectedResult) { string output = ExecuteContextMenu(selectedResult); @@ -59,50 +45,71 @@ namespace Flow.Launcher.Core.Plugin } } + + + private async Task> DeserializedResultAsync(Stream output) + { + if (output == Stream.Null) return null; + + JsonRPCQueryResponseModel queryResponseModel = await + JsonSerializer.DeserializeAsync(output); + + return ParseResults(queryResponseModel); + } + private List DeserializedResult(string output) { - if (!String.IsNullOrEmpty(output)) + if (string.IsNullOrEmpty(output)) return null; + + JsonRPCQueryResponseModel queryResponseModel = + JsonSerializer.Deserialize(output); + return ParseResults(queryResponseModel); + } + + private List ParseResults(JsonRPCQueryResponseModel queryResponseModel) + { + var results = new List(); + if (queryResponseModel.Result == null) return null; + + if(!string.IsNullOrEmpty(queryResponseModel.DebugMessage)) { - List results = new List(); + context.API.ShowMsg(queryResponseModel.DebugMessage); + } - JsonRPCQueryResponseModel queryResponseModel = JsonSerializer.Deserialize(output); - if (queryResponseModel.Result == null) return null; - - foreach (JsonRPCResult result in queryResponseModel.Result) + foreach (JsonRPCResult result in queryResponseModel.Result) + { + result.Action = c => { - JsonRPCResult result1 = result; - result.Action = c => - { - if (result1.JsonRPCAction == null) return false; + if (result.JsonRPCAction == null) return false; - if (!String.IsNullOrEmpty(result1.JsonRPCAction.Method)) + if (!string.IsNullOrEmpty(result.JsonRPCAction.Method)) + { + if (result.JsonRPCAction.Method.StartsWith("Flow.Launcher.")) { - if (result1.JsonRPCAction.Method.StartsWith("Flow.Launcher.")) + ExecuteFlowLauncherAPI(result.JsonRPCAction.Method.Substring(4), + result.JsonRPCAction.Parameters); + } + else + { + string actionReponse = ExecuteCallback(result.JsonRPCAction); + JsonRPCRequestModel jsonRpcRequestModel = + JsonSerializer.Deserialize(actionReponse); + if (jsonRpcRequestModel != null + && !string.IsNullOrEmpty(jsonRpcRequestModel.Method) + && jsonRpcRequestModel.Method.StartsWith("Flow.Launcher.")) { - ExecuteFlowLauncherAPI(result1.JsonRPCAction.Method.Substring(4), result1.JsonRPCAction.Parameters); - } - else - { - string actionReponse = ExecuteCallback(result1.JsonRPCAction); - JsonRPCRequestModel jsonRpcRequestModel = JsonSerializer.Deserialize(actionReponse); - if (jsonRpcRequestModel != null - && !String.IsNullOrEmpty(jsonRpcRequestModel.Method) - && jsonRpcRequestModel.Method.StartsWith("Flow.Launcher.")) - { - ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method.Substring(4), jsonRpcRequestModel.Parameters); - } + ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method.Substring(4), + jsonRpcRequestModel.Parameters); } } - return !result1.JsonRPCAction.DontHideAfterAction; - }; - results.Add(result); - } - return results; - } - else - { - return null; + } + + return !result.JsonRPCAction.DontHideAfterAction; + }; + results.Add(result); } + + return results; } private void ExecuteFlowLauncherAPI(string method, object[] parameters) @@ -117,9 +124,7 @@ namespace Flow.Launcher.Core.Plugin catch (Exception) { #if (DEBUG) - { - throw; - } + throw; #endif } } @@ -130,17 +135,20 @@ namespace Flow.Launcher.Core.Plugin /// /// /// + /// Cancellation Token /// - protected string Execute(string fileName, string arguments) + protected Task ExecuteAsync(string fileName, string arguments, CancellationToken token = default) { - ProcessStartInfo start = new ProcessStartInfo(); - start.FileName = fileName; - start.Arguments = arguments; - start.UseShellExecute = false; - start.CreateNoWindow = true; - start.RedirectStandardOutput = true; - start.RedirectStandardError = true; - return Execute(start); + ProcessStartInfo start = new ProcessStartInfo + { + FileName = fileName, + Arguments = arguments, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + return ExecuteAsync(start, token); } protected string Execute(ProcessStartInfo startInfo) @@ -148,52 +156,102 @@ namespace Flow.Launcher.Core.Plugin try { using var process = Process.Start(startInfo); - if (process == null) - { - Log.Error("|JsonRPCPlugin.Execute|Can't start new process"); - return string.Empty; - } + if (process == null) return string.Empty; using var standardOutput = process.StandardOutput; var result = standardOutput.ReadToEnd(); + if (string.IsNullOrEmpty(result)) { - using (var standardError = process.StandardError) + using var standardError = process.StandardError; + var error = standardError.ReadToEnd(); + if (!string.IsNullOrEmpty(error)) { - var error = standardError.ReadToEnd(); - if (!string.IsNullOrEmpty(error)) - { - Log.Error($"|JsonRPCPlugin.Execute|{error}"); - return string.Empty; - } - else - { - Log.Error("|JsonRPCPlugin.Execute|Empty standard output and standard error."); - return string.Empty; - } + Log.Error($"|JsonRPCPlugin.Execute|{error}"); + return string.Empty; } + + Log.Error("|JsonRPCPlugin.Execute|Empty standard output and standard error."); + return string.Empty; } - else if (result.StartsWith("DEBUG:")) + + if (result.StartsWith("DEBUG:")) { MessageBox.Show(new Form { TopMost = true }, result.Substring(6)); return string.Empty; } - else - { - return result; - } + + return result; } catch (Exception e) { - Log.Exception($"|JsonRPCPlugin.Execute|Exception for filename <{startInfo.FileName}> with argument <{startInfo.Arguments}>", e); + Log.Exception( + $"|JsonRPCPlugin.Execute|Exception for filename <{startInfo.FileName}> with argument <{startInfo.Arguments}>", + e); return string.Empty; } } - public void Init(PluginInitContext ctx) + protected async Task ExecuteAsync(ProcessStartInfo startInfo, CancellationToken token = default) { - context = ctx; + try + { + using var process = Process.Start(startInfo); + if (process == null) + { + Log.Error("|JsonRPCPlugin.ExecuteAsync|Can't start new process"); + return Stream.Null; + } + + var result = process.StandardOutput.BaseStream; + + token.ThrowIfCancellationRequested(); + + if (!process.StandardError.EndOfStream) + { + using var standardError = process.StandardError; + var error = await standardError.ReadToEndAsync(); + + if (!string.IsNullOrEmpty(error)) + { + Log.Error($"|JsonRPCPlugin.ExecuteAsync|{error}"); + return Stream.Null; + } + + Log.Error("|JsonRPCPlugin.ExecuteAsync|Empty standard output and standard error."); + return Stream.Null; + } + + return result; + } + catch (Exception e) + { + Log.Exception( + $"|JsonRPCPlugin.ExecuteAsync|Exception for filename <{startInfo.FileName}> with argument <{startInfo.Arguments}>", + e); + return Stream.Null; + } + } + + public async Task> QueryAsync(Query query, CancellationToken token) + { + var output = await ExecuteQueryAsync(query, token); + try + { + return await DeserializedResultAsync(output); + } + catch (Exception e) + { + Log.Exception($"|JsonRPCPlugin.Query|Exception when query <{query}>", e); + return null; + } + } + + public Task InitAsync(PluginInitContext context) + { + this.context = context; + return Task.CompletedTask; } } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Core/Plugin/PythonPlugin.cs b/Flow.Launcher.Core/Plugin/PythonPlugin.cs index 3c5a3a699..356a68a81 100644 --- a/Flow.Launcher.Core/Plugin/PythonPlugin.cs +++ b/Flow.Launcher.Core/Plugin/PythonPlugin.cs @@ -1,6 +1,8 @@ using System; using System.Diagnostics; using System.IO; +using System.Threading; +using System.Threading.Tasks; using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin; @@ -28,7 +30,7 @@ namespace Flow.Launcher.Core.Plugin } - protected override string ExecuteQuery(Query query) + protected override Task ExecuteQueryAsync(Query query, CancellationToken token) { JsonRPCServerRequestModel request = new JsonRPCServerRequestModel { @@ -40,13 +42,14 @@ namespace Flow.Launcher.Core.Plugin // todo happlebao why context can't be used in constructor _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; - return Execute(_startInfo); + return ExecuteAsync(_startInfo, token); } protected override string ExecuteCallback(JsonRPCRequestModel rpcRequest) { _startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{rpcRequest}\""; _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; + // TODO: Async Action return Execute(_startInfo); } @@ -58,6 +61,7 @@ namespace Flow.Launcher.Core.Plugin _startInfo.Arguments = $"-B \"{context.CurrentPluginMetadata.ExecuteFilePath}\" \"{request}\""; _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; + // TODO: Async Action return Execute(_startInfo); } }