From 67d1b896b1aaa90dd88a44e8a620694d05d1685e Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Wed, 31 Aug 2022 21:34:47 -0500 Subject: [PATCH 01/21] Implement JSONRPC V2 Draft --- Flow.Launcher.Core/Plugin/ExecutablePlugin.cs | 2 +- Flow.Launcher.Core/Plugin/JsonPRCModel.cs | 77 ++++---------- .../Plugin/JsonRPCModelContext.cs | 21 ++++ Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs | 46 ++++---- Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs | 100 ++++++++++++++++++ Flow.Launcher.Core/Plugin/PluginsLoader.cs | 24 +++-- Flow.Launcher.Core/Plugin/PythonPlugin.cs | 6 +- Flow.Launcher.Core/Plugin/PythonPluginV2.cs | 63 +++++++++++ Flow.Launcher.Plugin/AllowedLanguage.cs | 34 +++--- Flow.Launcher.Plugin/Query.cs | 10 +- .../Plugins/JsonRPCPluginTest.cs | 21 ++-- 11 files changed, 276 insertions(+), 128 deletions(-) create mode 100644 Flow.Launcher.Core/Plugin/JsonRPCModelContext.cs create mode 100644 Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs create mode 100644 Flow.Launcher.Core/Plugin/PythonPluginV2.cs diff --git a/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs b/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs index 049d1c583..1023ca933 100644 --- a/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs +++ b/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs @@ -6,7 +6,7 @@ using Flow.Launcher.Plugin; namespace Flow.Launcher.Core.Plugin { - internal class ExecutablePlugin : JsonRPCPlugin + internal class ExecutablePlugin : JsonRpcPlugin { private readonly ProcessStartInfo _startInfo; public override string SupportedLanguage { get; set; } = AllowedLanguage.Executable; diff --git a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs index e937779a1..cc06c8a0b 100644 --- a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs +++ b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs @@ -21,67 +21,36 @@ using System.Text.Json; namespace Flow.Launcher.Core.Plugin { - public class JsonRPCErrorModel - { - public int Code { get; set; } + public record JsonRPCRequestMessage(PluginMetadata PluginMetadata, IAsyncEnumerable Requests); + public record JsonRPCBase(int Id, JsonRPCErrorModel Error = default); + public record JsonRPCErrorModel(int Code, string Message, string Data); - public string Message { get; set; } + public record JsonRPCResponseModel(int Id, JsonRPCErrorModel Error = default) : JsonRPCBase(Id, Error); + public record JsonRPCQueryResponseModel(int Id, + [property: JsonPropertyName("result")] List Result, + Dictionary SettingsChange = null, + string DebugMessage = "", + JsonRPCErrorModel Error = default) : JsonRPCResponseModel(Id, Error); - public string Data { get; set; } - } + public record JsonRPCRequestModel(int Id, + string Method, + object[] Parameters, + Dictionary Settings = default, + JsonRPCErrorModel Error = default) : JsonRPCBase(Id, Error); - public class JsonRPCResponseModel - { - public string Result { get; set; } - - public JsonRPCErrorModel Error { get; set; } - } - - public class JsonRPCQueryResponseModel : JsonRPCResponseModel - { - [JsonPropertyName("result")] - public new List Result { get; set; } - - public Dictionary SettingsChange { get; set; } - - public string DebugMessage { get; set; } - } - - public class JsonRPCRequestModel - { - public string Method { get; set; } - - public object[] Parameters { get; set; } - - public Dictionary Settings { get; set; } - - private static readonly JsonSerializerOptions options = new() - { - PropertyNamingPolicy = JsonNamingPolicy.CamelCase - }; - public override string ToString() - { - return JsonSerializer.Serialize(this, options); - } - } - - /// - /// Json RPC Request that Flow Launcher sent to client - /// - public class JsonRPCServerRequestModel : JsonRPCRequestModel - { - - } - /// /// Json RPC Request(in query response) that client sent to Flow Launcher /// - public class JsonRPCClientRequestModel : JsonRPCRequestModel - { - public bool DontHideAfterAction { get; set; } - } - + public record JsonRPCClientRequestModel( + int Id, + string Method, + object[] Parameters, + Dictionary Settings, + bool DontHideAfterAction = false, + JsonRPCErrorModel Error = default) : JsonRPCRequestModel(Id, Method, Parameters, Settings, Error); + + /// /// Represent the json-rpc result item that client send to Flow Launcher /// Typically, we will send back this request model to client after user select the result item diff --git a/Flow.Launcher.Core/Plugin/JsonRPCModelContext.cs b/Flow.Launcher.Core/Plugin/JsonRPCModelContext.cs new file mode 100644 index 000000000..7309e740b --- /dev/null +++ b/Flow.Launcher.Core/Plugin/JsonRPCModelContext.cs @@ -0,0 +1,21 @@ +using System.Text.Json.Serialization; + +namespace Flow.Launcher.Core.Plugin +{ + // TODO: After Upgrading to .Net 7, adding Source Generating Context for IAsyncEnumerable JsonRPCMessage + + [JsonSerializable(typeof(JsonRPCQueryResponseModel))] + public partial class JsonRPCQueryResponseModelContext : JsonSerializerContext + { + } + + [JsonSerializable(typeof(JsonRPCRequestModel))] + public partial class JsonRPCRequestModelContext : JsonSerializerContext + { + } + + [JsonSerializable(typeof(JsonRPCClientRequestModel))] + public partial class JsonRPCClientRequestModelContext : JsonSerializerContext + { + } +} diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs index e3efcd296..222ec5a24 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs @@ -29,10 +29,10 @@ namespace Flow.Launcher.Core.Plugin /// Represent the plugin that using JsonPRC /// every JsonRPC plugin should has its own plugin instance /// - internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable + internal abstract class JsonRpcPlugin : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable { - protected PluginInitContext context; - public const string JsonRPC = "JsonRPC"; + protected PluginInitContext Context; + public const string JsonRpc = "JsonRPC"; /// /// The language this JsonRPCPlugin support @@ -43,19 +43,16 @@ namespace Flow.Launcher.Core.Plugin private static readonly RecyclableMemoryStreamManager BufferManager = new(); - private string SettingConfigurationPath => Path.Combine(context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml"); - private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, context.CurrentPluginMetadata.Name, "Settings.json"); + private int RequestId { get; set; } + + private string SettingConfigurationPath => Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml"); + private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, Context.CurrentPluginMetadata.Name, "Settings.json"); public List LoadContextMenus(Result selectedResult) { - var request = new JsonRPCRequestModel - { - Method = "context_menu", - Parameters = new[] - { - selectedResult.ContextData - } - }; + var request = new JsonRPCRequestModel(RequestId++, + "context_menu", + new[] { selectedResult.ContextData }); var output = Request(request); return DeserializedResult(output); } @@ -113,7 +110,7 @@ namespace Flow.Launcher.Core.Plugin if (!string.IsNullOrEmpty(queryResponseModel.DebugMessage)) { - context.API.ShowMsg(queryResponseModel.DebugMessage); + Context.API.ShowMsg(queryResponseModel.DebugMessage); } foreach (var result in queryResponseModel.Result) @@ -281,7 +278,7 @@ namespace Flow.Launcher.Core.Plugin { case (0, 0): const string errorMessage = "Empty JSON-RPC Response."; - Log.Warn($"|{nameof(JsonRPCPlugin)}.{nameof(ExecuteAsync)}|{errorMessage}"); + 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 @@ -295,20 +292,15 @@ namespace Flow.Launcher.Core.Plugin public async Task> QueryAsync(Query query, CancellationToken token) { - var request = new JsonRPCRequestModel - { - Method = "query", - Parameters = new object[] - { - query.Search - }, - Settings = Settings - }; + var request = new JsonRPCRequestModel(RequestId++, + "query", + new object[]{ query.Search }, + Settings); var output = await RequestAsync(request, token); return await DeserializedResultAsync(output); } - public async Task InitSettingAsync() + private async Task InitSettingAsync() { if (!File.Exists(SettingConfigurationPath)) return; @@ -337,7 +329,7 @@ namespace Flow.Launcher.Core.Plugin public virtual async Task InitAsync(PluginInitContext context) { - this.context = context; + this.Context = context; await InitSettingAsync(); } private static readonly Thickness settingControlMargin = new(10, 4, 10, 4); @@ -483,7 +475,7 @@ namespace Flow.Launcher.Core.Plugin { if (Settings != null) { - Helper.ValidateDirectory(Path.Combine(DataLocation.PluginSettingsDirectory, context.CurrentPluginMetadata.Name)); + Helper.ValidateDirectory(Path.Combine(DataLocation.PluginSettingsDirectory, Context.CurrentPluginMetadata.Name)); File.WriteAllText(SettingPath, JsonSerializer.Serialize(Settings, settingSerializeOption)); } } diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs new file mode 100644 index 000000000..0df090cd0 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; +using System.Windows.Controls; +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.Core.Plugin +{ + public abstract class JsonRpcPluginV2 : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable + { + public abstract string SupportedLanguage { get; set; } + + public const string JsonRpc = "JsonRPC"; + protected abstract Stream InputStream { get; set; } + protected abstract Stream OutputStream { get; set; } + protected abstract StreamReader ErrorStream { get; set; } + + protected Channel InputMessageChannel { get; set; } + + private (Task SendTask, Task ReceiveTask) MessageTask { get; set; } + private CancellationTokenSource MessageCancellationTokenSource { get; set; } + + protected int RequestId; + + private ConcurrentDictionary> RequestTaskDictionary { get; } = new(); + + // TODO: Switch to Async Task + private async void ReceiveMessageAsync(CancellationToken token) + { + var response = + JsonSerializer.DeserializeAsyncEnumerable(OutputStream, cancellationToken: token); + + ArgumentNullException.ThrowIfNull(response); + + await foreach (var message in response.WithCancellation(token)) + { + if (!RequestTaskDictionary.TryGetValue(message.Id, out var task)) + { + // Either Task is already handled or it is a invalid resopnse. + continue; + } + RequestTaskDictionary.Remove(message.Id, out _); + task.TrySetResult(message); + } + } + + // TODO: Switch to Async Task + private async void SendMessageAsync(PluginMetadata metadata, CancellationToken token) + { + var fullMessage = new JsonRPCRequestMessage(metadata, InputMessageChannel.Reader.ReadAllAsync(token)); + await JsonSerializer.SerializeAsync(InputStream, fullMessage, cancellationToken: token); + } + + public async Task> QueryAsync(Query query, CancellationToken token) + { + int currentRequestId = Interlocked.Add(ref RequestId, 1); + var message = new JsonRPCRequestModel(currentRequestId, "query", new object[] + { + query + }); + await InputMessageChannel.Writer.WriteAsync(message, token); + await Task.Delay(50); + await InputStream.FlushAsync(); + var task = new TaskCompletionSource(); + RequestTaskDictionary[currentRequestId] = task; + var result = await task.Task; + //TODO: Parse Result + return new List(); + } + public virtual Task InitAsync(PluginInitContext context) + { + InputMessageChannel = Channel.CreateUnbounded(); + MessageCancellationTokenSource = new CancellationTokenSource(); + SendMessageAsync(context.CurrentPluginMetadata, MessageCancellationTokenSource.Token); + ReceiveMessageAsync(MessageCancellationTokenSource.Token); + // MessageTask = + // (SendMessageAsync(context.CurrentPluginMetadata, MessageCancellationTokenSource.Token), + // ReceiveMessageAsync(MessageCancellationTokenSource.Token)); + return Task.CompletedTask; + } + public List LoadContextMenus(Result selectedResult) + { + throw new System.NotImplementedException(); + } + public Control CreateSettingPanel() + { + // TODO: Implement CreateSettingPanel + return new Control(); + } + public void Save() + { + // TODO: Save settings + } + } +} diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs index 752174263..d4dd4aa4f 100644 --- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs @@ -83,7 +83,10 @@ namespace Flow.Launcher.Core.Plugin return; } - plugins.Add(new PluginPair {Plugin = plugin, Metadata = metadata}); + plugins.Add(new PluginPair + { + Plugin = plugin, Metadata = metadata + }); }); metadata.InitTime += milliseconds; } @@ -110,14 +113,14 @@ namespace Flow.Launcher.Core.Plugin public static IEnumerable PythonPlugins(List source, PluginsSettings settings) { - if (!source.Any(o => o.Language.ToUpper() == AllowedLanguage.Python)) + if (!source.Any(o => o.Language.ToUpper() is AllowedLanguage.Python or AllowedLanguage.PythonV2)) return new List(); if (!string.IsNullOrEmpty(settings.PythonDirectory) && FilesFolders.LocationExists(settings.PythonDirectory)) return SetPythonPathForPluginPairs(source, Path.Combine(settings.PythonDirectory, PythonExecutable)); var pythonPath = string.Empty; - + if (MessageBox.Show("Flow detected you have installed Python plugins, which " + "will need Python to run. Would you like to download Python? " + Environment.NewLine + Environment.NewLine + @@ -185,17 +188,22 @@ namespace Flow.Launcher.Core.Plugin return SetPythonPathForPluginPairs(source, pythonPath); } - private static IEnumerable SetPythonPathForPluginPairs(List source, string pythonPath) - => source - .Where(o => o.Language.ToUpper() == AllowedLanguage.Python) + private static IEnumerable SetPythonPathForPluginPairs(IEnumerable source, string pythonPath) + => source + .Where(o => o.Language.ToUpper() is AllowedLanguage.Python or AllowedLanguage.PythonV2) .Select(metadata => new PluginPair { - Plugin = new PythonPlugin(pythonPath), + Plugin = metadata.Language.ToUpper() switch + { + AllowedLanguage.Python => new PythonPlugin(pythonPath), + AllowedLanguage.PythonV2 => new PythonPluginV2(pythonPath), + _ => throw new ArgumentOutOfRangeException() + }, Metadata = metadata }) .ToList(); - public static IEnumerable ExecutablePlugins(IEnumerable source) + public static IEnumerable ExecutablePlugins(IEnumerable source) { return source .Where(o => o.Language.ToUpper() == AllowedLanguage.Executable) diff --git a/Flow.Launcher.Core/Plugin/PythonPlugin.cs b/Flow.Launcher.Core/Plugin/PythonPlugin.cs index 8f7e5760a..62400db38 100644 --- a/Flow.Launcher.Core/Plugin/PythonPlugin.cs +++ b/Flow.Launcher.Core/Plugin/PythonPlugin.cs @@ -8,7 +8,7 @@ using Flow.Launcher.Plugin; namespace Flow.Launcher.Core.Plugin { - internal class PythonPlugin : JsonRPCPlugin + internal class PythonPlugin : JsonRpcPlugin { private readonly ProcessStartInfo _startInfo; public override string SupportedLanguage { get; set; } = AllowedLanguage.Python; @@ -25,7 +25,7 @@ namespace Flow.Launcher.Core.Plugin }; // temp fix for issue #667 - var path = Path.Combine(Constant.ProgramDirectory, JsonRPC); + var path = Path.Combine(Constant.ProgramDirectory, JsonRpc); _startInfo.EnvironmentVariables["PYTHONPATH"] = path; _startInfo.EnvironmentVariables["FLOW_VERSION"] = Constant.Version; @@ -47,7 +47,7 @@ namespace Flow.Launcher.Core.Plugin protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default) { _startInfo.ArgumentList[2] = rpcRequest.ToString(); - _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; + _startInfo.WorkingDirectory = Context.CurrentPluginMetadata.PluginDirectory; // TODO: Async Action return Execute(_startInfo); } diff --git a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs new file mode 100644 index 000000000..d5033e056 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs @@ -0,0 +1,63 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Threading; +using System.Threading.Tasks; +using Flow.Launcher.Infrastructure; +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.Core.Plugin +{ + public class PythonPluginV2 : JsonRpcPluginV2 + { + private readonly ProcessStartInfo _startInfo; + private Process _process; + public override string SupportedLanguage { get; set; } = AllowedLanguage.Python; + + protected override Stream InputStream { get; set; } + protected override Stream OutputStream { get; set; } + protected override StreamReader ErrorStream { get; set; } + + public PythonPluginV2(string filename) + { + _startInfo = new ProcessStartInfo + { + FileName = filename, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true, + RedirectStandardInput = true + }; + + // temp fix for issue #667 + var path = Path.Combine(Constant.ProgramDirectory, JsonRpc); + _startInfo.EnvironmentVariables["PYTHONPATH"] = path; + + _startInfo.EnvironmentVariables["FLOW_VERSION"] = Constant.Version; + _startInfo.EnvironmentVariables["FLOW_PROGRAM_DIRECTORY"] = Constant.ProgramDirectory; + _startInfo.EnvironmentVariables["FLOW_APPLICATION_DIRECTORY"] = Constant.ApplicationDirectory; + + + //Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable + _startInfo.ArgumentList.Add("-B"); + } + + + public override async Task InitAsync(PluginInitContext context) + { + _startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath); + _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; + + _process = Process.Start(_startInfo); + + ArgumentNullException.ThrowIfNull(_process); + + InputStream = _process.StandardInput.BaseStream; + OutputStream = _process.StandardOutput.BaseStream; + ErrorStream = _process.StandardError; + + await base.InitAsync(context); + } + } +} diff --git a/Flow.Launcher.Plugin/AllowedLanguage.cs b/Flow.Launcher.Plugin/AllowedLanguage.cs index 94c645d27..395f5f37f 100644 --- a/Flow.Launcher.Plugin/AllowedLanguage.cs +++ b/Flow.Launcher.Plugin/AllowedLanguage.cs @@ -1,4 +1,6 @@ -namespace Flow.Launcher.Plugin +using System; + +namespace Flow.Launcher.Plugin { /// /// Allowed plugin languages @@ -8,34 +10,27 @@ /// /// Python /// - public static string Python - { - get { return "PYTHON"; } - } + public const string Python = "PYTHON"; + + /// + /// Python V2 + /// + public const string PythonV2 = "PYTHON_V2"; /// /// C# /// - public static string CSharp - { - get { return "CSHARP"; } - } + public const string CSharp = "CSHARP"; /// /// F# /// - public static string FSharp - { - get { return "FSHARP"; } - } + public const string FSharp = "FSHARP"; /// /// Standard .exe /// - public static string Executable - { - get { return "EXECUTABLE"; } - } + public const string Executable = "EXECUTABLE"; /// /// Determines if this language is a .NET language @@ -56,8 +51,9 @@ public static bool IsAllowed(string language) { return IsDotNet(language) - || language.ToUpper() == Python.ToUpper() - || language.ToUpper() == Executable.ToUpper(); + || String.Equals(language, Python, StringComparison.CurrentCultureIgnoreCase) + || String.Equals(language, PythonV2, StringComparison.CurrentCultureIgnoreCase) + || String.Equals(language, Executable, StringComparison.CurrentCultureIgnoreCase); } } } diff --git a/Flow.Launcher.Plugin/Query.cs b/Flow.Launcher.Plugin/Query.cs index 95547d273..0983e882e 100644 --- a/Flow.Launcher.Plugin/Query.cs +++ b/Flow.Launcher.Plugin/Query.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; namespace Flow.Launcher.Plugin { @@ -71,26 +72,31 @@ namespace Flow.Launcher.Plugin public string ActionKeyword { get; init; } + [JsonIgnore] /// /// Return first search split by space if it has /// public string FirstSearch => SplitSearch(0); - + + [JsonIgnore] private string _secondToEndSearch; - + /// /// strings from second search (including) to last search /// + [JsonIgnore] public string SecondToEndSearch => SearchTerms.Length > 1 ? (_secondToEndSearch ??= string.Join(' ', SearchTerms[1..])) : ""; /// /// Return second search split by space if it has /// + [JsonIgnore] public string SecondSearch => SplitSearch(1); /// /// Return third search split by space if it has /// + [JsonIgnore] public string ThirdSearch => SplitSearch(2); private string SplitSearch(int index) diff --git a/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs b/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs index fb91c6388..ffa601ecd 100644 --- a/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs +++ b/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs @@ -14,7 +14,7 @@ namespace Flow.Launcher.Test.Plugins { [TestFixture] // ReSharper disable once InconsistentNaming - internal class JsonRPCPluginTest : JsonRPCPlugin + internal class JsonRPCPluginTest : JsonRpcPlugin { public override string SupportedLanguage { get; set; } = AllowedLanguage.Executable; @@ -56,21 +56,14 @@ namespace Flow.Launcher.Test.Plugins public static List ResponseModelsSource = new() { - new() + new JsonRPCQueryResponseModel(0, new List()), + new JsonRPCQueryResponseModel(0, new List { - Result = new() - }, - new() - { - Result = new() + new JsonRPCResult { - new JsonRPCResult - { - Title = "Test1", - SubTitle = "Test2" - } + Title = "Test1", SubTitle = "Test2" } - } + }) }; [TestCaseSource(typeof(JsonRPCPluginTest), nameof(ResponseModelsSource))] @@ -97,4 +90,4 @@ namespace Flow.Launcher.Test.Plugins } } -} \ No newline at end of file +} From 094da0ef0a1295508d6ac5a5b4564ff5e0795a7a Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Wed, 31 Aug 2022 21:37:29 -0500 Subject: [PATCH 02/21] Rollback rename --- Flow.Launcher.Core/Plugin/ExecutablePlugin.cs | 2 +- Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs | 4 ++-- Flow.Launcher.Core/Plugin/PythonPlugin.cs | 2 +- Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs b/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs index 1023ca933..049d1c583 100644 --- a/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs +++ b/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs @@ -6,7 +6,7 @@ using Flow.Launcher.Plugin; namespace Flow.Launcher.Core.Plugin { - internal class ExecutablePlugin : JsonRpcPlugin + internal class ExecutablePlugin : JsonRPCPlugin { private readonly ProcessStartInfo _startInfo; public override string SupportedLanguage { get; set; } = AllowedLanguage.Executable; diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs index 222ec5a24..00931f380 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs @@ -29,7 +29,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 : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable + internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable { protected PluginInitContext Context; public const string JsonRpc = "JsonRPC"; @@ -278,7 +278,7 @@ namespace Flow.Launcher.Core.Plugin { case (0, 0): const string errorMessage = "Empty JSON-RPC Response."; - Log.Warn($"|{nameof(JsonRpcPlugin)}.{nameof(ExecuteAsync)}|{errorMessage}"); + 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 diff --git a/Flow.Launcher.Core/Plugin/PythonPlugin.cs b/Flow.Launcher.Core/Plugin/PythonPlugin.cs index 62400db38..3c7b89f9a 100644 --- a/Flow.Launcher.Core/Plugin/PythonPlugin.cs +++ b/Flow.Launcher.Core/Plugin/PythonPlugin.cs @@ -8,7 +8,7 @@ using Flow.Launcher.Plugin; namespace Flow.Launcher.Core.Plugin { - internal class PythonPlugin : JsonRpcPlugin + internal class PythonPlugin : JsonRPCPlugin { private readonly ProcessStartInfo _startInfo; public override string SupportedLanguage { get; set; } = AllowedLanguage.Python; diff --git a/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs b/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs index ffa601ecd..216514722 100644 --- a/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs +++ b/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs @@ -14,7 +14,7 @@ namespace Flow.Launcher.Test.Plugins { [TestFixture] // ReSharper disable once InconsistentNaming - internal class JsonRPCPluginTest : JsonRpcPlugin + internal class JsonRPCPluginTest : JsonRPCPlugin { public override string SupportedLanguage { get; set; } = AllowedLanguage.Executable; From 2fd13c08d852752de83e7bfc0252ea21886f6506 Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Wed, 31 Aug 2022 21:38:10 -0500 Subject: [PATCH 03/21] Rollback rename --- Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs | 2 +- Flow.Launcher.Core/Plugin/PythonPlugin.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs index 00931f380..e5098ab92 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs @@ -32,7 +32,7 @@ namespace Flow.Launcher.Core.Plugin internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable { protected PluginInitContext Context; - public const string JsonRpc = "JsonRPC"; + public const string JsonRPC = "JsonRPC"; /// /// The language this JsonRPCPlugin support diff --git a/Flow.Launcher.Core/Plugin/PythonPlugin.cs b/Flow.Launcher.Core/Plugin/PythonPlugin.cs index 3c7b89f9a..0d97e36dd 100644 --- a/Flow.Launcher.Core/Plugin/PythonPlugin.cs +++ b/Flow.Launcher.Core/Plugin/PythonPlugin.cs @@ -25,7 +25,7 @@ namespace Flow.Launcher.Core.Plugin }; // temp fix for issue #667 - var path = Path.Combine(Constant.ProgramDirectory, JsonRpc); + var path = Path.Combine(Constant.ProgramDirectory, JsonRPC); _startInfo.EnvironmentVariables["PYTHONPATH"] = path; _startInfo.EnvironmentVariables["FLOW_VERSION"] = Constant.Version; From 16dcdf01fdaa1c49946e505a9a23f3d18777002d Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Sun, 26 Mar 2023 01:12:21 -0500 Subject: [PATCH 04/21] load v2 plugins --- .../Environments/AbstractPluginEnvironment.cs | 24 +++---------------- .../Environments/PythonV2Environment.cs | 13 ++++++++++ Flow.Launcher.Core/Plugin/PluginsLoader.cs | 3 +++ 3 files changed, 19 insertions(+), 21 deletions(-) create mode 100644 Flow.Launcher.Core/ExternalPlugins/Environments/PythonV2Environment.cs diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs index 9ebacc942..40eb1be3e 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/AbstractPluginEnvironment.cs @@ -41,18 +41,6 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments if (!PluginMetadataList.Any(o => o.Language.Equals(Language, StringComparison.OrdinalIgnoreCase))) return new List(); - // TODO: Remove. This is backwards compatibility for 1.10.0 release- changed PythonEmbeded to Environments/Python - if (Language.Equals(AllowedLanguage.Python, StringComparison.OrdinalIgnoreCase)) - { - FilesFolders.RemoveFolderIfExists(Path.Combine(DataLocation.DataDirectory(), "PythonEmbeddable")); - - if (!string.IsNullOrEmpty(PluginSettings.PythonDirectory) && PluginSettings.PythonDirectory.StartsWith(Path.Combine(DataLocation.DataDirectory(), "PythonEmbeddable"))) - { - InstallEnvironment(); - PluginSettings.PythonDirectory = string.Empty; - } - } - if (!string.IsNullOrEmpty(PluginsSettingsFilePath) && FilesFolders.FileExists(PluginsSettingsFilePath)) { // Ensure latest only if user is using Flow's environment setup. @@ -70,7 +58,7 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments string.Empty, MessageBoxButtons.YesNo) == DialogResult.No) { var msg = $"Please select the {EnvName} executable"; - var selectedFile = string.Empty; + string selectedFile; selectedFile = GetFileFromDialog(msg, FileDialogFilter); @@ -143,14 +131,8 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments }; var result = dlg.ShowDialog(); - if (result == DialogResult.OK) - { - return dlg.FileName; - } - else - { - return string.Empty; - } + return result == DialogResult.OK ? dlg.FileName : string.Empty; + } /// diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/PythonV2Environment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonV2Environment.cs new file mode 100644 index 000000000..180893774 --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonV2Environment.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.Core.ExternalPlugins.Environments +{ + internal class PythonV2Environment : PythonEnvironment + { + internal override string Language => AllowedLanguage.PythonV2; + + internal PythonV2Environment(List pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } + } +} diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs index e6329aba1..92e40fe66 100644 --- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs @@ -19,9 +19,11 @@ namespace Flow.Launcher.Core.Plugin var dotnetPlugins = DotNetPlugins(metadatas); var pythonEnv = new PythonEnvironment(metadatas, settings); + var pythonV2Env = new PythonV2Environment(metadatas, settings); var tsEnv = new TypeScriptEnvironment(metadatas, settings); var jsEnv = new JavaScriptEnvironment(metadatas, settings); var pythonPlugins = pythonEnv.Setup(); + var pythonV2Plugins = pythonV2Env.Setup(); var tsPlugins = tsEnv.Setup(); var jsPlugins = jsEnv.Setup(); @@ -29,6 +31,7 @@ namespace Flow.Launcher.Core.Plugin var plugins = dotnetPlugins .Concat(pythonPlugins) + .Concat(pythonV2Plugins) .Concat(tsPlugins) .Concat(jsPlugins) .Concat(executablePlugins) From 683f6ebce4cb56276c6fe6350ec7963cb4252a24 Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Sun, 26 Mar 2023 02:24:31 -0500 Subject: [PATCH 05/21] refactor jsonrpc structure (extract setting to a standalone file PortableSettings.cs) --- Flow.Launcher.Core/Plugin/JsonPRCModel.cs | 6 +- Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs | 462 +----------------- .../Plugin/JsonRPCPluginBase.cs | 176 +++++++ Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs | 4 +- Flow.Launcher.Core/Plugin/PortableSettings.cs | 402 +++++++++++++++ .../Storage/JsonStorage.cs | 105 +++- 6 files changed, 704 insertions(+), 451 deletions(-) create mode 100644 Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs create mode 100644 Flow.Launcher.Core/Plugin/PortableSettings.cs diff --git a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs index cc06c8a0b..477ee620d 100644 --- a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs +++ b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs @@ -28,14 +28,14 @@ namespace Flow.Launcher.Core.Plugin public record JsonRPCResponseModel(int Id, JsonRPCErrorModel Error = default) : JsonRPCBase(Id, Error); public record JsonRPCQueryResponseModel(int Id, [property: JsonPropertyName("result")] List Result, - Dictionary SettingsChange = null, + IReadOnlyDictionary SettingsChange = null, string DebugMessage = "", JsonRPCErrorModel Error = default) : JsonRPCResponseModel(Id, Error); public record JsonRPCRequestModel(int Id, string Method, object[] Parameters, - Dictionary Settings = default, + IReadOnlyDictionary Settings = default, JsonRPCErrorModel Error = default) : JsonRPCBase(Id, Error); @@ -46,7 +46,7 @@ namespace Flow.Launcher.Core.Plugin int Id, string Method, object[] Parameters, - Dictionary Settings, + IReadOnlyDictionary Settings, bool DontHideAfterAction = false, JsonRPCErrorModel Error = default) : JsonRPCRequestModel(Id, Method, Parameters, Settings, Error); diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs index d3fc90224..3a7fcb216 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs @@ -33,9 +33,8 @@ namespace Flow.Launcher.Core.Plugin /// Represent the plugin that using JsonPRC /// every JsonRPC plugin should has its own plugin instance /// - internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable + internal abstract class JsonRPCPlugin : JsonRPCPluginBase { - protected PluginInitContext Context; public const string JsonRPC = "JsonRPC"; protected abstract Task RequestAsync(JsonRPCRequestModel rpcRequest, CancellationToken token = default); @@ -48,7 +47,7 @@ namespace Flow.Launcher.Core.Plugin private string SettingConfigurationPath => Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml"); private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, Context.CurrentPluginMetadata.Name, "Settings.json"); - public List LoadContextMenus(Result selectedResult) + public override List LoadContextMenus(Result selectedResult) { var request = new JsonRPCRequestModel(RequestId++, "context_menu", @@ -77,7 +76,6 @@ namespace Flow.Launcher.Core.Plugin { WriteIndented = true }; - private Dictionary Settings { get; set; } private readonly Dictionary _settingControls = new(); @@ -103,85 +101,42 @@ namespace Flow.Launcher.Core.Plugin return ParseResults(queryResponseModel); } - - private List ParseResults(JsonRPCQueryResponseModel queryResponseModel) + protected override async Task ExecuteResultAsync(JsonRPCResult result) { - if (queryResponseModel.Result == null) return null; + if (result.JsonRPCAction == null) return false; - if (!string.IsNullOrEmpty(queryResponseModel.DebugMessage)) + if (string.IsNullOrEmpty(result.JsonRPCAction.Method)) { - Context.API.ShowMsg(queryResponseModel.DebugMessage); + return !result.JsonRPCAction.DontHideAfterAction; } - foreach (var result in queryResponseModel.Result) + if (result.JsonRPCAction.Method.StartsWith("Flow.Launcher.")) { - result.AsyncAction = async c => + ExecuteFlowLauncherAPI(result.JsonRPCAction.Method["Flow.Launcher.".Length..], + result.JsonRPCAction.Parameters); + } + else + { + await using var actionResponse = await RequestAsync(result.JsonRPCAction); + + if (actionResponse.Length == 0) { - UpdateSettings(result.SettingsChange); - - if (result.JsonRPCAction == null) return false; - - if (string.IsNullOrEmpty(result.JsonRPCAction.Method)) - { - return !result.JsonRPCAction.DontHideAfterAction; - } - - if (result.JsonRPCAction.Method.StartsWith("Flow.Launcher.")) - { - ExecuteFlowLauncherAPI(result.JsonRPCAction.Method["Flow.Launcher.".Length..], - result.JsonRPCAction.Parameters); - } - else - { - await using var actionResponse = await RequestAsync(result.JsonRPCAction); - - if (actionResponse.Length == 0) - { - return !result.JsonRPCAction.DontHideAfterAction; - } - - var jsonRpcRequestModel = await - JsonSerializer.DeserializeAsync(actionResponse, options); - - if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false) - { - ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method["Flow.Launcher.".Length..], - jsonRpcRequestModel.Parameters); - } - } - return !result.JsonRPCAction.DontHideAfterAction; - }; + } + + var jsonRpcRequestModel = await + JsonSerializer.DeserializeAsync(actionResponse, options); + + if (jsonRpcRequestModel?.Method?.StartsWith("Flow.Launcher.") ?? false) + { + ExecuteFlowLauncherAPI(jsonRpcRequestModel.Method["Flow.Launcher.".Length..], + jsonRpcRequestModel.Parameters); + } } - var results = new List(); - - results.AddRange(queryResponseModel.Result); - - UpdateSettings(queryResponseModel.SettingsChange); - - return results; + return !result.JsonRPCAction.DontHideAfterAction; } - private void ExecuteFlowLauncherAPI(string method, object[] parameters) - { - var parametersTypeArray = parameters.Select(param => param.GetType()).ToArray(); - var methodInfo = typeof(IPublicAPI).GetMethod(method, parametersTypeArray); - if (methodInfo == null) - { - return; - } - try - { - methodInfo.Invoke(PluginManager.API, parameters); - } - catch (Exception) - { -#if (DEBUG) - throw; -#endif - } - } /// /// Execute external program and return the output @@ -297,373 +252,10 @@ namespace Flow.Launcher.Core.Plugin } - public async Task> QueryAsync(Query query, CancellationToken token) + protected override async Task> QueryRequestAsync(JsonRPCRequestModel request, CancellationToken token) { - var request = new JsonRPCRequestModel(RequestId++, - "query", - new object[]{ query.Search }, - Settings); var output = await RequestAsync(request, token); return await DeserializedResultAsync(output); } - - private async Task InitSettingAsync() - { - if (!File.Exists(SettingConfigurationPath)) - return; - - if (File.Exists(SettingPath)) - { - await using var fileStream = File.OpenRead(SettingPath); - Settings = await JsonSerializer.DeserializeAsync>(fileStream, options); - } - - var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).Build(); - _settingsTemplate = deserializer.Deserialize(await File.ReadAllTextAsync(SettingConfigurationPath)); - - Settings ??= new Dictionary(); - - foreach (var (type, attribute) in _settingsTemplate.Body) - { - if (type == "textBlock") - continue; - if (!Settings.ContainsKey(attribute.Name)) - { - Settings[attribute.Name] = attribute.DefaultValue; - } - } - } - - public virtual async Task InitAsync(PluginInitContext context) - { - this.Context = context; - await InitSettingAsync(); - } - private static readonly Thickness settingControlMargin = new(0, 9, 18, 9); - private static readonly Thickness settingCheckboxMargin = new(0, 9, 9, 9); - private static readonly Thickness settingPanelMargin = new(0, 0, 0, 0); - private static readonly Thickness settingTextBlockMargin = new(70, 9, 18, 9); - private static readonly Thickness settingLabelPanelMargin = new(70, 9, 18, 9); - private static readonly Thickness settingLabelMargin = new(0, 0, 0, 0); - private static readonly Thickness settingDescMargin = new(0, 2, 0, 0); - private static readonly Thickness settingSepMargin = new(0, 0, 0, 2); - private JsonRpcConfigurationModel _settingsTemplate; - - public Control CreateSettingPanel() - { - if (Settings == null) - return new(); - var settingWindow = new UserControl(); - var mainPanel = new Grid - { - Margin = settingPanelMargin, VerticalAlignment = VerticalAlignment.Center - }; - ColumnDefinition gridCol1 = new ColumnDefinition(); - ColumnDefinition gridCol2 = new ColumnDefinition(); - - gridCol1.Width = new GridLength(70, GridUnitType.Star); - gridCol2.Width = new GridLength(30, GridUnitType.Star); - mainPanel.ColumnDefinitions.Add(gridCol1); - mainPanel.ColumnDefinitions.Add(gridCol2); - settingWindow.Content = mainPanel; - int rowCount = 0; - foreach (var (type, attribute) in _settingsTemplate.Body) - { - Separator sep = new Separator(); - sep.VerticalAlignment = VerticalAlignment.Top; - sep.Margin = settingSepMargin; - sep.SetResourceReference(Separator.BackgroundProperty, "Color03B"); /* for theme change */ - var panel = new StackPanel - { - Orientation = Orientation.Vertical, - VerticalAlignment = VerticalAlignment.Center, - Margin = settingLabelPanelMargin - }; - RowDefinition gridRow = new RowDefinition(); - mainPanel.RowDefinitions.Add(gridRow); - var name = new TextBlock() - { - Text = attribute.Label, - VerticalAlignment = VerticalAlignment.Center, - Margin = settingLabelMargin, - TextWrapping = TextWrapping.WrapWithOverflow - }; - var desc = new TextBlock() - { - Text = attribute.Description, - FontSize = 12, - VerticalAlignment = VerticalAlignment.Center, - Margin = settingDescMargin, - TextWrapping = TextWrapping.WrapWithOverflow - }; - desc.SetResourceReference(TextBlock.ForegroundProperty, "Color04B"); - - if (attribute.Description == null) /* if no description, hide */ - desc.Visibility = Visibility.Collapsed; - - - if (type != "textBlock") /* if textBlock, hide desc */ - { - panel.Children.Add(name); - panel.Children.Add(desc); - } - - - Grid.SetColumn(panel, 0); - Grid.SetRow(panel, rowCount); - - FrameworkElement contentControl; - - switch (type) - { - case "textBlock": - { - contentControl = new TextBlock - { - Text = attribute.Description.Replace("\\r\\n", "\r\n"), - Margin = settingTextBlockMargin, - Padding = new Thickness(0, 0, 0, 0), - HorizontalAlignment = System.Windows.HorizontalAlignment.Left, - TextAlignment = TextAlignment.Left, - TextWrapping = TextWrapping.Wrap - }; - Grid.SetColumn(contentControl, 0); - Grid.SetColumnSpan(contentControl, 2); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - } - case "input": - { - var textBox = new TextBox() - { - Text = Settings[attribute.Name] as string ?? string.Empty, - Margin = settingControlMargin, - HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, - ToolTip = attribute.Description - }; - textBox.TextChanged += (_, _) => - { - Settings[attribute.Name] = textBox.Text; - }; - contentControl = textBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - } - case "inputWithFileBtn": - { - var textBox = new TextBox() - { - Margin = new Thickness(10, 0, 0, 0), - Text = Settings[attribute.Name] as string ?? string.Empty, - HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, - ToolTip = attribute.Description - }; - textBox.TextChanged += (_, _) => - { - Settings[attribute.Name] = textBox.Text; - }; - var Btn = new System.Windows.Controls.Button() - { - Margin = new Thickness(10, 0, 0, 0), Content = "Browse" - }; - var dockPanel = new DockPanel() - { - Margin = settingControlMargin - }; - DockPanel.SetDock(Btn, Dock.Right); - dockPanel.Children.Add(Btn); - dockPanel.Children.Add(textBox); - contentControl = dockPanel; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - } - case "textarea": - { - var textBox = new TextBox() - { - Height = 120, - Margin = settingControlMargin, - VerticalAlignment = VerticalAlignment.Center, - TextWrapping = TextWrapping.WrapWithOverflow, - AcceptsReturn = true, - HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, - Text = Settings[attribute.Name] as string ?? string.Empty, - ToolTip = attribute.Description - }; - textBox.TextChanged += (sender, _) => - { - Settings[attribute.Name] = ((TextBox)sender).Text; - }; - contentControl = textBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - } - case "passwordBox": - { - var passwordBox = new PasswordBox() - { - Margin = settingControlMargin, - Password = Settings[attribute.Name] as string ?? string.Empty, - PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar, - HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, - ToolTip = attribute.Description - }; - passwordBox.PasswordChanged += (sender, _) => - { - Settings[attribute.Name] = ((PasswordBox)sender).Password; - }; - contentControl = passwordBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - } - case "dropdown": - { - var comboBox = new System.Windows.Controls.ComboBox() - { - ItemsSource = attribute.Options, - SelectedItem = Settings[attribute.Name], - Margin = settingControlMargin, - HorizontalAlignment = System.Windows.HorizontalAlignment.Right, - ToolTip = attribute.Description - }; - comboBox.SelectionChanged += (sender, _) => - { - Settings[attribute.Name] = (string)((System.Windows.Controls.ComboBox)sender).SelectedItem; - }; - contentControl = comboBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - } - case "checkbox": - var checkBox = new CheckBox - { - IsChecked = Settings[attribute.Name] is bool isChecked ? isChecked : bool.Parse(attribute.DefaultValue), - Margin = settingCheckboxMargin, - HorizontalAlignment = System.Windows.HorizontalAlignment.Right, - ToolTip = attribute.Description - }; - checkBox.Click += (sender, _) => - { - Settings[attribute.Name] = ((CheckBox)sender).IsChecked; - }; - contentControl = checkBox; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - case "hyperlink": - var hyperlink = new Hyperlink - { - ToolTip = attribute.Description, NavigateUri = attribute.url - }; - var linkbtn = new System.Windows.Controls.Button - { - HorizontalAlignment = System.Windows.HorizontalAlignment.Right, Margin = settingControlMargin - }; - linkbtn.Content = attribute.urlLabel; - - contentControl = linkbtn; - Grid.SetColumn(contentControl, 1); - Grid.SetRow(contentControl, rowCount); - if (rowCount != 0) - mainPanel.Children.Add(sep); - Grid.SetRow(sep, rowCount); - Grid.SetColumn(sep, 0); - Grid.SetColumnSpan(sep, 2); - break; - default: - continue; - } - if (type != "textBlock") - _settingControls[attribute.Name] = contentControl; - mainPanel.Children.Add(panel); - mainPanel.Children.Add(contentControl); - rowCount++; - - } - return settingWindow; - } - - public void Save() - { - if (Settings != null) - { - Helper.ValidateDirectory(Path.Combine(DataLocation.PluginSettingsDirectory, Context.CurrentPluginMetadata.Name)); - File.WriteAllText(SettingPath, JsonSerializer.Serialize(Settings, settingSerializeOption)); - } - } - - public void UpdateSettings(Dictionary settings) - { - if (settings == null || settings.Count == 0) - return; - - foreach (var (key, value) in settings) - { - if (Settings.ContainsKey(key)) - { - Settings[key] = value; - } - if (_settingControls.ContainsKey(key)) - { - - switch (_settingControls[key]) - { - case TextBox textBox: - textBox.Dispatcher.Invoke(() => textBox.Text = value as string); - break; - case PasswordBox passwordBox: - passwordBox.Dispatcher.Invoke(() => passwordBox.Password = value as string); - break; - case System.Windows.Controls.ComboBox comboBox: - comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value); - break; - case CheckBox checkBox: - checkBox.Dispatcher.Invoke(() => checkBox.IsChecked = value is bool isChecked ? isChecked : bool.Parse(value as string)); - break; - } - } - } - } } - } diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs new file mode 100644 index 000000000..2ff076926 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs @@ -0,0 +1,176 @@ +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.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 Microsoft.IO; +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 Orientation = System.Windows.Controls.Orientation; +using TextBox = System.Windows.Controls.TextBox; +using UserControl = System.Windows.Controls.UserControl; +using System.Windows.Documents; +using static System.Windows.Forms.LinkLabel; +using Droplex; +using System.Windows.Forms; +using Microsoft.VisualStudio.Threading; + +namespace Flow.Launcher.Core.Plugin +{ + /// + /// Represent the plugin that using JsonPRC + /// every JsonRPC plugin should has its own plugin instance + /// + internal abstract class JsonRPCPluginBase : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable + { + protected PluginInitContext Context; + public const string JsonRPC = "JsonRPC"; + + private int RequestId { get; set; } + + private string SettingConfigurationPath => Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml"); + + private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, Context.CurrentPluginMetadata.Name, "Settings.json"); + + public abstract List LoadContextMenus(Result selectedResult); + + private static readonly JsonSerializerOptions options = new() + { + PropertyNameCaseInsensitive = true, +#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 + // see: https://github.com/dotnet/runtime/issues/39152 + IgnoreNullValues = true, +#pragma warning restore SYSLIB0020 // Type or member is obsolete + Converters = + { + new JsonObjectConverter() + } + }; + + private static readonly JsonSerializerOptions settingSerializeOption = new() + { + WriteIndented = true + }; + + private readonly Dictionary _settingControls = new(); + + protected abstract Task ExecuteResultAsync(JsonRPCResult result); + protected abstract Task> QueryRequestAsync(JsonRPCRequestModel request, CancellationToken token); + + protected PortableSettings Settings { get; set; } + + protected List ParseResults(JsonRPCQueryResponseModel queryResponseModel) + { + if (queryResponseModel.Result == null) return null; + + if (!string.IsNullOrEmpty(queryResponseModel.DebugMessage)) + { + Context.API.ShowMsg(queryResponseModel.DebugMessage); + } + + foreach (var result in queryResponseModel.Result) + { + result.AsyncAction = async c => + { + Settings.UpdateSettings(result.SettingsChange); + + return await ExecuteResultAsync(result); + }; + } + + var results = new List(); + + results.AddRange(queryResponseModel.Result); + + Settings.UpdateSettings(queryResponseModel.SettingsChange); + + return results; + } + + protected void ExecuteFlowLauncherAPI(string method, object[] parameters) + { + var parametersTypeArray = parameters.Select(param => param.GetType()).ToArray(); + var methodInfo = typeof(IPublicAPI).GetMethod(method, parametersTypeArray); + + if (methodInfo == null) + { + return; + } + + try + { + methodInfo.Invoke(Context.API, parameters); + } + catch (Exception) + { +#if (DEBUG) + throw; +#endif + } + } + + public async Task> QueryAsync(Query query, CancellationToken token) + { + var request = new JsonRPCRequestModel(RequestId++, + "query", + new object[] + { + query.Search + }, + Settings.Inner); + + return await QueryRequestAsync(request, token); + + } + + + private async Task InitSettingAsync() + { + if (!File.Exists(SettingConfigurationPath)) + return; + + var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).Build(); + var configuration = deserializer.Deserialize(await File.ReadAllTextAsync(SettingConfigurationPath)); + + Settings ??= new PortableSettings + { + Configuration = configuration, + SettingPath = SettingPath, + API = Context.API + }; + + } + + public virtual async Task InitAsync(PluginInitContext context) + { + this.Context = context; + await InitSettingAsync(); + } + + public void Save() + { + Settings.Save(); + } + public Control CreateSettingPanel() + { + return Settings.CreateSettingPanel(); + } + } + +} diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs index 0df090cd0..3bef2f191 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs @@ -64,8 +64,8 @@ namespace Flow.Launcher.Core.Plugin query }); await InputMessageChannel.Writer.WriteAsync(message, token); - await Task.Delay(50); - await InputStream.FlushAsync(); + await Task.Delay(50, token); + await InputStream.FlushAsync(token); var task = new TaskCompletionSource(); RequestTaskDictionary[currentRequestId] = task; var result = await task.Task; diff --git a/Flow.Launcher.Core/Plugin/PortableSettings.cs b/Flow.Launcher.Core/Plugin/PortableSettings.cs new file mode 100644 index 000000000..36d09c3f1 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/PortableSettings.cs @@ -0,0 +1,402 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Documents; +using Flow.Launcher.Infrastructure.Storage; +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.Core.Plugin +{ + public class PortableSettings + { + public required JsonRpcConfigurationModel Configuration { get; init; } + + public required string SettingPath { get; init; } + public Dictionary SettingControls { get; } = new(); + + public IReadOnlyDictionary Inner => Settings; + protected Dictionary Settings { get; set; } + public required IPublicAPI API { get; init; } + + private JsonStorage> _storage; + + // maybe move to resource? + private static readonly Thickness settingControlMargin = new(0, 9, 18, 9); + private static readonly Thickness settingCheckboxMargin = new(0, 9, 9, 9); + private static readonly Thickness settingPanelMargin = new(0, 0, 0, 0); + private static readonly Thickness settingTextBlockMargin = new(70, 9, 18, 9); + private static readonly Thickness settingLabelPanelMargin = new(70, 9, 18, 9); + private static readonly Thickness settingLabelMargin = new(0, 0, 0, 0); + private static readonly Thickness settingDescMargin = new(0, 2, 0, 0); + private static readonly Thickness settingSepMargin = new(0, 0, 0, 2); + + public async Task InitializeAsync() + { + _storage = new JsonStorage>(SettingPath); + Settings = await _storage.LoadAsync(); + } + + + public void UpdateSettings(IReadOnlyDictionary settings) + { + if (settings == null || settings.Count == 0) + return; + + foreach (var (key, value) in settings) + { + if (Settings.ContainsKey(key)) + { + Settings[key] = value; + } + + if (SettingControls.TryGetValue(key, out var control)) + { + switch (control) + { + case TextBox textBox: + textBox.Dispatcher.Invoke(() => textBox.Text = value as string ?? string.Empty); + break; + case PasswordBox passwordBox: + passwordBox.Dispatcher.Invoke(() => passwordBox.Password = value as string ?? string.Empty); + break; + case ComboBox comboBox: + comboBox.Dispatcher.Invoke(() => comboBox.SelectedItem = value); + break; + case CheckBox checkBox: + checkBox.Dispatcher.Invoke(() => checkBox.IsChecked = value is bool isChecked ? isChecked : bool.Parse(value as string ?? string.Empty)); + break; + } + } + } + } + + public async Task SaveAsync() + { + await _storage.SaveAsync(); + } + + public void Save() + { + _storage.Save(); + } + + public Control CreateSettingPanel() + { + if (Settings == null) + return new(); + + var settingWindow = new UserControl(); + var mainPanel = new Grid + { + Margin = settingPanelMargin, VerticalAlignment = VerticalAlignment.Center + }; + + ColumnDefinition gridCol1 = new ColumnDefinition(); + ColumnDefinition gridCol2 = new ColumnDefinition(); + + gridCol1.Width = new GridLength(70, GridUnitType.Star); + gridCol2.Width = new GridLength(30, GridUnitType.Star); + mainPanel.ColumnDefinitions.Add(gridCol1); + mainPanel.ColumnDefinitions.Add(gridCol2); + settingWindow.Content = mainPanel; + int rowCount = 0; + + foreach (var (type, attribute) in Configuration.Body) + { + Separator sep = new Separator(); + sep.VerticalAlignment = VerticalAlignment.Top; + sep.Margin = settingSepMargin; + sep.SetResourceReference(Separator.BackgroundProperty, "Color03B"); /* for theme change */ + var panel = new StackPanel + { + Orientation = Orientation.Vertical, + VerticalAlignment = VerticalAlignment.Center, + Margin = settingLabelPanelMargin + }; + + RowDefinition gridRow = new RowDefinition(); + mainPanel.RowDefinitions.Add(gridRow); + var name = new TextBlock() + { + Text = attribute.Label, + VerticalAlignment = VerticalAlignment.Center, + Margin = settingLabelMargin, + TextWrapping = TextWrapping.WrapWithOverflow + }; + + var desc = new TextBlock() + { + Text = attribute.Description, + FontSize = 12, + VerticalAlignment = VerticalAlignment.Center, + Margin = settingDescMargin, + TextWrapping = TextWrapping.WrapWithOverflow + }; + + desc.SetResourceReference(TextBlock.ForegroundProperty, "Color04B"); + + if (attribute.Description == null) /* if no description, hide */ + desc.Visibility = Visibility.Collapsed; + + + if (type != "textBlock") /* if textBlock, hide desc */ + { + panel.Children.Add(name); + panel.Children.Add(desc); + } + + + Grid.SetColumn(panel, 0); + Grid.SetRow(panel, rowCount); + + FrameworkElement contentControl; + + switch (type) + { + case "textBlock": + { + contentControl = new TextBlock + { + Text = attribute.Description.Replace("\\r\\n", "\r\n"), + Margin = settingTextBlockMargin, + Padding = new Thickness(0, 0, 0, 0), + HorizontalAlignment = System.Windows.HorizontalAlignment.Left, + TextAlignment = TextAlignment.Left, + TextWrapping = TextWrapping.Wrap + }; + + Grid.SetColumn(contentControl, 0); + Grid.SetColumnSpan(contentControl, 2); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + + break; + } + case "input": + { + var textBox = new TextBox() + { + Text = Settings[attribute.Name] as string ?? string.Empty, + Margin = settingControlMargin, + HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, + ToolTip = attribute.Description + }; + + textBox.TextChanged += (_, _) => + { + Settings[attribute.Name] = textBox.Text; + }; + + contentControl = textBox; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + + break; + } + case "inputWithFileBtn": + { + var textBox = new TextBox() + { + Margin = new Thickness(10, 0, 0, 0), + Text = Settings[attribute.Name] as string ?? string.Empty, + HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, + ToolTip = attribute.Description + }; + + textBox.TextChanged += (_, _) => + { + Settings[attribute.Name] = textBox.Text; + }; + + var Btn = new System.Windows.Controls.Button() + { + Margin = new Thickness(10, 0, 0, 0), Content = "Browse" + }; + + var dockPanel = new DockPanel() + { + Margin = settingControlMargin + }; + + DockPanel.SetDock(Btn, Dock.Right); + dockPanel.Children.Add(Btn); + dockPanel.Children.Add(textBox); + contentControl = dockPanel; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + + break; + } + case "textarea": + { + var textBox = new TextBox() + { + Height = 120, + Margin = settingControlMargin, + VerticalAlignment = VerticalAlignment.Center, + TextWrapping = TextWrapping.WrapWithOverflow, + AcceptsReturn = true, + HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, + Text = Settings[attribute.Name] as string ?? string.Empty, + ToolTip = attribute.Description + }; + + textBox.TextChanged += (sender, _) => + { + Settings[attribute.Name] = ((TextBox)sender).Text; + }; + + contentControl = textBox; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + + break; + } + case "passwordBox": + { + var passwordBox = new PasswordBox() + { + Margin = settingControlMargin, + Password = Settings[attribute.Name] as string ?? string.Empty, + PasswordChar = attribute.passwordChar == default ? '*' : attribute.passwordChar, + HorizontalAlignment = System.Windows.HorizontalAlignment.Stretch, + ToolTip = attribute.Description + }; + + passwordBox.PasswordChanged += (sender, _) => + { + Settings[attribute.Name] = ((PasswordBox)sender).Password; + }; + + contentControl = passwordBox; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + + break; + } + case "dropdown": + { + var comboBox = new System.Windows.Controls.ComboBox() + { + ItemsSource = attribute.Options, + SelectedItem = Settings[attribute.Name], + Margin = settingControlMargin, + HorizontalAlignment = System.Windows.HorizontalAlignment.Right, + ToolTip = attribute.Description + }; + + comboBox.SelectionChanged += (sender, _) => + { + Settings[attribute.Name] = (string)((System.Windows.Controls.ComboBox)sender).SelectedItem; + }; + + contentControl = comboBox; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + + break; + } + case "checkbox": + var checkBox = new CheckBox + { + IsChecked = Settings[attribute.Name] is bool isChecked ? isChecked : bool.Parse(attribute.DefaultValue), + Margin = settingCheckboxMargin, + HorizontalAlignment = System.Windows.HorizontalAlignment.Right, + ToolTip = attribute.Description + }; + + checkBox.Click += (sender, _) => + { + Settings[attribute.Name] = ((CheckBox)sender).IsChecked; + }; + + contentControl = checkBox; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + + break; + case "hyperlink": + var hyperlink = new Hyperlink + { + ToolTip = attribute.Description, NavigateUri = attribute.url + }; + + var linkbtn = new System.Windows.Controls.Button + { + HorizontalAlignment = System.Windows.HorizontalAlignment.Right, Margin = settingControlMargin + }; + + linkbtn.Content = attribute.urlLabel; + + contentControl = linkbtn; + Grid.SetColumn(contentControl, 1); + Grid.SetRow(contentControl, rowCount); + if (rowCount != 0) + mainPanel.Children.Add(sep); + + Grid.SetRow(sep, rowCount); + Grid.SetColumn(sep, 0); + Grid.SetColumnSpan(sep, 2); + + break; + default: + continue; + } + + if (type != "textBlock") + SettingControls[attribute.Name] = contentControl; + + mainPanel.Children.Add(panel); + mainPanel.Children.Add(contentControl); + rowCount++; + + } + + return settingWindow; + } + + + } +} diff --git a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs index 45456ddeb..7181ae225 100644 --- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs @@ -3,6 +3,7 @@ using System; using System.Globalization; using System.IO; using System.Text.Json; +using System.Threading.Tasks; using Flow.Launcher.Infrastructure.Logger; namespace Flow.Launcher.Infrastructure.Storage @@ -26,6 +27,82 @@ namespace Flow.Launcher.Infrastructure.Storage protected string DirectoryPath { get; init; } = null!; + // Let the derived class to set the file path + protected JsonStorage() + { + } + public JsonStorage(string filePath) + { + FilePath = filePath; + } + + public async Task LoadAsync() + { + if (Data != null) + return Data; + + string? serialized = null; + + if (File.Exists(FilePath)) + { + serialized = await File.ReadAllTextAsync(FilePath); + } + + if (!string.IsNullOrEmpty(serialized)) + { + try + { + Data = JsonSerializer.Deserialize(serialized) ?? await LoadBackupOrDefaultAsync(); + } + catch (JsonException) + { + Data = await LoadBackupOrDefaultAsync(); + } + } + else + { + Data = await LoadBackupOrDefaultAsync(); + } + + return Data.NonNull(); + } + + private async ValueTask LoadBackupOrDefaultAsync() + { + var backup = await TryLoadBackupAsync(); + + return backup ?? LoadDefault(); + } + + private async ValueTask TryLoadBackupAsync() + { + if (!File.Exists(BackupFilePath)) + return default; + + try + { + await using var source = File.OpenRead(BackupFilePath); + var data = await JsonSerializer.DeserializeAsync(source) ?? default; + + if (data != null) + RestoreBackup(); + + return data; + } + catch (JsonException) + { + return default; + } + } + private void RestoreBackup() + { + Log.Info($"|JsonStorage.Load|Failed to load settings.json, {BackupFilePath} restored successfully"); + + if (File.Exists(FilePath)) + File.Replace(BackupFilePath, FilePath, null); + else + File.Move(BackupFilePath, FilePath); + } public T Load() { @@ -75,18 +152,9 @@ namespace Flow.Launcher.Infrastructure.Storage var data = JsonSerializer.Deserialize(File.ReadAllText(BackupFilePath)); if (data != null) - { - Log.Info($"|JsonStorage.Load|Failed to load settings.json, {BackupFilePath} restored successfully"); - - if(File.Exists(FilePath)) - File.Replace(BackupFilePath, FilePath, null); - else - File.Move(BackupFilePath, FilePath); + RestoreBackup(); - return data; - } - - return default; + return data; } catch (JsonException) { @@ -115,6 +183,20 @@ namespace Flow.Launcher.Infrastructure.Storage File.WriteAllText(TempFilePath, serialized); + AtomicWriteSetting(); + } + public async Task SaveAsync() + { + var tempOutput = File.OpenWrite(TempFilePath); + await JsonSerializer.SerializeAsync(tempOutput, Data, + new JsonSerializerOptions + { + WriteIndented = true + }); + AtomicWriteSetting(); + } + private void AtomicWriteSetting() + { if (!File.Exists(FilePath)) { File.Move(TempFilePath, FilePath); @@ -124,5 +206,6 @@ namespace Flow.Launcher.Infrastructure.Storage File.Replace(TempFilePath, FilePath, BackupFilePath); } } + } } From a3367abd7a735f3d3bc0a5c916eb5d8e43106827 Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Sun, 26 Mar 2023 14:04:06 -0500 Subject: [PATCH 06/21] fix some bug (v1 still broken) --- .../Plugin/JsonRPCPluginBase.cs | 6 +++-- Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs | 27 ++++--------------- Flow.Launcher.Core/Plugin/PortableSettings.cs | 13 +++++++++ Flow.Launcher.Core/Plugin/PythonPlugin.cs | 3 ++- Flow.Launcher.Core/Plugin/PythonPluginV2.cs | 13 +++++++-- .../Storage/JsonStorage.cs | 3 +++ 6 files changed, 38 insertions(+), 27 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs index 2ff076926..c3790709d 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs @@ -47,7 +47,7 @@ namespace Flow.Launcher.Core.Plugin public abstract List LoadContextMenus(Result selectedResult); - private static readonly JsonSerializerOptions options = new() + protected static readonly JsonSerializerOptions options = new() { PropertyNameCaseInsensitive = true, #pragma warning disable SYSLIB0020 @@ -155,6 +155,8 @@ namespace Flow.Launcher.Core.Plugin API = Context.API }; + await Settings.InitializeAsync(); + } public virtual async Task InitAsync(PluginInitContext context) @@ -165,7 +167,7 @@ namespace Flow.Launcher.Core.Plugin public void Save() { - Settings.Save(); + Settings?.Save(); } public Control CreateSettingPanel() { diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs index 3bef2f191..bc5d00ed4 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs @@ -11,7 +11,7 @@ using Flow.Launcher.Plugin; namespace Flow.Launcher.Core.Plugin { - public abstract class JsonRpcPluginV2 : IAsyncPlugin, IContextMenu, ISettingProvider, ISavable + internal abstract class JsonRpcPluginV2 : JsonRPCPluginBase { public abstract string SupportedLanguage { get; set; } @@ -56,14 +56,10 @@ namespace Flow.Launcher.Core.Plugin await JsonSerializer.SerializeAsync(InputStream, fullMessage, cancellationToken: token); } - public async Task> QueryAsync(Query query, CancellationToken token) + protected override async Task> QueryRequestAsync(JsonRPCRequestModel query, CancellationToken token) { int currentRequestId = Interlocked.Add(ref RequestId, 1); - var message = new JsonRPCRequestModel(currentRequestId, "query", new object[] - { - query - }); - await InputMessageChannel.Writer.WriteAsync(message, token); + await InputMessageChannel.Writer.WriteAsync(query, token); await Task.Delay(50, token); await InputStream.FlushAsync(token); var task = new TaskCompletionSource(); @@ -72,8 +68,9 @@ namespace Flow.Launcher.Core.Plugin //TODO: Parse Result return new List(); } - public virtual Task InitAsync(PluginInitContext context) + public override async Task InitAsync(PluginInitContext context) { + await base.InitAsync(context); InputMessageChannel = Channel.CreateUnbounded(); MessageCancellationTokenSource = new CancellationTokenSource(); SendMessageAsync(context.CurrentPluginMetadata, MessageCancellationTokenSource.Token); @@ -81,20 +78,6 @@ namespace Flow.Launcher.Core.Plugin // MessageTask = // (SendMessageAsync(context.CurrentPluginMetadata, MessageCancellationTokenSource.Token), // ReceiveMessageAsync(MessageCancellationTokenSource.Token)); - return Task.CompletedTask; - } - public List LoadContextMenus(Result selectedResult) - { - throw new System.NotImplementedException(); - } - public Control CreateSettingPanel() - { - // TODO: Implement CreateSettingPanel - return new Control(); - } - public void Save() - { - // TODO: Save settings } } } diff --git a/Flow.Launcher.Core/Plugin/PortableSettings.cs b/Flow.Launcher.Core/Plugin/PortableSettings.cs index 36d09c3f1..542460877 100644 --- a/Flow.Launcher.Core/Plugin/PortableSettings.cs +++ b/Flow.Launcher.Core/Plugin/PortableSettings.cs @@ -35,6 +35,19 @@ namespace Flow.Launcher.Core.Plugin { _storage = new JsonStorage>(SettingPath); Settings = await _storage.LoadAsync(); + + foreach (var (type, attributes) in Configuration.Body) + { + if (attributes.Name == null) + { + continue; + } + + if (!Settings.ContainsKey(attributes.Name)) + { + Settings[attributes.Name] = attributes.DefaultValue; + } + } } diff --git a/Flow.Launcher.Core/Plugin/PythonPlugin.cs b/Flow.Launcher.Core/Plugin/PythonPlugin.cs index 96838a1d1..62f260867 100644 --- a/Flow.Launcher.Core/Plugin/PythonPlugin.cs +++ b/Flow.Launcher.Core/Plugin/PythonPlugin.cs @@ -1,6 +1,7 @@ using System; using System.Diagnostics; using System.IO; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Flow.Launcher.Infrastructure; @@ -38,7 +39,7 @@ namespace Flow.Launcher.Core.Plugin protected override Task RequestAsync(JsonRPCRequestModel request, CancellationToken token = default) { - _startInfo.ArgumentList[2] = request.ToString(); + _startInfo.ArgumentList[2] = JsonSerializer.Serialize(request); return ExecuteAsync(_startInfo, token); } diff --git a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs index d5033e056..1e7a74a58 100644 --- a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Threading; @@ -8,7 +9,7 @@ using Flow.Launcher.Plugin; namespace Flow.Launcher.Core.Plugin { - public class PythonPluginV2 : JsonRpcPluginV2 + internal class PythonPluginV2 : JsonRpcPluginV2 { private readonly ProcessStartInfo _startInfo; private Process _process; @@ -42,8 +43,16 @@ namespace Flow.Launcher.Core.Plugin //Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable _startInfo.ArgumentList.Add("-B"); } - + + public override List LoadContextMenus(Result selectedResult) + { + throw new NotImplementedException(); + } + protected override Task ExecuteResultAsync(JsonRPCResult result) + { + throw new NotImplementedException(); + } public override async Task InitAsync(PluginInitContext context) { _startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath); diff --git a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs index 7181ae225..642250627 100644 --- a/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs +++ b/Flow.Launcher.Infrastructure/Storage/JsonStorage.cs @@ -34,6 +34,9 @@ namespace Flow.Launcher.Infrastructure.Storage public JsonStorage(string filePath) { FilePath = filePath; + DirectoryPath = Path.GetDirectoryName(filePath) ?? throw new ArgumentException("Invalid file path"); + + Helper.ValidateDirectory(DirectoryPath); } public async Task LoadAsync() From b425aac159d7f6dd255b68b12e4754e8cb44a0d1 Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Sun, 26 Mar 2023 14:05:41 -0500 Subject: [PATCH 07/21] fix a little bit more --- Flow.Launcher.Core/Plugin/PythonPlugin.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/PythonPlugin.cs b/Flow.Launcher.Core/Plugin/PythonPlugin.cs index 62f260867..4aea1ee29 100644 --- a/Flow.Launcher.Core/Plugin/PythonPlugin.cs +++ b/Flow.Launcher.Core/Plugin/PythonPlugin.cs @@ -47,7 +47,7 @@ namespace Flow.Launcher.Core.Plugin protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default) { // since this is not static, request strings will build up in ArgumentList if index is not specified - _startInfo.ArgumentList[2] = rpcRequest.ToString(); + _startInfo.ArgumentList[2] = JsonSerializer.Serialize(rpcRequest); _startInfo.WorkingDirectory = Context.CurrentPluginMetadata.PluginDirectory; // TODO: Async Action return Execute(_startInfo); From 1551567269bd4c2c2329aaa1460b380d5e891229 Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Sun, 26 Mar 2023 14:21:36 -0500 Subject: [PATCH 08/21] fix v1 --- Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs | 8 +++----- Flow.Launcher.Core/Plugin/PythonPlugin.cs | 4 ++-- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs index c3790709d..e3386a52f 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs @@ -47,7 +47,7 @@ namespace Flow.Launcher.Core.Plugin public abstract List LoadContextMenus(Result selectedResult); - protected static readonly JsonSerializerOptions options = new() + protected static readonly JsonSerializerOptions DeserializeOption = new() { PropertyNameCaseInsensitive = true, #pragma warning disable SYSLIB0020 @@ -63,13 +63,11 @@ namespace Flow.Launcher.Core.Plugin } }; - private static readonly JsonSerializerOptions settingSerializeOption = new() + protected static readonly JsonSerializerOptions RequestSerializeOption = new() { - WriteIndented = true + PropertyNameCaseInsensitive = true, PropertyNamingPolicy = JsonNamingPolicy.CamelCase, }; - private readonly Dictionary _settingControls = new(); - protected abstract Task ExecuteResultAsync(JsonRPCResult result); protected abstract Task> QueryRequestAsync(JsonRPCRequestModel request, CancellationToken token); diff --git a/Flow.Launcher.Core/Plugin/PythonPlugin.cs b/Flow.Launcher.Core/Plugin/PythonPlugin.cs index 4aea1ee29..fbe5edc7c 100644 --- a/Flow.Launcher.Core/Plugin/PythonPlugin.cs +++ b/Flow.Launcher.Core/Plugin/PythonPlugin.cs @@ -39,7 +39,7 @@ namespace Flow.Launcher.Core.Plugin protected override Task RequestAsync(JsonRPCRequestModel request, CancellationToken token = default) { - _startInfo.ArgumentList[2] = JsonSerializer.Serialize(request); + _startInfo.ArgumentList[2] = JsonSerializer.Serialize(request, RequestSerializeOption); return ExecuteAsync(_startInfo, token); } @@ -47,7 +47,7 @@ namespace Flow.Launcher.Core.Plugin protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default) { // since this is not static, request strings will build up in ArgumentList if index is not specified - _startInfo.ArgumentList[2] = JsonSerializer.Serialize(rpcRequest); + _startInfo.ArgumentList[2] = JsonSerializer.Serialize(rpcRequest, RequestSerializeOption); _startInfo.WorkingDirectory = Context.CurrentPluginMetadata.PluginDirectory; // TODO: Async Action return Execute(_startInfo); From e183920b8e32da7aaf865bd20dcd85b5562067d7 Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Fri, 2 Jun 2023 23:05:09 +0800 Subject: [PATCH 09/21] implement v2 --- .../Environments/PythonV2Environment.cs | 10 +++ Flow.Launcher.Core/Flow.Launcher.Core.csproj | 5 ++ Flow.Launcher.Core/Plugin/JsonPRCModel.cs | 2 +- Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs | 12 ++- .../Plugin/JsonRPCPluginBase.cs | 17 +--- Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs | 89 +++++++++---------- .../JsonRPCV2Models/JsonRPCExecuteResponse.cs | 4 + .../JsonRPCV2Models/JsonRPCQueryRequest.cs | 9 ++ Flow.Launcher.Core/Plugin/PythonPluginV2.cs | 33 ++++--- Flow.Launcher/Flow.Launcher.csproj | 2 +- Flow.Launcher/Notification.cs | 12 +-- 11 files changed, 109 insertions(+), 86 deletions(-) create mode 100644 Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCExecuteResponse.cs create mode 100644 Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCQueryRequest.cs diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/PythonV2Environment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonV2Environment.cs index 180893774..4d75e1b8f 100644 --- a/Flow.Launcher.Core/ExternalPlugins/Environments/PythonV2Environment.cs +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/PythonV2Environment.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Flow.Launcher.Core.Plugin; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; @@ -8,6 +9,15 @@ namespace Flow.Launcher.Core.ExternalPlugins.Environments { internal override string Language => AllowedLanguage.PythonV2; + internal override PluginPair CreatePluginPair(string filePath, PluginMetadata metadata) + { + return new PluginPair + { + Plugin = new PythonPluginV2(filePath), + Metadata = metadata + }; + } + internal PythonV2Environment(List pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } } } diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj index 4077320bc..0188f7b46 100644 --- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj +++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj @@ -57,11 +57,16 @@ + + + + + \ No newline at end of file diff --git a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs index 477ee620d..f4ea43894 100644 --- a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs +++ b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs @@ -28,7 +28,7 @@ namespace Flow.Launcher.Core.Plugin public record JsonRPCResponseModel(int Id, JsonRPCErrorModel Error = default) : JsonRPCBase(Id, Error); public record JsonRPCQueryResponseModel(int Id, [property: JsonPropertyName("result")] List Result, - IReadOnlyDictionary SettingsChange = null, + IReadOnlyDictionary SettingsChanges = null, string DebugMessage = "", JsonRPCErrorModel Error = default) : JsonRPCResponseModel(Id, Error); diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs index 3a7fcb216..cfe134549 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs @@ -251,10 +251,18 @@ namespace Flow.Launcher.Core.Plugin return sourceBuffer; } - - protected override async Task> QueryRequestAsync(JsonRPCRequestModel request, CancellationToken token) + public override async Task> QueryAsync(Query query, CancellationToken token) { + var request = new JsonRPCRequestModel(RequestId++, + "query", + new object[] + { + query.Search + }, + Settings.Inner); + var output = await RequestAsync(request, token); + return await DeserializedResultAsync(output); } } diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs index e3386a52f..85b474157 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs @@ -69,7 +69,6 @@ namespace Flow.Launcher.Core.Plugin }; protected abstract Task ExecuteResultAsync(JsonRPCResult result); - protected abstract Task> QueryRequestAsync(JsonRPCRequestModel request, CancellationToken token); protected PortableSettings Settings { get; set; } @@ -96,7 +95,7 @@ namespace Flow.Launcher.Core.Plugin results.AddRange(queryResponseModel.Result); - Settings.UpdateSettings(queryResponseModel.SettingsChange); + Settings.UpdateSettings(queryResponseModel.SettingsChanges); return results; } @@ -123,19 +122,7 @@ namespace Flow.Launcher.Core.Plugin } } - public async Task> QueryAsync(Query query, CancellationToken token) - { - var request = new JsonRPCRequestModel(RequestId++, - "query", - new object[] - { - query.Search - }, - Settings.Inner); - - return await QueryRequestAsync(request, token); - - } + public abstract Task> QueryAsync(Query query, CancellationToken token); private async Task InitSettingAsync() diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs index bc5d00ed4..e1e79359d 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs @@ -7,77 +7,70 @@ using System.Threading; using System.Threading.Channels; using System.Threading.Tasks; using System.Windows.Controls; +using Flow.Launcher.Core.Plugin.JsonRPCV2Models; using Flow.Launcher.Plugin; +using StreamJsonRpc; + namespace Flow.Launcher.Core.Plugin { internal abstract class JsonRpcPluginV2 : JsonRPCPluginBase { public abstract string SupportedLanguage { get; set; } - + public const string JsonRpc = "JsonRPC"; - protected abstract Stream InputStream { get; set; } - protected abstract Stream OutputStream { get; set; } - protected abstract StreamReader ErrorStream { get; set; } - protected Channel InputMessageChannel { get; set; } + protected abstract JsonRpc Rpc { get; set; } - private (Task SendTask, Task ReceiveTask) MessageTask { get; set; } - private CancellationTokenSource MessageCancellationTokenSource { get; set; } + protected StreamReader ErrorStream { get; set; } - protected int RequestId; - private ConcurrentDictionary> RequestTaskDictionary { get; } = new(); - - // TODO: Switch to Async Task - private async void ReceiveMessageAsync(CancellationToken token) + protected override async Task ExecuteResultAsync(JsonRPCResult result) { - var response = - JsonSerializer.DeserializeAsyncEnumerable(OutputStream, cancellationToken: token); - - ArgumentNullException.ThrowIfNull(response); - - await foreach (var message in response.WithCancellation(token)) + try { - if (!RequestTaskDictionary.TryGetValue(message.Id, out var task)) - { - // Either Task is already handled or it is a invalid resopnse. - continue; - } - RequestTaskDictionary.Remove(message.Id, out _); - task.TrySetResult(message); + var res = await Rpc.InvokeAsync(result.JsonRPCAction.Method, argument: result.JsonRPCAction.Parameters); + + return res.Hide; + } + catch + { + return false; } } - // TODO: Switch to Async Task - private async void SendMessageAsync(PluginMetadata metadata, CancellationToken token) + public override async Task> QueryAsync(Query query, CancellationToken token) { - var fullMessage = new JsonRPCRequestMessage(metadata, InputMessageChannel.Reader.ReadAllAsync(token)); - await JsonSerializer.SerializeAsync(InputStream, fullMessage, cancellationToken: token); + try + { + var res = await Rpc.InvokeAsync("query", query); + + var results = ParseResults(res); + + return results; + } + catch + { + return new List(); + } } - protected override async Task> QueryRequestAsync(JsonRPCRequestModel query, CancellationToken token) - { - int currentRequestId = Interlocked.Add(ref RequestId, 1); - await InputMessageChannel.Writer.WriteAsync(query, token); - await Task.Delay(50, token); - await InputStream.FlushAsync(token); - var task = new TaskCompletionSource(); - RequestTaskDictionary[currentRequestId] = task; - var result = await task.Task; - //TODO: Parse Result - return new List(); - } + public override async Task InitAsync(PluginInitContext context) { await base.InitAsync(context); - InputMessageChannel = Channel.CreateUnbounded(); - MessageCancellationTokenSource = new CancellationTokenSource(); - SendMessageAsync(context.CurrentPluginMetadata, MessageCancellationTokenSource.Token); - ReceiveMessageAsync(MessageCancellationTokenSource.Token); - // MessageTask = - // (SendMessageAsync(context.CurrentPluginMetadata, MessageCancellationTokenSource.Token), - // ReceiveMessageAsync(MessageCancellationTokenSource.Token)); + + _ = ReadErrorAsync(); + + async Task ReadErrorAsync() + { + var error = await ErrorStream.ReadToEndAsync(); + + if (!string.IsNullOrEmpty(error)) + { + throw new Exception(error); + } + } } } } diff --git a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCExecuteResponse.cs b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCExecuteResponse.cs new file mode 100644 index 000000000..6a130f70f --- /dev/null +++ b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCExecuteResponse.cs @@ -0,0 +1,4 @@ +namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models +{ + public record JsonRPCExecuteResponse(bool Hide = true); +} diff --git a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCQueryRequest.cs b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCQueryRequest.cs new file mode 100644 index 000000000..003724a23 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCQueryRequest.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models +{ + public record JsonRPCQueryRequest( + List Results + ); +} diff --git a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs index 1e7a74a58..c3b47a79c 100644 --- a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs @@ -2,10 +2,14 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.Text; using System.Threading; using System.Threading.Tasks; +using System.Windows.Input; using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin; +using Microsoft.VisualStudio.Threading; +using StreamJsonRpc; namespace Flow.Launcher.Core.Plugin { @@ -13,11 +17,11 @@ namespace Flow.Launcher.Core.Plugin { private readonly ProcessStartInfo _startInfo; private Process _process; + public override string SupportedLanguage { get; set; } = AllowedLanguage.Python; - protected override Stream InputStream { get; set; } - protected override Stream OutputStream { get; set; } - protected override StreamReader ErrorStream { get; set; } + protected override JsonRpc Rpc { get; set; } + public PythonPluginV2(string filename) { @@ -49,23 +53,26 @@ namespace Flow.Launcher.Core.Plugin { throw new NotImplementedException(); } - protected override Task ExecuteResultAsync(JsonRPCResult result) - { - throw new NotImplementedException(); - } + public override async Task InitAsync(PluginInitContext context) { _startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath); _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; _process = Process.Start(_startInfo); - + ArgumentNullException.ThrowIfNull(_process); - - InputStream = _process.StandardInput.BaseStream; - OutputStream = _process.StandardOutput.BaseStream; - ErrorStream = _process.StandardError; - + + var formatter = new JsonMessageFormatter(); + var handler = new NewLineDelimitedMessageHandler(_process.StandardInput.BaseStream, + _process.StandardOutput.BaseStream, + formatter); + + Rpc = new JsonRpc(handler, context.API); + Rpc.StartListening(); + + _ = _process.StandardError.ReadToEndAsync().ContinueWith(e => throw new Exception(e.Result)); + await base.InitAsync(context); } } diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj index 1143f7f72..b0e1391e3 100644 --- a/Flow.Launcher/Flow.Launcher.csproj +++ b/Flow.Launcher/Flow.Launcher.csproj @@ -90,7 +90,7 @@ - + all diff --git a/Flow.Launcher/Notification.cs b/Flow.Launcher/Notification.cs index 57c1e88f2..bc130c834 100644 --- a/Flow.Launcher/Notification.cs +++ b/Flow.Launcher/Notification.cs @@ -31,12 +31,12 @@ namespace Flow.Launcher var Icon = !File.Exists(iconPath) ? Path.Combine(Constant.ProgramDirectory, "Images\\app.png") : iconPath; - - new ToastContentBuilder() - .AddText(title, hintMaxLines: 1) - .AddText(subTitle) - .AddAppLogoOverride(new Uri(Icon)) - .Show(); + + // new ToastContentBuilder() + // .AddText(title, hintMaxLines: 1) + // .AddText(subTitle) + // .AddAppLogoOverride(new Uri(Icon)) + // .Show(); } private static void LegacyShow(string title, string subTitle, string iconPath) From 32bbf1eaf02f5b5bfe10c09340f51748ca110c94 Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Sun, 25 Jun 2023 00:43:21 +0800 Subject: [PATCH 10/21] Finally make JSONRPC Bidirection work --- .../ExternalPlugins/PluginsManifest.cs | 2 +- Flow.Launcher.Core/Flow.Launcher.Core.csproj | 4 - .../Plugin/JsonRPCPluginBase.cs | 31 +- Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs | 23 +- .../JsonRPCV2Models/JsonRPCExecuteResponse.cs | 2 +- .../JsonRPCV2Models/JsonRPCPublicAPI.cs | 326 ++++++++++++++++++ Flow.Launcher.Core/Plugin/PythonPluginV2.cs | 52 ++- 7 files changed, 397 insertions(+), 43 deletions(-) create mode 100644 Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs diff --git a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs index e3f0e2a2f..1e30895cc 100644 --- a/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs +++ b/Flow.Launcher.Core/ExternalPlugins/PluginsManifest.cs @@ -12,7 +12,7 @@ namespace Flow.Launcher.Core.ExternalPlugins { public static class PluginsManifest { - private const string manifestFileUrl = "https://cdn.jsdelivr.net/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@plugin_api_v2/plugins.json"; + private const string manifestFileUrl = "https://jsdelivr.bobocdn.tk/gh/Flow-Launcher/Flow.Launcher.PluginsManifest@plugin_api_v2/plugins.json"; private static readonly SemaphoreSlim manifestUpdateLock = new(1); diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj index 35a998896..133ed02e3 100644 --- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj +++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj @@ -64,9 +64,5 @@ - - - - \ No newline at end of file diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs index 85b474157..18f787018 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs @@ -41,9 +41,11 @@ namespace Flow.Launcher.Core.Plugin private int RequestId { get; set; } - private string SettingConfigurationPath => Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml"); + private string SettingConfigurationPath => + Path.Combine(Context.CurrentPluginMetadata.PluginDirectory, "SettingsTemplate.yaml"); - private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, Context.CurrentPluginMetadata.Name, "Settings.json"); + private string SettingPath => Path.Combine(DataLocation.PluginSettingsDirectory, + Context.CurrentPluginMetadata.Name, "Settings.json"); public abstract List LoadContextMenus(Result selectedResult); @@ -57,10 +59,7 @@ namespace Flow.Launcher.Core.Plugin // see: https://github.com/dotnet/runtime/issues/39152 IgnoreNullValues = true, #pragma warning restore SYSLIB0020 // Type or member is obsolete - Converters = - { - new JsonObjectConverter() - } + Converters = { new JsonObjectConverter() } }; protected static readonly JsonSerializerOptions RequestSerializeOption = new() @@ -83,9 +82,9 @@ namespace Flow.Launcher.Core.Plugin foreach (var result in queryResponseModel.Result) { - result.AsyncAction = async c => + result.AsyncAction = async _ => { - Settings.UpdateSettings(result.SettingsChange); + Settings?.UpdateSettings(result.SettingsChange); return await ExecuteResultAsync(result); }; @@ -95,7 +94,7 @@ namespace Flow.Launcher.Core.Plugin results.AddRange(queryResponseModel.Result); - Settings.UpdateSettings(queryResponseModel.SettingsChanges); + Settings?.UpdateSettings(queryResponseModel.SettingsChanges); return results; } @@ -130,18 +129,18 @@ namespace Flow.Launcher.Core.Plugin if (!File.Exists(SettingConfigurationPath)) return; - var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance).Build(); - var configuration = deserializer.Deserialize(await File.ReadAllTextAsync(SettingConfigurationPath)); + var deserializer = new DeserializerBuilder().WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + var configuration = + deserializer.Deserialize( + await File.ReadAllTextAsync(SettingConfigurationPath)); Settings ??= new PortableSettings { - Configuration = configuration, - SettingPath = SettingPath, - API = Context.API + Configuration = configuration, SettingPath = SettingPath, API = Context.API }; await Settings.InitializeAsync(); - } public virtual async Task InitAsync(PluginInitContext context) @@ -154,10 +153,10 @@ namespace Flow.Launcher.Core.Plugin { Settings?.Save(); } + public Control CreateSettingPanel() { return Settings.CreateSettingPanel(); } } - } diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs index e1e79359d..df9be0d79 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs @@ -1,12 +1,8 @@ using System; -using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; -using System.Text.Json; using System.Threading; -using System.Threading.Channels; using System.Threading.Tasks; -using System.Windows.Controls; using Flow.Launcher.Core.Plugin.JsonRPCV2Models; using Flow.Launcher.Plugin; using StreamJsonRpc; @@ -14,13 +10,13 @@ using StreamJsonRpc; namespace Flow.Launcher.Core.Plugin { - internal abstract class JsonRpcPluginV2 : JsonRPCPluginBase + internal abstract class JsonRPCPluginV2 : JsonRPCPluginBase, IDisposable { public abstract string SupportedLanguage { get; set; } public const string JsonRpc = "JsonRPC"; - protected abstract JsonRpc Rpc { get; set; } + protected abstract JsonRpc RPC { get; set; } protected StreamReader ErrorStream { get; set; } @@ -29,7 +25,8 @@ namespace Flow.Launcher.Core.Plugin { try { - var res = await Rpc.InvokeAsync(result.JsonRPCAction.Method, argument: result.JsonRPCAction.Parameters); + var res = await RPC.InvokeAsync(result.JsonRPCAction.Method, + argument: result.JsonRPCAction.Parameters); return res.Hide; } @@ -43,7 +40,9 @@ namespace Flow.Launcher.Core.Plugin { try { - var res = await Rpc.InvokeAsync("query", query); + var res = await RPC.InvokeWithCancellationAsync("query", + new[] { query }, + token); var results = ParseResults(res); @@ -51,7 +50,7 @@ namespace Flow.Launcher.Core.Plugin } catch { - return new List(); + return new List(); } } @@ -72,5 +71,11 @@ namespace Flow.Launcher.Core.Plugin } } } + + public void Dispose() + { + RPC?.Dispose(); + ErrorStream?.Dispose(); + } } } diff --git a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCExecuteResponse.cs b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCExecuteResponse.cs index 6a130f70f..632bb9501 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCExecuteResponse.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCExecuteResponse.cs @@ -1,4 +1,4 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models { - public record JsonRPCExecuteResponse(bool Hide = true); + public abstract record JsonRPCExecuteResponse(bool Hide = true); } diff --git a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs new file mode 100644 index 000000000..a65d5db22 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs @@ -0,0 +1,326 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Runtime.CompilerServices; +using System.Threading; +using System.Threading.Tasks; +using Flow.Launcher.Plugin; +using Flow.Launcher.Plugin.SharedModels; + +namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models +{ + public class JsonRPCPublicAPI + { + private IPublicAPI _api; + + public JsonRPCPublicAPI(IPublicAPI api) + { + _api = api; + } + + /// + /// Change Flow.Launcher query + /// + /// query text + /// + /// Force requery. By default, Flow Launcher will not fire query if your query is same with existing one. + /// Set this to to force Flow Launcher requerying + /// + public void ChangeQuery(string query, bool requery = false) + { + _api.ChangeQuery(query, requery); + } + + /// + /// Restart Flow Launcher + /// + public void RestartApp() + { + _api.RestartApp(); + } + + /// + /// Run a shell command + /// + /// The command or program to run + /// the shell type to run, e.g. powershell.exe + /// Thrown when unable to find the file specified in the command + /// Thrown when error occurs during the execution of the command + public void ShellRun(string cmd, string filename = "cmd.exe") + { + _api.ShellRun(cmd, filename); + } + + /// + /// Copies the passed in text and shows a message indicating whether the operation was completed successfully. + /// When directCopy is set to true and passed in text is the path to a file or directory, + /// the actual file/directory will be copied to clipboard. Otherwise the text itself will still be copied to clipboard. + /// + /// Text to save on clipboard + /// When true it will directly copy the file/folder from the path specified in text + /// Whether to show the default notification from this method after copy is done. + /// It will show file/folder/text is copied successfully. + /// Turn this off to show your own notification after copy is done.> + public void CopyToClipboard(string text, bool directCopy = false, bool showDefaultNotification = true) + { + _api.CopyToClipboard(text, directCopy, showDefaultNotification); + } + + /// + /// Save everything, all of Flow Launcher and plugins' data and settings + /// + public void SaveAppAllSettings() + { + _api.SaveAppAllSettings(); + } + + /// + /// Save all Flow's plugins settings + /// + public void SavePluginSettings() + { + _api.SavePluginSettings(); + } + + /// + /// Reloads any Plugins that have the + /// IReloadable implemented. It refeshes + /// Plugin's in memory data with new content + /// added by user. + /// + public Task ReloadAllPluginDataAsync() + { + return _api.ReloadAllPluginData(); + } + + /// + /// Check for new Flow Launcher update + /// + public void CheckForNewUpdate() + { + _api.CheckForNewUpdate(); + } + + /// + /// Show the error message using Flow's standard error icon. + /// + /// Message title + /// Optional message subtitle + public void ShowMsgError(string title, string subTitle = "") + { + _api.ShowMsgError(title, subTitle); + } + + /// + /// Show the MainWindow when hiding + /// + public void ShowMainWindow() + { + _api.ShowMainWindow(); + } + + /// + /// Hide MainWindow + /// + public void HideMainWindow() + { + _api.HideMainWindow(); + } + + /// + /// Representing whether the main window is visible + /// + /// + public bool IsMainWindowVisible() + { + return _api.IsMainWindowVisible(); + } + + /// + /// Show message box + /// + /// Message title + /// Message subtitle + /// Message icon path (relative path to your plugin folder) + public void ShowMsg(string title, string subTitle = "", string iconPath = "") + { + _api.ShowMsg(title, subTitle, iconPath); + } + + /// + /// Show message box + /// + /// Message title + /// Message subtitle + /// Message icon path (relative path to your plugin folder) + /// when true will use main windows as the owner + public void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true) + { + _api.ShowMsg(title, subTitle, iconPath, useMainWindowAsOwner); + } + + /// + /// Open setting dialog + /// + public void OpenSettingDialog() + { + _api.OpenSettingDialog(); + } + + /// + /// Get translation of current language + /// You need to implement IPluginI18n if you want to support multiple languages for your plugin + /// + /// + /// + public string GetTranslation(string key) + { + return _api.GetTranslation(key); + } + + /// + /// Get all loaded plugins + /// + /// + public List GetAllPlugins() + { + return _api.GetAllPlugins(); + } + + + /// + /// Fuzzy Search the string with the given query. This is the core search mechanism Flow uses + /// + /// Query string + /// The string that will be compared against the query + /// Match results + public MatchResult FuzzySearch(string query, string stringToCompare) + { + return _api.FuzzySearch(query, stringToCompare); + } + + /// + /// Http download the spefic url and return as string + /// + /// URL to call Http Get + /// Cancellation Token + /// Task to get string result + public Task HttpGetStringAsync(string url, CancellationToken token = default) + { + return _api.HttpGetStringAsync(url, token); + } + + /// + /// Http download the spefic url and return as stream + /// + /// URL to call Http Get + /// Cancellation Token + /// Task to get stream result + public Task HttpGetStreamAsync(string url, CancellationToken token = default) + { + return _api.HttpGetStreamAsync(url, token); + } + + /// + /// Download the specific url to a cretain file path + /// + /// URL to download file + /// path to save downloaded file + /// place to store file + /// Task showing the progress + public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, + CancellationToken token = default) + { + return _api.HttpDownloadAsync(url, filePath, token); + } + + /// + /// Add ActionKeyword for specific plugin + /// + /// ID for plugin that needs to add action keyword + /// The actionkeyword that is supposed to be added + public void AddActionKeyword(string pluginId, string newActionKeyword) + { + _api.AddActionKeyword(pluginId, newActionKeyword); + } + + /// + /// Remove ActionKeyword for specific plugin + /// + /// ID for plugin that needs to remove action keyword + /// The actionkeyword that is supposed to be removed + public void RemoveActionKeyword(string pluginId, string oldActionKeyword) + { + _api.RemoveActionKeyword(pluginId, oldActionKeyword); + } + + /// + /// Check whether specific ActionKeyword is assigned to any of the plugin + /// + /// The actionkeyword for checking + /// True if the actionkeyword is already assigned, False otherwise + public bool ActionKeywordAssigned(string actionKeyword) + { + return _api.ActionKeywordAssigned(actionKeyword); + } + + /// + /// Log debug message + /// Message will only be logged in Debug mode + /// + public void LogDebug(string className, string message, [CallerMemberName] string methodName = "") + { + _api.LogDebug(className, message, methodName); + } + + /// + /// Log info message + /// + public void LogInfo(string className, string message, [CallerMemberName] string methodName = "") + { + _api.LogInfo(className, message, methodName); + } + + /// + /// Log warning message + /// + public void LogWarn(string className, string message, [CallerMemberName] string methodName = "") + { + _api.LogWarn(className, message, methodName); + } + + + /// + /// Open directory in an explorer configured by user via Flow's Settings. The default is Windows Explorer + /// + /// Directory Path to open + /// Extra FileName Info + public void OpenDirectory(string DirectoryPath, string FileNameOrFilePath = null) + { + _api.OpenDirectory(DirectoryPath, FileNameOrFilePath); + } + + + /// + /// Opens the URL with the given string. + /// The browser and mode used is based on what's configured in Flow's default browser settings. + /// Non-C# plugins should use this method. + /// + public void OpenUrl(string url, bool? inPrivate = null) + { + _api.OpenUrl(url, inPrivate); + } + + + /// + /// Opens the application URI with the given string, e.g. obsidian://search-query-example + /// Non-C# plugins should use this method + /// + public void OpenAppUri(string appUri) + { + _api.OpenAppUri(appUri); + } + } +} diff --git a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs index c3b47a79c..59bb0bd84 100644 --- a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs @@ -6,6 +6,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows.Input; +using Flow.Launcher.Core.Plugin.JsonRPCV2Models; using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin; using Microsoft.VisualStudio.Threading; @@ -13,14 +14,14 @@ using StreamJsonRpc; namespace Flow.Launcher.Core.Plugin { - internal class PythonPluginV2 : JsonRpcPluginV2 + internal class PythonPluginV2 : JsonRPCPluginV2, IReloadable, IDisposable { private readonly ProcessStartInfo _startInfo; private Process _process; public override string SupportedLanguage { get; set; } = AllowedLanguage.Python; - protected override JsonRpc Rpc { get; set; } + protected override JsonRpc RPC { get; set; } public PythonPluginV2(string filename) @@ -53,7 +54,7 @@ namespace Flow.Launcher.Core.Plugin { throw new NotImplementedException(); } - + public override async Task InitAsync(PluginInitContext context) { _startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath); @@ -63,17 +64,44 @@ namespace Flow.Launcher.Core.Plugin ArgumentNullException.ThrowIfNull(_process); - var formatter = new JsonMessageFormatter(); - var handler = new NewLineDelimitedMessageHandler(_process.StandardInput.BaseStream, - _process.StandardOutput.BaseStream, - formatter); - - Rpc = new JsonRpc(handler, context.API); - Rpc.StartListening(); - - _ = _process.StandardError.ReadToEndAsync().ContinueWith(e => throw new Exception(e.Result)); + SetupJsonRPC(_process, context.API); await base.InitAsync(context); } + + public void Dispose() + { + _process.Kill(true); + _process.Dispose(); + base.Dispose(); + } + + public void ReloadData() + { + var oldProcess = _process; + _process = Process.Start(_startInfo); + ArgumentNullException.ThrowIfNull(_process); + SetupJsonRPC(_process, Context.API); + oldProcess.Kill(true); + oldProcess.Dispose(); + } + + private void SetupJsonRPC(Process process, IPublicAPI api) + { + var formatter = new JsonMessageFormatter(); + var handler = new NewLineDelimitedMessageHandler(process.StandardInput.BaseStream, + process.StandardOutput.BaseStream, + formatter); + + RPC = new JsonRpc(handler, new JsonRPCPublicAPI(api)); + RPC.SynchronizationContext = null; + RPC.StartListening(); + + _ = process.StandardError.ReadToEndAsync().ContinueWith(e => + { + if (e.Result.Length > 0) + throw new Exception(e.Result); + }); + } } } From cb6fb80e7089751c642ab82c48fb7d1c7a7940fd Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Sun, 25 Jun 2023 12:21:45 +0800 Subject: [PATCH 11/21] Change abstract back to normal class for JsonRPCExecuteResponse.cs; Cleanup unused classes --- Flow.Launcher.Core/Plugin/JsonPRCModel.cs | 1 - .../Plugin/JsonRPCV2Models/JsonRPCExecuteResponse.cs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs index 41d961a6b..48606eea4 100644 --- a/Flow.Launcher.Core/Plugin/JsonPRCModel.cs +++ b/Flow.Launcher.Core/Plugin/JsonPRCModel.cs @@ -19,7 +19,6 @@ using System.Text.Json; namespace Flow.Launcher.Core.Plugin { - public record JsonRPCRequestMessage(PluginMetadata PluginMetadata, IAsyncEnumerable Requests); public record JsonRPCBase(int Id, JsonRPCErrorModel Error = default); public record JsonRPCErrorModel(int Code, string Message, string Data); diff --git a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCExecuteResponse.cs b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCExecuteResponse.cs index 632bb9501..6a130f70f 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCExecuteResponse.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCExecuteResponse.cs @@ -1,4 +1,4 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models { - public abstract record JsonRPCExecuteResponse(bool Hide = true); + public record JsonRPCExecuteResponse(bool Hide = true); } From a9711565192788816ec540edc5d2e6db99f86ef2 Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Sun, 25 Jun 2023 12:43:47 +0800 Subject: [PATCH 12/21] Add Initialization Code --- Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs | 6 ++++-- Flow.Launcher.Core/Plugin/PythonPluginV2.cs | 6 ------ 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs index df9be0d79..ee7009f9a 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs @@ -40,7 +40,7 @@ namespace Flow.Launcher.Core.Plugin { try { - var res = await RPC.InvokeWithCancellationAsync("query", + var res = await RPC.InvokeWithCancellationAsync("query", new[] { query }, token); @@ -50,7 +50,7 @@ namespace Flow.Launcher.Core.Plugin } catch { - return new List(); + return new List(); } } @@ -61,6 +61,8 @@ namespace Flow.Launcher.Core.Plugin _ = ReadErrorAsync(); + await RPC.InvokeAsync("initialize", context); + async Task ReadErrorAsync() { var error = await ErrorStream.ReadToEndAsync(); diff --git a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs index 59bb0bd84..7b8ea7c76 100644 --- a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs @@ -96,12 +96,6 @@ namespace Flow.Launcher.Core.Plugin RPC = new JsonRpc(handler, new JsonRPCPublicAPI(api)); RPC.SynchronizationContext = null; RPC.StartListening(); - - _ = process.StandardError.ReadToEndAsync().ContinueWith(e => - { - if (e.Result.Length > 0) - throw new Exception(e.Result); - }); } } } From 83a61109d772ac3766e7a6c9d179a4b7bdb14386 Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Mon, 3 Jul 2023 12:23:30 +0800 Subject: [PATCH 13/21] fix error stream issue --- Flow.Launcher.Core/Plugin/PythonPluginV2.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs index 7b8ea7c76..83dce2187 100644 --- a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs @@ -93,6 +93,8 @@ namespace Flow.Launcher.Core.Plugin process.StandardOutput.BaseStream, formatter); + ErrorStream = _process.StandardError; + RPC = new JsonRpc(handler, new JsonRPCPublicAPI(api)); RPC.SynchronizationContext = null; RPC.StartListening(); From 0459d6e4fa67d8e7454893ff50826a17eefb7bdf Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Mon, 3 Jul 2023 12:58:44 +0800 Subject: [PATCH 14/21] fix v1 plugin issue - Settings NullReference - ExecutablePlugin not working --- Flow.Launcher.Core/Plugin/ExecutablePlugin.cs | 5 +++-- Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs b/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs index a7bbccfec..857122aa6 100644 --- a/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs +++ b/Flow.Launcher.Core/Plugin/ExecutablePlugin.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.IO; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; @@ -27,14 +28,14 @@ namespace Flow.Launcher.Core.Plugin protected override Task RequestAsync(JsonRPCRequestModel request, CancellationToken token = default) { // since this is not static, request strings will build up in ArgumentList if index is not specified - _startInfo.ArgumentList[0] = request.ToString(); + _startInfo.ArgumentList[0] = JsonSerializer.Serialize(request, RequestSerializeOption); return ExecuteAsync(_startInfo, token); } protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default) { // since this is not static, request strings will build up in ArgumentList if index is not specified - _startInfo.ArgumentList[0] = rpcRequest.ToString(); + _startInfo.ArgumentList[0] = JsonSerializer.Serialize(rpcRequest, RequestSerializeOption); return Execute(_startInfo); } } diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs index a3f58b5b9..97c3c8981 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs @@ -256,7 +256,7 @@ namespace Flow.Launcher.Core.Plugin { query.Search }, - Settings.Inner); + Settings?.Inner); var output = await RequestAsync(request, token); From e1deefc1d271128e605985571ce31a8d782ba79a Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Mon, 3 Jul 2023 16:19:27 +0800 Subject: [PATCH 15/21] Add UpdateResults Functionality --- Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs | 41 +++++++++++++++++-- Flow.Launcher.Core/Plugin/PythonPluginV2.cs | 43 +++++++++----------- 2 files changed, 58 insertions(+), 26 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs index ee7009f9a..c6ef870c3 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.IO.Pipelines; using System.Threading; using System.Threading.Tasks; using Flow.Launcher.Core.Plugin.JsonRPCV2Models; @@ -10,16 +11,18 @@ using StreamJsonRpc; namespace Flow.Launcher.Core.Plugin { - internal abstract class JsonRPCPluginV2 : JsonRPCPluginBase, IDisposable + internal abstract class JsonRPCPluginV2 : JsonRPCPluginBase, IAsyncDisposable, IAsyncReloadable, IResultUpdated { public abstract string SupportedLanguage { get; set; } public const string JsonRpc = "JsonRPC"; - protected abstract JsonRpc RPC { get; set; } + protected abstract IDuplexPipe ClientPipe { get; set; } protected StreamReader ErrorStream { get; set; } + private JsonRpc RPC { get; set; } + protected override async Task ExecuteResultAsync(JsonRPCResult result) { @@ -59,6 +62,8 @@ namespace Flow.Launcher.Core.Plugin { await base.InitAsync(context); + SetupJsonRPC(); + _ = ReadErrorAsync(); await RPC.InvokeAsync("initialize", context); @@ -74,10 +79,40 @@ namespace Flow.Launcher.Core.Plugin } } - public void Dispose() + public event ResultUpdatedEventHandler ResultsUpdated; + + + private void SetupJsonRPC() + { + var formatter = new JsonMessageFormatter(); + var handler = new NewLineDelimitedMessageHandler(ClientPipe, + formatter); + + RPC = new JsonRpc(handler, new JsonRPCPublicAPI(Context.API)); + + RPC.AddLocalRpcMethod("UpdateResults", new Action((rawQuery, response) => + { + var results = ParseResults(response); + ResultsUpdated?.Invoke(this, new ResultUpdatedEventArgs { Query = new Query() + { + RawQuery = rawQuery + }, Results = results }); + })); + RPC.SynchronizationContext = null; + RPC.StartListening(); + } + + public virtual Task ReloadDataAsync() + { + SetupJsonRPC(); + return Task.CompletedTask; + } + + public virtual ValueTask DisposeAsync() { RPC?.Dispose(); ErrorStream?.Dispose(); + return ValueTask.CompletedTask; } } } diff --git a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs index 83dce2187..98b55f896 100644 --- a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Diagnostics; using System.IO; +using System.IO.Pipelines; using System.Text; using System.Threading; using System.Threading.Tasks; @@ -10,18 +11,19 @@ using Flow.Launcher.Core.Plugin.JsonRPCV2Models; using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin; using Microsoft.VisualStudio.Threading; +using Nerdbank.Streams; using StreamJsonRpc; namespace Flow.Launcher.Core.Plugin { - internal class PythonPluginV2 : JsonRPCPluginV2, IReloadable, IDisposable + internal class PythonPluginV2 : JsonRPCPluginV2 { private readonly ProcessStartInfo _startInfo; private Process _process; public override string SupportedLanguage { get; set; } = AllowedLanguage.Python; - protected override JsonRpc RPC { get; set; } + protected override IDuplexPipe ClientPipe { get; set; } public PythonPluginV2(string filename) @@ -61,43 +63,38 @@ namespace Flow.Launcher.Core.Plugin _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; _process = Process.Start(_startInfo); - ArgumentNullException.ThrowIfNull(_process); - - SetupJsonRPC(_process, context.API); + + SetupPipe(_process); await base.InitAsync(context); } - public void Dispose() + public override async ValueTask DisposeAsync() { _process.Kill(true); + await _process.WaitForExitAsync(); _process.Dispose(); - base.Dispose(); + await base.DisposeAsync(); } - public void ReloadData() + private void SetupPipe(Process process) + { + var (reader, writer) = (PipeReader.Create(process.StandardOutput.BaseStream), + PipeWriter.Create(process.StandardInput.BaseStream)); + ClientPipe = new DuplexPipe(reader, writer); + } + + public override async Task ReloadDataAsync() { var oldProcess = _process; _process = Process.Start(_startInfo); ArgumentNullException.ThrowIfNull(_process); - SetupJsonRPC(_process, Context.API); + SetupPipe(_process); + await base.ReloadDataAsync(); oldProcess.Kill(true); + await oldProcess.WaitForExitAsync(); oldProcess.Dispose(); } - - private void SetupJsonRPC(Process process, IPublicAPI api) - { - var formatter = new JsonMessageFormatter(); - var handler = new NewLineDelimitedMessageHandler(process.StandardInput.BaseStream, - process.StandardOutput.BaseStream, - formatter); - - ErrorStream = _process.StandardError; - - RPC = new JsonRpc(handler, new JsonRPCPublicAPI(api)); - RPC.SynchronizationContext = null; - RPC.StartListening(); - } } } From c50d98c5e2dd1726c973b13917ef4e28308b49fb Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Mon, 3 Jul 2023 16:44:50 +0800 Subject: [PATCH 16/21] Abstract out ProcessStreamPluginV2.cs and add ExecutablePluginV2.cs --- .../Plugin/ExecutablePluginV2.cs | 26 +++++++ Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs | 7 ++ .../Plugin/ProcessStreamPluginV2.cs | 68 +++++++++++++++++++ Flow.Launcher.Core/Plugin/PythonPluginV2.cs | 66 ++---------------- 4 files changed, 107 insertions(+), 60 deletions(-) create mode 100644 Flow.Launcher.Core/Plugin/ExecutablePluginV2.cs create mode 100644 Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs diff --git a/Flow.Launcher.Core/Plugin/ExecutablePluginV2.cs b/Flow.Launcher.Core/Plugin/ExecutablePluginV2.cs new file mode 100644 index 000000000..ee1b315c2 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/ExecutablePluginV2.cs @@ -0,0 +1,26 @@ +using System.Diagnostics; +using System.IO; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; + +namespace Flow.Launcher.Core.Plugin +{ + internal sealed class ExecutablePluginV2 : ProcessStreamPluginV2 + { + protected override ProcessStartInfo StartInfo { get; set; } + + public ExecutablePluginV2(string filename) + { + StartInfo = new ProcessStartInfo + { + FileName = filename, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + } + + } +} diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs index c6ef870c3..60130843e 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs @@ -6,7 +6,9 @@ using System.Threading; using System.Threading.Tasks; using Flow.Launcher.Core.Plugin.JsonRPCV2Models; using Flow.Launcher.Plugin; +using Microsoft.VisualStudio.Threading; using StreamJsonRpc; +using IAsyncDisposable = System.IAsyncDisposable; namespace Flow.Launcher.Core.Plugin @@ -39,6 +41,11 @@ namespace Flow.Launcher.Core.Plugin } } + public override List LoadContextMenus(Result selectedResult) + { + throw new NotImplementedException(); + } + public override async Task> QueryAsync(Query query, CancellationToken token) { try diff --git a/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs b/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs new file mode 100644 index 000000000..be35d481c --- /dev/null +++ b/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs @@ -0,0 +1,68 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.IO.Pipelines; +using System.Threading.Tasks; +using Flow.Launcher.Infrastructure; +using Flow.Launcher.Plugin; +using Nerdbank.Streams; + +namespace Flow.Launcher.Core.Plugin +{ + internal abstract class ProcessStreamPluginV2 : JsonRPCPluginV2 + { + + public override string SupportedLanguage { get; set; } + protected override IDuplexPipe ClientPipe { get; set; } + + protected abstract ProcessStartInfo StartInfo { get; set; } + + public Process ClientProcess { get; set; } + + public override async Task InitAsync(PluginInitContext context) + { + StartInfo.EnvironmentVariables["FLOW_VERSION"] = Constant.Version; + StartInfo.EnvironmentVariables["FLOW_PROGRAM_DIRECTORY"] = Constant.ProgramDirectory; + StartInfo.EnvironmentVariables["FLOW_APPLICATION_DIRECTORY"] = Constant.ApplicationDirectory; + + StartInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath); + StartInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; + + ClientProcess = Process.Start(StartInfo); + ArgumentNullException.ThrowIfNull(ClientProcess); + + SetupPipe(ClientProcess); + + await base.InitAsync(context); + } + + private void SetupPipe(Process process) + { + var (reader, writer) = (PipeReader.Create(process.StandardOutput.BaseStream), + PipeWriter.Create(process.StandardInput.BaseStream)); + ClientPipe = new DuplexPipe(reader, writer); + } + + + public override async Task ReloadDataAsync() + { + var oldProcess = ClientProcess; + ClientProcess = Process.Start(StartInfo); + ArgumentNullException.ThrowIfNull(ClientProcess); + SetupPipe(ClientProcess); + await base.ReloadDataAsync(); + oldProcess.Kill(true); + await oldProcess.WaitForExitAsync(); + oldProcess.Dispose(); + } + + + public override async ValueTask DisposeAsync() + { + ClientProcess.Kill(true); + await ClientProcess.WaitForExitAsync(); + ClientProcess.Dispose(); + await base.DisposeAsync(); + } + } +} diff --git a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs index 98b55f896..09c1a069a 100644 --- a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs @@ -16,19 +16,16 @@ using StreamJsonRpc; namespace Flow.Launcher.Core.Plugin { - internal class PythonPluginV2 : JsonRPCPluginV2 + internal sealed class PythonPluginV2 : ProcessStreamPluginV2 { - private readonly ProcessStartInfo _startInfo; - private Process _process; - public override string SupportedLanguage { get; set; } = AllowedLanguage.Python; protected override IDuplexPipe ClientPipe { get; set; } - - + protected override ProcessStartInfo StartInfo { get; set; } + public PythonPluginV2(string filename) { - _startInfo = new ProcessStartInfo + StartInfo = new ProcessStartInfo { FileName = filename, UseShellExecute = false, @@ -40,61 +37,10 @@ namespace Flow.Launcher.Core.Plugin // temp fix for issue #667 var path = Path.Combine(Constant.ProgramDirectory, JsonRpc); - _startInfo.EnvironmentVariables["PYTHONPATH"] = path; - - _startInfo.EnvironmentVariables["FLOW_VERSION"] = Constant.Version; - _startInfo.EnvironmentVariables["FLOW_PROGRAM_DIRECTORY"] = Constant.ProgramDirectory; - _startInfo.EnvironmentVariables["FLOW_APPLICATION_DIRECTORY"] = Constant.ApplicationDirectory; - + StartInfo.EnvironmentVariables["PYTHONPATH"] = path; //Add -B flag to tell python don't write .py[co] files. Because .pyc contains location infos which will prevent python portable - _startInfo.ArgumentList.Add("-B"); - } - - - public override List LoadContextMenus(Result selectedResult) - { - throw new NotImplementedException(); - } - - public override async Task InitAsync(PluginInitContext context) - { - _startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath); - _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; - - _process = Process.Start(_startInfo); - ArgumentNullException.ThrowIfNull(_process); - - SetupPipe(_process); - - await base.InitAsync(context); - } - - public override async ValueTask DisposeAsync() - { - _process.Kill(true); - await _process.WaitForExitAsync(); - _process.Dispose(); - await base.DisposeAsync(); - } - - private void SetupPipe(Process process) - { - var (reader, writer) = (PipeReader.Create(process.StandardOutput.BaseStream), - PipeWriter.Create(process.StandardInput.BaseStream)); - ClientPipe = new DuplexPipe(reader, writer); - } - - public override async Task ReloadDataAsync() - { - var oldProcess = _process; - _process = Process.Start(_startInfo); - ArgumentNullException.ThrowIfNull(_process); - SetupPipe(_process); - await base.ReloadDataAsync(); - oldProcess.Kill(true); - await oldProcess.WaitForExitAsync(); - oldProcess.Dispose(); + StartInfo.ArgumentList.Add("-B"); } } } From 29aefee390e606b4eaad59708519d6b557431713 Mon Sep 17 00:00:00 2001 From: Jeremy Wu Date: Wed, 23 Aug 2023 08:37:43 +1000 Subject: [PATCH 17/21] remove irrelevant comment --- Flow.Launcher.Core/Plugin/PythonPlugin.cs | 1 - Flow.Launcher.Core/Plugin/PythonPluginV2.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/PythonPlugin.cs b/Flow.Launcher.Core/Plugin/PythonPlugin.cs index d5258565e..536e69b3d 100644 --- a/Flow.Launcher.Core/Plugin/PythonPlugin.cs +++ b/Flow.Launcher.Core/Plugin/PythonPlugin.cs @@ -23,7 +23,6 @@ namespace Flow.Launcher.Core.Plugin RedirectStandardError = true, }; - // temp fix for issue #667 var path = Path.Combine(Constant.ProgramDirectory, JsonRPC); _startInfo.EnvironmentVariables["PYTHONPATH"] = path; diff --git a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs index 09c1a069a..842dc5cad 100644 --- a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs @@ -35,7 +35,6 @@ namespace Flow.Launcher.Core.Plugin RedirectStandardInput = true }; - // temp fix for issue #667 var path = Path.Combine(Constant.ProgramDirectory, JsonRpc); StartInfo.EnvironmentVariables["PYTHONPATH"] = path; From 20f23b01bb623c448cfa076a644d08d947f269c0 Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Mon, 28 Aug 2023 18:57:12 +0800 Subject: [PATCH 18/21] update streamjsonrpc and use systemtextjsonformatter --- Flow.Launcher.Core/Flow.Launcher.Core.csproj | 2 +- Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj index 133ed02e3..f9e057b49 100644 --- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj +++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj @@ -57,7 +57,7 @@ - + diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs index 60130843e..305ba9b65 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginV2.cs @@ -91,7 +91,7 @@ namespace Flow.Launcher.Core.Plugin private void SetupJsonRPC() { - var formatter = new JsonMessageFormatter(); + var formatter = new SystemTextJsonFormatter(); var handler = new NewLineDelimitedMessageHandler(ClientPipe, formatter); From c67d0faeaf65e2b5c71dc787b03ac2b6d094a3aa Mon Sep 17 00:00:00 2001 From: Jeremy Wu Date: Tue, 12 Sep 2023 08:23:34 +1000 Subject: [PATCH 19/21] remove obsolete comment --- Flow.Launcher.Core/Plugin/JsonRPCModelContext.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/Flow.Launcher.Core/Plugin/JsonRPCModelContext.cs b/Flow.Launcher.Core/Plugin/JsonRPCModelContext.cs index 7309e740b..b84801578 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCModelContext.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCModelContext.cs @@ -2,7 +2,6 @@ namespace Flow.Launcher.Core.Plugin { - // TODO: After Upgrading to .Net 7, adding Source Generating Context for IAsyncEnumerable JsonRPCMessage [JsonSerializable(typeof(JsonRPCQueryResponseModel))] public partial class JsonRPCQueryResponseModelContext : JsonSerializerContext From 1c63cd9350db5f0eb7207267fbda59a53c24f89d Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Tue, 31 Oct 2023 08:43:10 -0500 Subject: [PATCH 20/21] remove duplicate comment summary --- .../JsonRPCV2Models/JsonRPCPublicAPI.cs | 149 ------------------ 1 file changed, 149 deletions(-) diff --git a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs index a65d5db22..b8bfee591 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCV2Models/JsonRPCPublicAPI.cs @@ -20,304 +20,155 @@ namespace Flow.Launcher.Core.Plugin.JsonRPCV2Models _api = api; } - /// - /// Change Flow.Launcher query - /// - /// query text - /// - /// Force requery. By default, Flow Launcher will not fire query if your query is same with existing one. - /// Set this to to force Flow Launcher requerying - /// public void ChangeQuery(string query, bool requery = false) { _api.ChangeQuery(query, requery); } - /// - /// Restart Flow Launcher - /// public void RestartApp() { _api.RestartApp(); } - /// - /// Run a shell command - /// - /// The command or program to run - /// the shell type to run, e.g. powershell.exe - /// Thrown when unable to find the file specified in the command - /// Thrown when error occurs during the execution of the command public void ShellRun(string cmd, string filename = "cmd.exe") { _api.ShellRun(cmd, filename); } - /// - /// Copies the passed in text and shows a message indicating whether the operation was completed successfully. - /// When directCopy is set to true and passed in text is the path to a file or directory, - /// the actual file/directory will be copied to clipboard. Otherwise the text itself will still be copied to clipboard. - /// - /// Text to save on clipboard - /// When true it will directly copy the file/folder from the path specified in text - /// Whether to show the default notification from this method after copy is done. - /// It will show file/folder/text is copied successfully. - /// Turn this off to show your own notification after copy is done.> public void CopyToClipboard(string text, bool directCopy = false, bool showDefaultNotification = true) { _api.CopyToClipboard(text, directCopy, showDefaultNotification); } - /// - /// Save everything, all of Flow Launcher and plugins' data and settings - /// public void SaveAppAllSettings() { _api.SaveAppAllSettings(); } - /// - /// Save all Flow's plugins settings - /// public void SavePluginSettings() { _api.SavePluginSettings(); } - /// - /// Reloads any Plugins that have the - /// IReloadable implemented. It refeshes - /// Plugin's in memory data with new content - /// added by user. - /// public Task ReloadAllPluginDataAsync() { return _api.ReloadAllPluginData(); } - /// - /// Check for new Flow Launcher update - /// public void CheckForNewUpdate() { _api.CheckForNewUpdate(); } - /// - /// Show the error message using Flow's standard error icon. - /// - /// Message title - /// Optional message subtitle public void ShowMsgError(string title, string subTitle = "") { _api.ShowMsgError(title, subTitle); } - /// - /// Show the MainWindow when hiding - /// public void ShowMainWindow() { _api.ShowMainWindow(); } - /// - /// Hide MainWindow - /// public void HideMainWindow() { _api.HideMainWindow(); } - /// - /// Representing whether the main window is visible - /// - /// public bool IsMainWindowVisible() { return _api.IsMainWindowVisible(); } - /// - /// Show message box - /// - /// Message title - /// Message subtitle - /// Message icon path (relative path to your plugin folder) public void ShowMsg(string title, string subTitle = "", string iconPath = "") { _api.ShowMsg(title, subTitle, iconPath); } - /// - /// Show message box - /// - /// Message title - /// Message subtitle - /// Message icon path (relative path to your plugin folder) - /// when true will use main windows as the owner public void ShowMsg(string title, string subTitle, string iconPath, bool useMainWindowAsOwner = true) { _api.ShowMsg(title, subTitle, iconPath, useMainWindowAsOwner); } - /// - /// Open setting dialog - /// public void OpenSettingDialog() { _api.OpenSettingDialog(); } - /// - /// Get translation of current language - /// You need to implement IPluginI18n if you want to support multiple languages for your plugin - /// - /// - /// public string GetTranslation(string key) { return _api.GetTranslation(key); } - /// - /// Get all loaded plugins - /// - /// public List GetAllPlugins() { return _api.GetAllPlugins(); } - /// - /// Fuzzy Search the string with the given query. This is the core search mechanism Flow uses - /// - /// Query string - /// The string that will be compared against the query - /// Match results public MatchResult FuzzySearch(string query, string stringToCompare) { return _api.FuzzySearch(query, stringToCompare); } - /// - /// Http download the spefic url and return as string - /// - /// URL to call Http Get - /// Cancellation Token - /// Task to get string result public Task HttpGetStringAsync(string url, CancellationToken token = default) { return _api.HttpGetStringAsync(url, token); } - /// - /// Http download the spefic url and return as stream - /// - /// URL to call Http Get - /// Cancellation Token - /// Task to get stream result public Task HttpGetStreamAsync(string url, CancellationToken token = default) { return _api.HttpGetStreamAsync(url, token); } - /// - /// Download the specific url to a cretain file path - /// - /// URL to download file - /// path to save downloaded file - /// place to store file - /// Task showing the progress public Task HttpDownloadAsync([NotNull] string url, [NotNull] string filePath, CancellationToken token = default) { return _api.HttpDownloadAsync(url, filePath, token); } - /// - /// Add ActionKeyword for specific plugin - /// - /// ID for plugin that needs to add action keyword - /// The actionkeyword that is supposed to be added public void AddActionKeyword(string pluginId, string newActionKeyword) { _api.AddActionKeyword(pluginId, newActionKeyword); } - /// - /// Remove ActionKeyword for specific plugin - /// - /// ID for plugin that needs to remove action keyword - /// The actionkeyword that is supposed to be removed public void RemoveActionKeyword(string pluginId, string oldActionKeyword) { _api.RemoveActionKeyword(pluginId, oldActionKeyword); } - /// - /// Check whether specific ActionKeyword is assigned to any of the plugin - /// - /// The actionkeyword for checking - /// True if the actionkeyword is already assigned, False otherwise public bool ActionKeywordAssigned(string actionKeyword) { return _api.ActionKeywordAssigned(actionKeyword); } - /// - /// Log debug message - /// Message will only be logged in Debug mode - /// public void LogDebug(string className, string message, [CallerMemberName] string methodName = "") { _api.LogDebug(className, message, methodName); } - /// - /// Log info message - /// public void LogInfo(string className, string message, [CallerMemberName] string methodName = "") { _api.LogInfo(className, message, methodName); } - /// - /// Log warning message - /// public void LogWarn(string className, string message, [CallerMemberName] string methodName = "") { _api.LogWarn(className, message, methodName); } - - /// - /// Open directory in an explorer configured by user via Flow's Settings. The default is Windows Explorer - /// - /// Directory Path to open - /// Extra FileName Info public void OpenDirectory(string DirectoryPath, string FileNameOrFilePath = null) { _api.OpenDirectory(DirectoryPath, FileNameOrFilePath); } - /// - /// Opens the URL with the given string. - /// The browser and mode used is based on what's configured in Flow's default browser settings. - /// Non-C# plugins should use this method. - /// public void OpenUrl(string url, bool? inPrivate = null) { _api.OpenUrl(url, inPrivate); } - /// - /// Opens the application URI with the given string, e.g. obsidian://search-query-example - /// Non-C# plugins should use this method - /// public void OpenAppUri(string appUri) { _api.OpenAppUri(appUri); From f5b1b4f830eb423be029f6024530f80808ad8940 Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Fri, 3 Nov 2023 21:16:51 -0500 Subject: [PATCH 21/21] rename and add js/ts v2 --- .../Environments/JavaScriptV2Environment.cs | 14 ++++++ .../Environments/TypeScriptV2Environment.cs | 44 +++++++++++++++++++ .../Plugin/JsonRPCModelContext.cs | 20 --------- .../Plugin/JsonRPCPluginBase.cs | 4 +- ...leSettings.cs => JsonRPCPluginSettings.cs} | 2 +- Flow.Launcher.Core/Plugin/NodePlugin.cs | 8 ++-- Flow.Launcher.Core/Plugin/NodePluginV2.cs | 38 ++++++++++++++++ Flow.Launcher.Core/Plugin/PluginsLoader.cs | 40 ++++++++++++----- .../Plugin/ProcessStreamPluginV2.cs | 2 +- Flow.Launcher.Core/Plugin/PythonPluginV2.cs | 2 - Flow.Launcher.Plugin/AllowedLanguage.cs | 33 +++++++++++--- 11 files changed, 159 insertions(+), 48 deletions(-) create mode 100644 Flow.Launcher.Core/ExternalPlugins/Environments/JavaScriptV2Environment.cs create mode 100644 Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs delete mode 100644 Flow.Launcher.Core/Plugin/JsonRPCModelContext.cs rename Flow.Launcher.Core/Plugin/{PortableSettings.cs => JsonRPCPluginSettings.cs} (99%) create mode 100644 Flow.Launcher.Core/Plugin/NodePluginV2.cs diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/JavaScriptV2Environment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/JavaScriptV2Environment.cs new file mode 100644 index 000000000..6c8c5aa57 --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/JavaScriptV2Environment.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.Core.ExternalPlugins.Environments +{ + + internal class JavaScriptV2Environment : TypeScriptV2Environment + { + internal override string Language => AllowedLanguage.JavaScriptV2; + + internal JavaScriptV2Environment(List pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } + } +} diff --git a/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs new file mode 100644 index 000000000..11ed94d3f --- /dev/null +++ b/Flow.Launcher.Core/ExternalPlugins/Environments/TypeScriptV2Environment.cs @@ -0,0 +1,44 @@ +using System.Collections.Generic; +using Droplex; +using Flow.Launcher.Infrastructure.UserSettings; +using Flow.Launcher.Plugin.SharedCommands; +using Flow.Launcher.Plugin; +using System.IO; +using Flow.Launcher.Core.Plugin; + +namespace Flow.Launcher.Core.ExternalPlugins.Environments +{ + internal class TypeScriptV2Environment : AbstractPluginEnvironment + { + internal override string Language => AllowedLanguage.TypeScriptV2; + + internal override string EnvName => DataLocation.NodeEnvironmentName; + + internal override string EnvPath => Path.Combine(DataLocation.PluginEnvironmentsPath, EnvName); + + internal override string InstallPath => Path.Combine(EnvPath, "Node-v16.18.0"); + internal override string ExecutablePath => Path.Combine(InstallPath, "node-v16.18.0-win-x64\\node.exe"); + + internal override string PluginsSettingsFilePath { get => PluginSettings.NodeExecutablePath; set => PluginSettings.NodeExecutablePath = value; } + + internal TypeScriptV2Environment(List pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { } + + internal override void InstallEnvironment() + { + FilesFolders.RemoveFolderIfExists(InstallPath); + + DroplexPackage.Drop(App.nodejs_16_18_0, InstallPath).Wait(); + + PluginsSettingsFilePath = ExecutablePath; + } + + internal override PluginPair CreatePluginPair(string filePath, PluginMetadata metadata) + { + return new PluginPair + { + Plugin = new NodePluginV2(filePath), + Metadata = metadata + }; + } + } +} diff --git a/Flow.Launcher.Core/Plugin/JsonRPCModelContext.cs b/Flow.Launcher.Core/Plugin/JsonRPCModelContext.cs deleted file mode 100644 index b84801578..000000000 --- a/Flow.Launcher.Core/Plugin/JsonRPCModelContext.cs +++ /dev/null @@ -1,20 +0,0 @@ -using System.Text.Json.Serialization; - -namespace Flow.Launcher.Core.Plugin -{ - - [JsonSerializable(typeof(JsonRPCQueryResponseModel))] - public partial class JsonRPCQueryResponseModelContext : JsonSerializerContext - { - } - - [JsonSerializable(typeof(JsonRPCRequestModel))] - public partial class JsonRPCRequestModelContext : JsonSerializerContext - { - } - - [JsonSerializable(typeof(JsonRPCClientRequestModel))] - public partial class JsonRPCClientRequestModelContext : JsonSerializerContext - { - } -} diff --git a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs index 18f787018..330120c12 100644 --- a/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginBase.cs @@ -69,7 +69,7 @@ namespace Flow.Launcher.Core.Plugin protected abstract Task ExecuteResultAsync(JsonRPCResult result); - protected PortableSettings Settings { get; set; } + protected JsonRPCPluginSettings Settings { get; set; } protected List ParseResults(JsonRPCQueryResponseModel queryResponseModel) { @@ -135,7 +135,7 @@ namespace Flow.Launcher.Core.Plugin deserializer.Deserialize( await File.ReadAllTextAsync(SettingConfigurationPath)); - Settings ??= new PortableSettings + Settings ??= new JsonRPCPluginSettings { Configuration = configuration, SettingPath = SettingPath, API = Context.API }; diff --git a/Flow.Launcher.Core/Plugin/PortableSettings.cs b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs similarity index 99% rename from Flow.Launcher.Core/Plugin/PortableSettings.cs rename to Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs index 542460877..b87623c56 100644 --- a/Flow.Launcher.Core/Plugin/PortableSettings.cs +++ b/Flow.Launcher.Core/Plugin/JsonRPCPluginSettings.cs @@ -8,7 +8,7 @@ using Flow.Launcher.Plugin; namespace Flow.Launcher.Core.Plugin { - public class PortableSettings + public class JsonRPCPluginSettings { public required JsonRpcConfigurationModel Configuration { get; init; } diff --git a/Flow.Launcher.Core/Plugin/NodePlugin.cs b/Flow.Launcher.Core/Plugin/NodePlugin.cs index 8ea5c4b78..40eb057cb 100644 --- a/Flow.Launcher.Core/Plugin/NodePlugin.cs +++ b/Flow.Launcher.Core/Plugin/NodePlugin.cs @@ -1,5 +1,6 @@ using System.Diagnostics; using System.IO; +using System.Text.Json; using System.Threading; using System.Threading.Tasks; using Flow.Launcher.Plugin; @@ -27,23 +28,22 @@ namespace Flow.Launcher.Core.Plugin protected override Task RequestAsync(JsonRPCRequestModel request, CancellationToken token = default) { - _startInfo.ArgumentList[1] = request.ToString(); + _startInfo.ArgumentList[1] = JsonSerializer.Serialize(request); return ExecuteAsync(_startInfo, token); } protected override string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default) { // since this is not static, request strings will build up in ArgumentList if index is not specified - _startInfo.ArgumentList[1] = rpcRequest.ToString(); + _startInfo.ArgumentList[1] = JsonSerializer.Serialize(rpcRequest); return Execute(_startInfo); } public override async Task InitAsync(PluginInitContext context) { _startInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath); - _startInfo.ArgumentList.Add(string.Empty); - await base.InitAsync(context); _startInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; + await base.InitAsync(context); } } } diff --git a/Flow.Launcher.Core/Plugin/NodePluginV2.cs b/Flow.Launcher.Core/Plugin/NodePluginV2.cs new file mode 100644 index 000000000..6c95777f0 --- /dev/null +++ b/Flow.Launcher.Core/Plugin/NodePluginV2.cs @@ -0,0 +1,38 @@ +using System.Diagnostics; +using System.IO; +using System.IO.Pipelines; +using System.Threading; +using System.Threading.Tasks; +using Flow.Launcher.Plugin; + +namespace Flow.Launcher.Core.Plugin +{ + /// + /// Execution of JavaScript & TypeScript plugins + /// + internal class NodePluginV2 : ProcessStreamPluginV2 + { + public NodePluginV2(string filename) + { + StartInfo = new ProcessStartInfo + { + FileName = filename, + UseShellExecute = false, + CreateNoWindow = true, + RedirectStandardOutput = true, + RedirectStandardError = true + }; + } + + public override string SupportedLanguage { get; set; } + protected override ProcessStartInfo StartInfo { get; set; } + + public override async Task InitAsync(PluginInitContext context) + { + StartInfo.ArgumentList.Add(context.CurrentPluginMetadata.ExecuteFilePath); + StartInfo.ArgumentList.Add(string.Empty); + StartInfo.WorkingDirectory = context.CurrentPluginMetadata.PluginDirectory; + await base.InitAsync(context); + } + } +} diff --git a/Flow.Launcher.Core/Plugin/PluginsLoader.cs b/Flow.Launcher.Core/Plugin/PluginsLoader.cs index e6018d800..0f2e4f996 100644 --- a/Flow.Launcher.Core/Plugin/PluginsLoader.cs +++ b/Flow.Launcher.Core/Plugin/PluginsLoader.cs @@ -19,25 +19,33 @@ namespace Flow.Launcher.Core.Plugin public static List Plugins(List metadatas, PluginsSettings settings) { var dotnetPlugins = DotNetPlugins(metadatas); - + var pythonEnv = new PythonEnvironment(metadatas, settings); var pythonV2Env = new PythonV2Environment(metadatas, settings); var tsEnv = new TypeScriptEnvironment(metadatas, settings); var jsEnv = new JavaScriptEnvironment(metadatas, settings); + var tsV2Env = new TypeScriptV2Environment(metadatas, settings); + var jsV2Env = new JavaScriptV2Environment(metadatas, settings); var pythonPlugins = pythonEnv.Setup(); var pythonV2Plugins = pythonV2Env.Setup(); var tsPlugins = tsEnv.Setup(); var jsPlugins = jsEnv.Setup(); - + var tsV2Plugins = tsV2Env.Setup(); + var jsV2Plugins = jsV2Env.Setup(); + var executablePlugins = ExecutablePlugins(metadatas); - + var executableV2Plugins = ExecutableV2Plugins(metadatas); + var plugins = dotnetPlugins - .Concat(pythonPlugins) - .Concat(pythonV2Plugins) - .Concat(tsPlugins) - .Concat(jsPlugins) - .Concat(executablePlugins) - .ToList(); + .Concat(pythonPlugins) + .Concat(pythonV2Plugins) + .Concat(tsPlugins) + .Concat(jsPlugins) + .Concat(tsV2Plugins) + .Concat(jsV2Plugins) + .Concat(executablePlugins) + .Concat(executableV2Plugins) + .ToList(); return plugins; } @@ -96,7 +104,7 @@ namespace Flow.Launcher.Core.Plugin return; } - plugins.Add(new PluginPair {Plugin = plugin, Metadata = metadata}); + plugins.Add(new PluginPair { Plugin = plugin, Metadata = metadata }); }); metadata.InitTime += milliseconds; } @@ -121,7 +129,7 @@ namespace Flow.Launcher.Core.Plugin return plugins; } - public static IEnumerable ExecutablePlugins(IEnumerable source) + public static IEnumerable ExecutablePlugins(IEnumerable source) { return source .Where(o => o.Language.Equals(AllowedLanguage.Executable, StringComparison.OrdinalIgnoreCase)) @@ -130,5 +138,15 @@ namespace Flow.Launcher.Core.Plugin Plugin = new ExecutablePlugin(metadata.ExecuteFilePath), Metadata = metadata }); } + + public static IEnumerable ExecutableV2Plugins(IEnumerable source) + { + return source + .Where(o => o.Language.Equals(AllowedLanguage.ExecutableV2, StringComparison.OrdinalIgnoreCase)) + .Select(metadata => new PluginPair + { + Plugin = new ExecutablePluginV2(metadata.ExecuteFilePath), Metadata = metadata + }); + } } } diff --git a/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs b/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs index be35d481c..24d06d975 100644 --- a/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/ProcessStreamPluginV2.cs @@ -13,7 +13,7 @@ namespace Flow.Launcher.Core.Plugin { public override string SupportedLanguage { get; set; } - protected override IDuplexPipe ClientPipe { get; set; } + protected sealed override IDuplexPipe ClientPipe { get; set; } protected abstract ProcessStartInfo StartInfo { get; set; } diff --git a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs index 842dc5cad..4a8d8d7de 100644 --- a/Flow.Launcher.Core/Plugin/PythonPluginV2.cs +++ b/Flow.Launcher.Core/Plugin/PythonPluginV2.cs @@ -19,8 +19,6 @@ namespace Flow.Launcher.Core.Plugin internal sealed class PythonPluginV2 : ProcessStreamPluginV2 { public override string SupportedLanguage { get; set; } = AllowedLanguage.Python; - - protected override IDuplexPipe ClientPipe { get; set; } protected override ProcessStartInfo StartInfo { get; set; } public PythonPluginV2(string filename) diff --git a/Flow.Launcher.Plugin/AllowedLanguage.cs b/Flow.Launcher.Plugin/AllowedLanguage.cs index 96f90e093..619a94deb 100644 --- a/Flow.Launcher.Plugin/AllowedLanguage.cs +++ b/Flow.Launcher.Plugin/AllowedLanguage.cs @@ -11,7 +11,7 @@ namespace Flow.Launcher.Plugin /// Python /// public const string Python = "Python"; - + /// /// Python V2 /// @@ -32,16 +32,31 @@ namespace Flow.Launcher.Plugin /// public const string Executable = "Executable"; + /// + /// Standard .exe + /// + public const string ExecutableV2 = "Executable_V2"; + /// /// TypeScript /// public const string TypeScript = "TypeScript"; + /// + /// TypeScript + /// + public const string TypeScriptV2 = "TypeScript_V2"; + /// /// JavaScript /// public const string JavaScript = "JavaScript"; + /// + /// JavaScript + /// + public const string JavaScriptV2 = "JavaScript_V2"; + /// /// Determines if this language is a .NET language /// @@ -50,7 +65,7 @@ namespace Flow.Launcher.Plugin public static bool IsDotNet(string language) { return language.Equals(CSharp, StringComparison.OrdinalIgnoreCase) - || language.Equals(FSharp, StringComparison.OrdinalIgnoreCase); + || language.Equals(FSharp, StringComparison.OrdinalIgnoreCase); } /// @@ -61,11 +76,15 @@ namespace Flow.Launcher.Plugin public static bool IsAllowed(string language) { return IsDotNet(language) - || language.Equals(Python, StringComparison.OrdinalIgnoreCase) - || language.Equals(PythonV2, StringComparison.OrdinalIgnoreCase) - || language.Equals(Executable, StringComparison.OrdinalIgnoreCase) - || language.Equals(TypeScript, StringComparison.OrdinalIgnoreCase) - || language.Equals(JavaScript, StringComparison.OrdinalIgnoreCase); + || language.Equals(Python, StringComparison.OrdinalIgnoreCase) + || language.Equals(PythonV2, StringComparison.OrdinalIgnoreCase) + || language.Equals(Executable, StringComparison.OrdinalIgnoreCase) + || language.Equals(TypeScript, StringComparison.OrdinalIgnoreCase) + || language.Equals(JavaScript, StringComparison.OrdinalIgnoreCase) + || language.Equals(ExecutableV2, StringComparison.OrdinalIgnoreCase) + || language.Equals(TypeScriptV2, StringComparison.OrdinalIgnoreCase) + || language.Equals(JavaScriptV2, StringComparison.OrdinalIgnoreCase); + ; } } }