Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
4ea1ee0
add Node.js environment setup
jjw24 Oct 23, 2022
eb203c2
executable plugin language check ignore case
jjw24 Oct 23, 2022
7441e90
python language check ignore case
jjw24 Oct 23, 2022
9e55fc5
add node plugin execution
jjw24 Oct 23, 2022
9b4a592
add Node.js file path browser to Settings window
jjw24 Oct 24, 2022
e29e2bd
change language key selectPythonDirectory to select
jjw24 Oct 24, 2022
acb6306
in Settings Window separate language from plugin env section
jjw24 Oct 24, 2022
65db4e4
add translation for file browser window title
jjw24 Oct 24, 2022
5263bf5
add filter to file dialog for selecting Python exe
jjw24 Oct 25, 2022
76c0f03
update python directory to python file directory
jjw24 Oct 25, 2022
56e6d6e
remove pythonDirectory language key
jjw24 Oct 25, 2022
1bb2de3
change wording 'No Setting' to 'None' when no path
jjw24 Oct 25, 2022
adf712d
add Node.js path to error reporting
jjw24 Oct 25, 2022
4b0d17c
set Constant env paths variables when PlugingSettings is set
jjw24 Oct 25, 2022
f76e296
update set plugin path for python
jjw24 Oct 28, 2022
2dcd4d7
centralise all plugin env folders in Environments directory
jjw24 Oct 28, 2022
c013713
Merge remote-tracking branch 'origin/dev' into add_nodejs_env
jjw24 Nov 19, 2022
c7cee4a
add ensure latest installation
jjw24 Nov 20, 2022
4e48c3e
refactor
jjw24 Nov 21, 2022
6561253
ensure latest only when using Flow's env setup
jjw24 Nov 21, 2022
2dba518
Merge remote-tracking branch 'origin/dev' into add_nodejs_env
jjw24 Nov 27, 2022
9eb5489
update setting's environment file paths after app update
jjw24 Nov 27, 2022
07bc2a7
remove PythonEmbeddable location
jjw24 Nov 28, 2022
680ed77
add abstract create plugin pair method to handle each plugin type
jjw24 Nov 30, 2022
d297779
remove unused SupportedLanguage property
jjw24 Nov 30, 2022
5975b6e
plugin env paths correction after update or mode change
jjw24 Dec 1, 2022
87e49cd
formatting
jjw24 Dec 1, 2022
e1e8951
remove duplicated updater pre-startup methods
jjw24 Dec 1, 2022
e28906d
rename method
jjw24 Dec 1, 2022
cef53bc
add empty string defaults to plugin executable paths
jjw24 Dec 2, 2022
0daf52f
remove PythonEmbeddable folder regardless if it's being used or not
jjw24 Dec 6, 2022
d2658c7
add null check
jjw24 Dec 6, 2022
5af272a
Merge branch 'dev' into add_nodejs_env
jjw24 Dec 20, 2022
d386735
Merge remote-tracking branch 'origin/dev' into add_nodejs_env
jjw24 Dec 24, 2022
ae66a2c
Merge branch 'dev' into add_nodejs_env
VictoriousRaptor Dec 25, 2022
7da7e60
Merge branch 'dev' into add_nodejs_env
jjw24 Dec 27, 2022
75a23d1
Fix option order in setting window
VictoriousRaptor Dec 28, 2022
50cd17e
Merge branch 'dev' into add_nodejs_env
VictoriousRaptor Dec 28, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,234 @@
using Flow.Launcher.Infrastructure.Logger;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Windows.Forms;

namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
public abstract class AbstractPluginEnvironment
{
internal abstract string Language { get; }

internal abstract string EnvName { get; }

internal abstract string EnvPath { get; }

internal abstract string InstallPath { get; }

internal abstract string ExecutablePath { get; }

internal virtual string FileDialogFilter => string.Empty;

internal abstract string PluginsSettingsFilePath { get; set; }

internal List<PluginMetadata> PluginMetadataList;

internal PluginsSettings PluginSettings;

internal AbstractPluginEnvironment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings)
{
PluginMetadataList = pluginMetadataList;
PluginSettings = pluginSettings;
}

internal IEnumerable<PluginPair> Setup()
{
if (!PluginMetadataList.Any(o => o.Language.Equals(Language, StringComparison.OrdinalIgnoreCase)))
return new List<PluginPair>();

// 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.
if (PluginsSettingsFilePath.StartsWith(EnvPath, StringComparison.OrdinalIgnoreCase))
EnsureLatestInstalled(ExecutablePath, PluginsSettingsFilePath, EnvPath);

return SetPathForPluginPairs(PluginsSettingsFilePath, Language);
}

