2025-03-31 13:27:59 +00:00
|
|
|
|
using System;
|
2022-11-21 22:20:27 +00:00
|
|
|
|
using System.Collections.Generic;
|
2025-02-24 04:37:08 +00:00
|
|
|
|
using System.IO;
|
2022-11-21 22:20:27 +00:00
|
|
|
|
using System.Linq;
|
2024-11-25 02:38:43 +00:00
|
|
|
|
using System.Windows;
|
2022-11-21 22:20:27 +00:00
|
|
|
|
using System.Windows.Forms;
|
2025-03-31 13:27:59 +00:00
|
|
|
|
using Flow.Launcher.Infrastructure.UserSettings;
|
|
|
|
|
|
using Flow.Launcher.Plugin;
|
|
|
|
|
|
using Flow.Launcher.Plugin.SharedCommands;
|
2022-11-21 22:20:27 +00:00
|
|
|
|
|
|
|
|
|
|
namespace Flow.Launcher.Core.ExternalPlugins.Environments
|
|
|
|
|
|
{
|
2022-12-01 03:40:24 +00:00
|
|
|
|
public abstract class AbstractPluginEnvironment
|
2022-11-21 22:20:27 +00:00
|
|
|
|
{
|
2025-04-13 09:26:21 +00:00
|
|
|
|
private static readonly string ClassName = nameof(AbstractPluginEnvironment);
|
|
|
|
|
|
|
2025-09-23 09:52:49 +00:00
|
|
|
|
protected readonly IPublicAPI API = PublicApi.Instance;
|
|
|
|
|
|
|
2022-11-21 22:20:27 +00:00
|
|
|
|
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;
|
|
|
|
|
|
|
2025-01-12 12:04:44 +00:00
|
|
|
|
internal abstract string PluginsSettingsFilePath { get; set; }
|
2022-11-21 22:20:27 +00:00
|
|
|
|
|
|
|
|
|
|
internal List<PluginMetadata> PluginMetadataList;
|
|
|
|
|
|
|
|
|
|
|
|
internal PluginsSettings PluginSettings;
|
|
|
|
|
|
|
|
|
|
|
|
internal AbstractPluginEnvironment(List<PluginMetadata> pluginMetadataList, PluginsSettings pluginSettings)
|
|
|
|
|
|
{
|
|
|
|
|
|
PluginMetadataList = pluginMetadataList;
|
|
|
|
|
|
PluginSettings = pluginSettings;
|
|
|
|
|
|
}
|
2022-11-27 21:44:42 +00:00
|
|
|
|
|
2022-11-21 22:20:27 +00:00
|
|
|
|
internal IEnumerable<PluginPair> Setup()
|
|
|
|
|
|
{
|
2025-03-31 13:27:59 +00:00
|
|
|
|
// If no plugin is using the language, return empty list
|
2022-11-21 22:20:27 +00:00
|
|
|
|
if (!PluginMetadataList.Any(o => o.Language.Equals(Language, StringComparison.OrdinalIgnoreCase)))
|
2025-03-31 13:27:59 +00:00
|
|
|
|
{
|
2022-11-21 22:20:27 +00:00
|
|
|
|
return new List<PluginPair>();
|
2025-03-31 13:27:59 +00:00
|
|
|
|
}
|
2022-11-21 22:20:27 +00:00
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(PluginsSettingsFilePath) && FilesFolders.FileExists(PluginsSettingsFilePath))
|
|
|
|
|
|
{
|
2022-11-21 22:28:23 +00:00
|
|
|
|
// Ensure latest only if user is using Flow's environment setup.
|
|
|
|
|
|
if (PluginsSettingsFilePath.StartsWith(EnvPath, StringComparison.OrdinalIgnoreCase))
|
|
|
|
|
|
EnsureLatestInstalled(ExecutablePath, PluginsSettingsFilePath, EnvPath);
|
2022-11-21 22:20:27 +00:00
|
|
|
|
|
|
|
|
|
|
return SetPathForPluginPairs(PluginsSettingsFilePath, Language);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-23 09:14:30 +00:00
|
|
|
|
var noRuntimeMessage = Localize.runtimePluginInstalledChooseRuntimePrompt(Language, EnvName, Environment.NewLine);
|
2025-09-23 09:52:49 +00:00
|
|
|
|
if (API.ShowMsgBox(noRuntimeMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.No)
|
2022-11-21 22:20:27 +00:00
|
|
|
|
{
|
2025-09-23 09:14:30 +00:00
|
|
|
|
var msg = Localize.runtimePluginChooseRuntimeExecutable(EnvName);
|
2022-11-21 22:20:27 +00:00
|
|
|
|
|
2025-03-31 13:27:59 +00:00
|
|
|
|
var selectedFile = GetFileFromDialog(msg, FileDialogFilter);
|
2022-11-21 22:20:27 +00:00
|
|
|
|
|
2025-04-01 01:26:39 +00:00
|
|
|
|
if (!string.IsNullOrEmpty(selectedFile))
|
|
|
|
|
|
{
|
|
|
|
|
|
PluginsSettingsFilePath = selectedFile;
|
|
|
|
|
|
}
|
2022-11-21 22:20:27 +00:00
|
|
|
|
// Nothing selected because user pressed cancel from the file dialog window
|
2025-04-01 01:26:39 +00:00
|
|
|
|
else
|
|
|
|
|
|
{
|
2025-09-23 09:14:30 +00:00
|
|
|
|
var forceDownloadMessage = Localize.runtimeExecutableInvalidChooseDownload(Language, EnvName, Environment.NewLine);
|
2025-04-01 01:26:39 +00:00
|
|
|
|
|
|
|
|
|
|
// Let users select valid path or choose to download
|
|
|
|
|
|
while (string.IsNullOrEmpty(selectedFile))
|
|
|
|
|
|
{
|
2025-09-23 09:52:49 +00:00
|
|
|
|
if (API.ShowMsgBox(forceDownloadMessage, string.Empty, MessageBoxButton.YesNo) == MessageBoxResult.Yes)
|
2025-04-01 01:26:39 +00:00
|
|
|
|
{
|
|
|
|
|
|
// Continue select file
|
|
|
|
|
|
selectedFile = GetFileFromDialog(msg, FileDialogFilter);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
// User selected no, break the loop
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!string.IsNullOrEmpty(selectedFile))
|
|
|
|
|
|
{
|
|
|
|
|
|
PluginsSettingsFilePath = selectedFile;
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
InstallEnvironment();
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2022-11-21 22:20:27 +00:00
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
|
|
|
|
|
InstallEnvironment();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (FilesFolders.FileExists(PluginsSettingsFilePath))
|
|
|
|
|
|
{
|
|
|
|
|
|
return SetPathForPluginPairs(PluginsSettingsFilePath, Language);
|
|
|
|
|
|
}
|
|
|
|
|
|
else
|
|
|
|
|
|
{
|
2025-09-23 09:52:49 +00:00
|
|
|
|
API.ShowMsgBox(Localize.runtimePluginUnableToSetExecutablePath(Language));
|
|
|
|
|
|
API.LogError(ClassName,
|
2022-11-21 22:20:27 +00:00
|
|
|
|
$"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)
|
|
|
|
|
|
{
|
2025-03-31 13:27:59 +00:00
|
|
|
|
if (expectedPath == currentPath) return;
|
2022-11-21 22:20:27 +00:00
|
|
|
|
|
2025-09-23 09:52:49 +00:00
|
|
|
|
FilesFolders.RemoveFolderIfExists(installedDirPath, (s) => API.ShowMsgBox(s));
|
2022-11-21 22:20:27 +00:00
|
|
|
|
|
|
|
|
|
|
InstallEnvironment();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-11-30 10:06:54 +00:00
|
|
|
|
internal abstract PluginPair CreatePluginPair(string filePath, PluginMetadata metadata);
|
|
|
|
|
|
|
2022-11-21 22:20:27 +00:00
|
|
|
|
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))
|
2025-02-24 04:37:08 +00:00
|
|
|
|
{
|
|
|
|
|
|
metadata.AssemblyName = string.Empty;
|
2025-02-24 05:28:58 +00:00
|
|
|
|
pluginPairs.Add(CreatePluginPair(filePath, metadata));
|
2025-02-24 04:37:08 +00:00
|
|
|
|
}
|
2022-11-21 22:20:27 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return pluginPairs;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-03-31 13:27:59 +00:00
|
|
|
|
private static string GetFileFromDialog(string title, string filter = "")
|
2022-11-21 22:20:27 +00:00
|
|
|
|
{
|
|
|
|
|
|
var dlg = new OpenFileDialog
|
|
|
|
|
|
{
|
|
|
|
|
|
InitialDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
|
|
|
|
|
|
Multiselect = false,
|
|
|
|
|
|
CheckFileExists = true,
|
|
|
|
|
|
CheckPathExists = true,
|
|
|
|
|
|
Title = title,
|
|
|
|
|
|
Filter = filter
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
var result = dlg.ShowDialog();
|
2023-03-26 06:12:21 +00:00
|
|
|
|
return result == DialogResult.OK ? dlg.FileName : string.Empty;
|
2022-11-21 22:20:27 +00:00
|
|
|
|
}
|
2022-12-01 03:40:24 +00:00
|
|
|
|
|
2022-12-01 09:58:31 +00:00
|
|
|
|
/// <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>
|
2022-12-01 21:53:57 +00:00
|
|
|
|
public static void PreStartPluginExecutablePathUpdate(Settings settings)
|
2022-12-01 03:40:24 +00:00
|
|
|
|
{
|
|
|
|
|
|
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
|
2022-12-01 09:35:23 +00:00
|
|
|
|
= GetUpdatedEnvironmentPath(settings.PluginSettings.PythonExecutablePath);
|
2022-12-01 03:40:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (IsUsingPortablePath(settings.PluginSettings.NodeExecutablePath, DataLocation.NodeEnvironmentName)
|
|
|
|
|
|
&& !settings.PluginSettings.NodeExecutablePath.StartsWith(DataLocation.PortableDataPath))
|
|
|
|
|
|
{
|
|
|
|
|
|
settings.PluginSettings.NodeExecutablePath
|
2022-12-01 09:35:23 +00:00
|
|
|
|
= GetUpdatedEnvironmentPath(settings.PluginSettings.NodeExecutablePath);
|
2022-12-01 03:40:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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))
|
2025-03-31 13:27:59 +00:00
|
|
|
|
{
|
2022-12-01 03:40:24 +00:00
|
|
|
|
settings.PluginSettings.PythonExecutablePath
|
2022-12-01 09:35:23 +00:00
|
|
|
|
= GetUpdatedEnvironmentPath(settings.PluginSettings.PythonExecutablePath);
|
2025-03-31 13:27:59 +00:00
|
|
|
|
}
|
2022-12-01 03:40:24 +00:00
|
|
|
|
|
|
|
|
|
|
if (IsUsingPortablePath(settings.PluginSettings.NodeExecutablePath, DataLocation.NodeEnvironmentName))
|
2025-03-31 13:27:59 +00:00
|
|
|
|
{
|
2022-12-01 03:40:24 +00:00
|
|
|
|
settings.PluginSettings.NodeExecutablePath
|
2022-12-01 09:35:23 +00:00
|
|
|
|
= GetUpdatedEnvironmentPath(settings.PluginSettings.NodeExecutablePath);
|
2025-03-31 13:27:59 +00:00
|
|
|
|
}
|
2022-12-01 03:40:24 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static bool IsUsingPortablePath(string filePath, string pluginEnvironmentName)
|
|
|
|
|
|
{
|
2025-03-31 13:27:59 +00:00
|
|
|
|
if (string.IsNullOrEmpty(filePath)) return false;
|
2022-12-06 06:57:27 +00:00
|
|
|
|
|
2022-12-01 09:58:31 +00:00
|
|
|
|
// DataLocation.PortableDataPath returns the current portable path, this determines if an out
|
|
|
|
|
|
// of date path is also a portable path.
|
2025-03-31 13:27:59 +00:00
|
|
|
|
var portableAppEnvLocation = Path.Combine("UserData", DataLocation.PluginEnvironments, pluginEnvironmentName);
|
2022-12-01 03:40:24 +00:00
|
|
|
|
|
|
|
|
|
|
return filePath.Contains(portableAppEnvLocation);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private static bool IsUsingRoamingPath(string filePath)
|
|
|
|
|
|
{
|
2025-03-31 13:27:59 +00:00
|
|
|
|
if (string.IsNullOrEmpty(filePath)) return false;
|
2022-12-06 06:57:27 +00:00
|
|
|
|
|
2022-12-01 03:40:24 +00:00
|
|
|
|
return filePath.StartsWith(DataLocation.RoamingDataPath);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2022-12-01 09:35:23 +00:00
|
|
|
|
private static string GetUpdatedEnvironmentPath(string filePath)
|
2022-12-01 03:40:24 +00:00
|
|
|
|
{
|
|
|
|
|
|
var index = filePath.IndexOf(DataLocation.PluginEnvironments);
|
2025-09-23 09:52:49 +00:00
|
|
|
|
|
2022-12-01 09:35:23 +00:00
|
|
|
|
// get the substring after "Environments" because we can not determine it dynamically
|
2025-04-01 01:57:04 +00:00
|
|
|
|
var executablePathSubstring = filePath[(index + DataLocation.PluginEnvironments.Length)..];
|
|
|
|
|
|
return $"{DataLocation.PluginEnvironmentsPath}{executablePathSubstring}";
|
2022-12-01 03:40:24 +00:00
|
|
|
|
}
|
2022-11-21 22:20:27 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|