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 +}