if (MessageBox.Show($"Flow detected you have installed {Language} plugins, which " +
$"will require {EnvName} to run. Would you like to download {EnvName}? " +
Environment.NewLine + Environment.NewLine +
"Click no if it's already installed, " +
$"and you will be prompted to select the folder that contains the {EnvName} executable",
string.Empty, MessageBoxButtons.YesNo) == DialogResult.No)
{
var msg = $"Please select the {EnvName} executable";
var selectedFile = string.Empty;

selectedFile = GetFileFromDialog(msg, FileDialogFilter);

if (!string.IsNullOrEmpty(selectedFile))
PluginsSettingsFilePath = selectedFile;

// Nothing selected because user pressed cancel from the file dialog window
if (string.IsNullOrEmpty(selectedFile))
InstallEnvironment();
}
else
{
InstallEnvironment();
}

if (FilesFolders.FileExists(PluginsSettingsFilePath))
{
return SetPathForPluginPairs(PluginsSettingsFilePath, Language);
}
else
{
MessageBox.Show(
$"Unable to set {Language} executable path, please try from Flow's settings (scroll down to the bottom).");
Log.Error("PluginsLoader",
$"Not able to successfully set {EnvName} path, setting's plugin executable path variable is still an empty string.",
$"{Language}Environment");

return new List<PluginPair>();
}
}

internal abstract void InstallEnvironment();

private void EnsureLatestInstalled(string expectedPath, string currentPath, string installedDirPath)
{
if (expectedPath == currentPath)
return;

FilesFolders.RemoveFolderIfExists(installedDirPath);

InstallEnvironment();

}

internal abstract PluginPair CreatePluginPair(string filePath, PluginMetadata metadata);

private IEnumerable<PluginPair> SetPathForPluginPairs(string filePath, string languageToSet)
{
var pluginPairs = new List<PluginPair>();

foreach (var metadata in PluginMetadataList)
{
if (metadata.Language.Equals(languageToSet, StringComparison.OrdinalIgnoreCase))
pluginPairs.Add(CreatePluginPair(filePath, metadata));
}

return pluginPairs;
}

private string GetFileFromDialog(string title, string filter = "")
{
var dlg = new OpenFileDialog
{
InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
Multiselect = false,
CheckFileExists = true,
CheckPathExists = true,
Title = title,
Filter = filter
};

var result = dlg.ShowDialog();
if (result == DialogResult.OK)
{
return dlg.FileName;
}
else
{
return string.Empty;
}
}

/// <summary>
/// After app updated while in portable mode or switched between portable/roaming mode,
/// need to update each plugin's executable path so user will not be prompted again to reinstall the environments.
/// </summary>
/// <param name="settings"></param>
public static void PreStartPluginExecutablePathUpdate(Settings settings)
{
if (DataLocation.PortableDataLocationInUse())
{
// When user is using portable but has moved flow to a different location
if (IsUsingPortablePath(settings.PluginSettings.PythonExecutablePath, DataLocation.PythonEnvironmentName)
&& !settings.PluginSettings.PythonExecutablePath.StartsWith(DataLocation.PortableDataPath))
{
settings.PluginSettings.PythonExecutablePath
= GetUpdatedEnvironmentPath(settings.PluginSettings.PythonExecutablePath);
}

if (IsUsingPortablePath(settings.PluginSettings.NodeExecutablePath, DataLocation.NodeEnvironmentName)
&& !settings.PluginSettings.NodeExecutablePath.StartsWith(DataLocation.PortableDataPath))
{
settings.PluginSettings.NodeExecutablePath
= GetUpdatedEnvironmentPath(settings.PluginSettings.NodeExecutablePath);
}

// When user has switched from roaming to portable
if (IsUsingRoamingPath(settings.PluginSettings.PythonExecutablePath))
{
settings.PluginSettings.PythonExecutablePath
= settings.PluginSettings.PythonExecutablePath.Replace(DataLocation.RoamingDataPath, DataLocation.PortableDataPath);
}

if (IsUsingRoamingPath(settings.PluginSettings.NodeExecutablePath))
{
settings.PluginSettings.NodeExecutablePath
= settings.PluginSettings.NodeExecutablePath.Replace(DataLocation.RoamingDataPath, DataLocation.PortableDataPath);
}
}
else
{
if (IsUsingPortablePath(settings.PluginSettings.PythonExecutablePath, DataLocation.PythonEnvironmentName))
settings.PluginSettings.PythonExecutablePath
= GetUpdatedEnvironmentPath(settings.PluginSettings.PythonExecutablePath);

if (IsUsingPortablePath(settings.PluginSettings.NodeExecutablePath, DataLocation.NodeEnvironmentName))
settings.PluginSettings.NodeExecutablePath
= GetUpdatedEnvironmentPath(settings.PluginSettings.NodeExecutablePath);
}
}

private static bool IsUsingPortablePath(string filePath, string pluginEnvironmentName)
{
if (string.IsNullOrEmpty(filePath))
return false;

// DataLocation.PortableDataPath returns the current portable path, this determines if an out
// of date path is also a portable path.
var portableAppEnvLocation = $"UserData\\{DataLocation.PluginEnvironments}\\{pluginEnvironmentName}";

return filePath.Contains(portableAppEnvLocation);
}

private static bool IsUsingRoamingPath(string filePath)
{
if (string.IsNullOrEmpty(filePath))
return false;

return filePath.StartsWith(DataLocation.RoamingDataPath);
}

private static string GetUpdatedEnvironmentPath(string filePath)
{
var index = filePath.IndexOf(DataLocation.PluginEnvironments);

// get the substring after "Environments" because we can not determine it dynamically
var ExecutablePathSubstring = filePath.Substring(index + DataLocation.PluginEnvironments.Count());
return $"{DataLocation.PluginEnvironmentsPath}{ExecutablePathSubstring}";
}
}
}
Original file line number Diff line number Diff line change
@@ -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 JavaScriptEnvironment : TypeScriptEnvironment
Copy link
Member

Choose a reason for hiding this comment

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

LOL why javascript extends from typescript

Copy link
Member Author

Choose a reason for hiding this comment

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

Mainly so we don't need to reimplement anything since they both are using same everything. So plugin dev can set their plugin type as JavaScript or Typescript.

{
internal override string Language => AllowedLanguage.JavaScript;

internal JavaScriptEnvironment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
using Droplex;
using Flow.Launcher.Core.Plugin;
using Flow.Launcher.Infrastructure.UserSettings;
using Flow.Launcher.Plugin;
using Flow.Launcher.Plugin.SharedCommands;
using System.Collections.Generic;
using System.IO;

namespace Flow.Launcher.Core.ExternalPlugins.Environments
{
internal class PythonEnvironment : AbstractPluginEnvironment
{
internal override string Language => AllowedLanguage.Python;

internal override string EnvName => DataLocation.PythonEnvironmentName;

internal override string EnvPath => Path.Combine(DataLocation.PluginEnvironmentsPath, EnvName);

internal override string InstallPath => Path.Combine(EnvPath, "PythonEmbeddable-v3.8.9");

internal override string ExecutablePath => Path.Combine(InstallPath, "pythonw.exe");

internal override string FileDialogFilter => "Python|pythonw.exe";

internal override string PluginsSettingsFilePath { get => PluginSettings.PythonExecutablePath; set => PluginSettings.PythonExecutablePath = value; }

internal PythonEnvironment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings) : base(pluginMetadataList, pluginSettings) { }

internal override void InstallEnvironment()
{
FilesFolders.RemoveFolderIfExists(InstallPath);

// Python 3.8.9 is used for Windows 7 compatibility
DroplexPackage.Drop(App.python_3_8_9_embeddable, InstallPath).Wait();

PluginsSettingsFilePath = ExecutablePath;
}

internal override PluginPair CreatePluginPair(string filePath, PluginMetadata metadata)
{
return new PluginPair
{
Plugin = new PythonPlugin(filePath),
Metadata = metadata
};
}
}
}
Original file line number Diff line number Diff line change
@@ -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 TypeScriptEnvironment : AbstractPluginEnvironment
{
internal override string Language => AllowedLanguage.TypeScript;

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 TypeScriptEnvironment(List<PluginMetadata> 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 NodePlugin(filePath),
Metadata = metadata
};
}
}
}
2 changes: 1 addition & 1 deletion Flow.Launcher.Core/Flow.Launcher.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Droplex" Version="1.4.1" />
<PackageReference Include="Droplex" Version="1.6.0" />
<PackageReference Include="FSharp.Core" Version="6.0.6" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="2.2.1" />
<PackageReference Include="squirrel.windows" Version="1.5.2" NoWarn="NU1701" />
Expand Down
1 change: 0 additions & 1 deletion Flow.Launcher.Core/Plugin/ExecutablePlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ namespace Flow.Launcher.Core.Plugin
internal class ExecutablePlugin : JsonRPCPlugin
{
private readonly ProcessStartInfo _startInfo;
public override string SupportedLanguage { get; set; } = AllowedLanguage.Executable;

public ExecutablePlugin(string filename)
{
Expand Down
5 changes: 1 addition & 4 deletions Flow.Launcher.Core/Plugin/JsonRPCPlugin.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,7 @@ internal abstract class JsonRPCPlugin : IAsyncPlugin, IContextMenu, ISettingProv
{
protected PluginInitContext context;
public const string JsonRPC = "JsonRPC";
/// <summary>
/// The language this JsonRPCPlugin support
/// </summary>
public abstract string SupportedLanguage { get; set; }

protected abstract Task<Stream> RequestAsync(JsonRPCRequestModel rpcRequest, CancellationToken token = default);
protected abstract string Request(JsonRPCRequestModel rpcRequest, CancellationToken token = default);

Expand Down
Loading