diff --git a/.github/actions/spelling/allow.txt b/.github/actions/spelling/allow.txt index a36a6af3efa..7845c6a6566 100644 --- a/.github/actions/spelling/allow.txt +++ b/.github/actions/spelling/allow.txt @@ -3,6 +3,24 @@ https ssh ubuntu runcount +nunit +velopack +vpk +vsc +appveyor +appwrite +IME +hotkey +prioritise +runas +softpedia +sourcelink +TRAYMOUSEMESSAGE +uninstaller +vkcode +winget +workaround +nupkg Firefox Português Português (Brasil) diff --git a/.github/actions/spelling/excludes.txt b/.github/actions/spelling/excludes.txt index 224014ebac5..feb6f0ff007 100644 --- a/.github/actions/spelling/excludes.txt +++ b/.github/actions/spelling/excludes.txt @@ -71,3 +71,4 @@ ignore$ \.csproj$ \.DotSettings$ \.targets$ +.*Test.cs$ diff --git a/.github/actions/spelling/expect.txt b/.github/actions/spelling/expect.txt index 6ba41e410ea..c44b43fabc8 100644 --- a/.github/actions/spelling/expect.txt +++ b/.github/actions/spelling/expect.txt @@ -110,3 +110,11 @@ JsonRPC JsonRPCV2 Softpedia img +ime +LPWStr +flowlauncher +hotkeys +LPW +productversion +requery +Wnd diff --git a/.github/actions/spelling/patterns.txt b/.github/actions/spelling/patterns.txt index f29f57ad50e..fd134d799d6 100644 --- a/.github/actions/spelling/patterns.txt +++ b/.github/actions/spelling/patterns.txt @@ -27,7 +27,14 @@ # Automatically suggested patterns # hit-count: 360 file-count: 108 # IServiceProvider -\bI(?=(?:[A-Z][a-z]{2,})+\b) +# IPublicAPI +\bI(?=(?:[A-Z][a-z]{2,}[A-Z]*)+\b) + +# KListener +\bK(?=(?:[A-Z][a-z]{2,})+\b) + +# TApplication +\bT(?=(?:[A-Z][a-z]{2,})+\b) # hit-count: 297 file-count: 18 # uuid: diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 00000000000..9ba4f20ac78 --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,110 @@ +# This workflow will build a .NET project +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net + +name: .NET + +on: + workflow_dispatch: + push: + branches: + - dev + - master + pull_request: + branches: + - dev + - master + +jobs: + build: + + runs-on: windows-latest + env: + FlowVersion: 1.18.0 + NUGET_CERT_REVOCATION_MODE: offline + BUILD_NUMBER: ${{ github.run_number }} + steps: + - uses: actions/checkout@v4 + - name: Set Flow.Launcher.csproj version + id: update + uses: vers-one/dotnet-project-version-updater@v1.5 + with: + file: | + "**/SolutionAssemblyInfo.cs" + version: ${{ env.FlowVersion }}.${{ env.BUILD_NUMBER }} + - uses: actions/cache@v4 + name: Restore Nuget Cache + with: + path: | + ~/.nuget/packages + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj') }} + restore-keys: | + ${{ runner.os }}-nuget + - uses: actions/cache@v4 + name: Restore dotnet tool Cache + with: + path: | + ~/.dotnet/tools + key: ${{ runner.os }}-dotnet-tools-${{ hashFiles('~/.dotnet/tools/**') }} + restore-keys: | + ${{ runner.os }}-dotnet-tools + - name: Setup .NET + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 7.0.x + - name: Install vpk + Install vpk tool (dotnet tool install will not reinstall if already installed) + We will update the cli by removing cache + run: | + if (!(Get-Command vpk -ErrorAction SilentlyContinue)) { + dotnet tool install -g vpk + } + - name: Restore dependencies + run: dotnet restore + - name: Build + run: dotnet build --no-restore -c Release + - name: Initialize Service + run: | + sc config WSearch start= auto # Starts Windows Search service- Needed for running ExplorerTest + net start WSearch + - name: Test + run: dotnet test --no-build --verbosity normal -c Release + - name: Perform post_build tasks + shell: pwsh + run: .\Scripts\post_build.ps1 -flowversion "${env:FlowVersion}-build.${env:BUILD_NUMBER}" + - name: Upload Plugin Nupkg + uses: actions/upload-artifact@v4 + with: + name: Plugin nupkg + path: | + Output\Release\Flow.Launcher.Plugin.*.nupkg + compression-level: 0 + - name: Upload Setup + uses: actions/upload-artifact@v4 + with: + name: Flow Installer + path: | + Releases\FlowLauncher-*.exe + compression-level: 0 + - name: Upload Portable Version + uses: actions/upload-artifact@v4 + with: + name: Portable Version + path: | + Releases\FlowLauncher-*-Portable.zip + compression-level: 0 +# - name: Upload Full Nupkg +# uses: actions/upload-artifact@v4 +# with: +# name: Full nupkg +# path: | +# Releases\FlowLauncher-*-full.nupkg +# +# compression-level: 0 +# - name: Upload Release Information +# uses: actions/upload-artifact@v4 +# with: +# name: RELEASES +# path: | +# Releases\RELEASES* +# compression-level: 0 +# diff --git a/Flow.Launcher.Core/Configuration/IPortable.cs b/Flow.Launcher.Core/Configuration/IPortable.cs index e56b0188cfb..a741a0e9d41 100644 --- a/Flow.Launcher.Core/Configuration/IPortable.cs +++ b/Flow.Launcher.Core/Configuration/IPortable.cs @@ -5,12 +5,6 @@ public interface IPortable { void EnablePortableMode(); void DisablePortableMode(); - void RemoveShortcuts(); - void RemoveUninstallerEntry(); - void CreateShortcuts(); - void CreateUninstallerEntry(); - void MoveUserDataFolder(string fromLocation, string toLocation); - void VerifyUserDataAfterMove(string fromLocation, string toLocation); bool CanUpdatePortability(); } -} \ No newline at end of file +} diff --git a/Flow.Launcher.Core/Configuration/Portable.cs b/Flow.Launcher.Core/Configuration/Portable.cs index b58154dcb23..0353c232e4e 100644 --- a/Flow.Launcher.Core/Configuration/Portable.cs +++ b/Flow.Launcher.Core/Configuration/Portable.cs @@ -1,32 +1,18 @@ -using Microsoft.Win32; -using Squirrel; -using System; +using System; using System.IO; -using System.Reflection; using System.Windows; using Flow.Launcher.Infrastructure; using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin.SharedCommands; -using System.Linq; +using Flow.Launcher.Core.Plugin; +using Microsoft.Win32; +using Velopack.Windows; namespace Flow.Launcher.Core.Configuration { public class Portable : IPortable { - /// - /// As at Squirrel.Windows version 1.5.2, UpdateManager needs to be disposed after finish - /// - /// - private UpdateManager NewUpdateManager() - { - var applicationFolderName = Constant.ApplicationDirectory - .Split(new[] { Path.DirectorySeparatorChar }, StringSplitOptions.None) - .Last(); - - return new UpdateManager(string.Empty, applicationFolderName, Constant.RootDirectory); - } - public void DisablePortableMode() { try @@ -36,14 +22,15 @@ public void DisablePortableMode() // Create shortcuts and uninstaller are not required in debug mode, // otherwise will repoint the path of the actual installed production version to the debug version CreateShortcuts(); - CreateUninstallerEntry(); + // CreateUninstallerEntry(); #endif + IndicateDeletion(DataLocation.PortableDataPath); MessageBox.Show("Flow Launcher needs to restart to finish disabling portable mode, " + - "after the restart your portable data profile will be deleted and roaming data profile kept"); + "after the restart your portable data profile will be deleted and roaming data profile kept"); - UpdateManager.RestartApp(Constant.ApplicationFileName); + PluginManager.API.RestartApp(); } catch (Exception e) { @@ -51,23 +38,53 @@ public void DisablePortableMode() } } + public void CreateShortcuts() + { + var shortcuts = new Shortcuts(); + + shortcuts.CreateShortcutForThisExe(); + } + + public void RemoveShortcuts() + { + var shortcuts = new Shortcuts(); + + shortcuts.DeleteShortcuts(Velopack.Locators.VelopackLocator.GetDefault(null).ThisExeRelativePath, + ShortcutLocation.Desktop | ShortcutLocation.StartMenu); + } + + public void CreateUninstallerEntry() + { + var uninstallRegSubKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall"; + + using var baseKey = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Default); + using var subKey1 = baseKey.CreateSubKey(uninstallRegSubKey, RegistryKeyPermissionCheck.ReadWriteSubTree); + using var subKey2 = + subKey1.CreateSubKey(Constant.FlowLauncher, RegistryKeyPermissionCheck.ReadWriteSubTree); + subKey2?.SetValue("DisplayIcon", Path.Combine(Constant.ApplicationDirectory, "app.ico"), + RegistryValueKind.String); + } + + public void EnablePortableMode() { try { MoveUserDataFolder(DataLocation.RoamingDataPath, DataLocation.PortableDataPath); + #if !DEBUG // Remove shortcuts and uninstaller are not required in debug mode, // otherwise will delete the actual installed production version RemoveShortcuts(); - RemoveUninstallerEntry(); + // RemoveUninstallerEntry(); #endif + IndicateDeletion(DataLocation.RoamingDataPath); MessageBox.Show("Flow Launcher needs to restart to finish enabling portable mode, " + - "after the restart your roaming data profile will be deleted and portable data profile kept"); + "after the restart your roaming data profile will be deleted and portable data profile kept"); - UpdateManager.RestartApp(Constant.ApplicationFileName); + PluginManager.API.RestartApp(); } catch (Exception e) { @@ -75,23 +92,6 @@ public void EnablePortableMode() } } - public void RemoveShortcuts() - { - using (var portabilityUpdater = NewUpdateManager()) - { - portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu); - portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop); - portabilityUpdater.RemoveShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup); - } - } - - public void RemoveUninstallerEntry() - { - using (var portabilityUpdater = NewUpdateManager()) - { - portabilityUpdater.RemoveUninstallerRegistryEntry(); - } - } public void MoveUserDataFolder(string fromLocation, string toLocation) { @@ -104,36 +104,10 @@ public void VerifyUserDataAfterMove(string fromLocation, string toLocation) FilesFolders.VerifyBothFolderFilesEqual(fromLocation, toLocation); } - public void CreateShortcuts() - { - using (var portabilityUpdater = NewUpdateManager()) - { - portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.StartMenu, false); - portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Desktop, false); - portabilityUpdater.CreateShortcutsForExecutable(Constant.ApplicationFileName, ShortcutLocation.Startup, false); - } - } - - public void CreateUninstallerEntry() - { - var uninstallRegSubKey = @"Software\Microsoft\Windows\CurrentVersion\Uninstall"; - - using (var baseKey = RegistryKey.OpenBaseKey(RegistryHive.CurrentUser, RegistryView.Default)) - using (var subKey1 = baseKey.CreateSubKey(uninstallRegSubKey, RegistryKeyPermissionCheck.ReadWriteSubTree)) - using (var subKey2 = subKey1.CreateSubKey(Constant.FlowLauncher, RegistryKeyPermissionCheck.ReadWriteSubTree)) - { - subKey2.SetValue("DisplayIcon", Path.Combine(Constant.ApplicationDirectory, "app.ico"), RegistryValueKind.String); - } - - using (var portabilityUpdater = NewUpdateManager()) - { - _ = portabilityUpdater.CreateUninstallerRegistryEntry(); - } - } - internal void IndicateDeletion(string filePathTodelete) + internal void IndicateDeletion(string filePathToDelete) { - var deleteFilePath = Path.Combine(filePathTodelete, DataLocation.DeletionIndicatorFile); + var deleteFilePath = Path.Combine(filePathToDelete, DataLocation.DeletionIndicatorFile); using (var _ = File.CreateText(deleteFilePath)) { } @@ -146,14 +120,15 @@ internal void IndicateDeletion(string filePathTodelete) public void PreStartCleanUpAfterPortabilityUpdate() { // Specify here so this method does not rely on other environment variables to initialise - var portableDataDir = Path.Combine(Directory.GetParent(Assembly.GetExecutingAssembly().Location.NonNull()).ToString(), "UserData"); - var roamingDataDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "FlowLauncher"); + var portableDataDir = DataLocation.PortableDataPath; + var roamingDataDir = DataLocation.RoamingDataPath; + // Get full path to the .dead files for each case var portableDataDeleteFilePath = Path.Combine(portableDataDir, DataLocation.DeletionIndicatorFile); var roamingDataDeleteFilePath = Path.Combine(roamingDataDir, DataLocation.DeletionIndicatorFile); - // If the data folder in %appdata% is marked for deletion, + // If the data folder in %AppData% is marked for deletion, // delete it and prompt the user to pick the portable data location if (File.Exists(roamingDataDeleteFilePath)) { @@ -161,7 +136,7 @@ public void PreStartCleanUpAfterPortabilityUpdate() if (MessageBox.Show("Flow Launcher has detected you enabled portable mode, " + "would you like to move it to a different location?", string.Empty, - MessageBoxButton.YesNo) == MessageBoxResult.Yes) + MessageBoxButton.YesNo) == MessageBoxResult.Yes) { FilesFolders.OpenPath(Constant.RootDirectory); @@ -175,7 +150,7 @@ public void PreStartCleanUpAfterPortabilityUpdate() FilesFolders.RemoveFolderIfExists(portableDataDir); MessageBox.Show("Flow Launcher has detected you disabled portable mode, " + - "the relevant shortcuts and uninstaller entry have been created"); + "the relevant shortcuts and uninstaller entry have been created"); } } @@ -187,8 +162,8 @@ public bool CanUpdatePortability() if (roamingLocationExists && portableLocationExists) { MessageBox.Show(string.Format("Flow Launcher detected your user data exists both in {0} and " + - "{1}. {2}{2}Please delete {1} in order to proceed. No changes have occurred.", - DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine)); + "{1}. {2}{2}Please delete {1} in order to proceed. No changes have occurred.", + DataLocation.PortableDataPath, DataLocation.RoamingDataPath, Environment.NewLine)); return false; } diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj index 851541fc2fb..8ec73bfcd3d 100644 --- a/Flow.Launcher.Core/Flow.Launcher.Core.csproj +++ b/Flow.Launcher.Core/Flow.Launcher.Core.csproj @@ -57,8 +57,8 @@ - + diff --git a/Flow.Launcher.Core/Properties/AssemblyInfo.cs b/Flow.Launcher.Core/Properties/AssemblyInfo.cs index ad60e2c9f53..77c4fb79a46 100644 --- a/Flow.Launcher.Core/Properties/AssemblyInfo.cs +++ b/Flow.Launcher.Core/Properties/AssemblyInfo.cs @@ -1,3 +1,4 @@ -using System.Runtime.CompilerServices; +using System.Reflection; +using System.Runtime.CompilerServices; [assembly: InternalsVisibleTo("Flow.Launcher.Test")] diff --git a/Flow.Launcher.Core/Updater.cs b/Flow.Launcher.Core/Updater.cs index 3f64b273e4c..7fc50a02dbe 100644 --- a/Flow.Launcher.Core/Updater.cs +++ b/Flow.Launcher.Core/Updater.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.IO; using System.Net; using System.Net.Http; using System.Net.Sockets; @@ -7,7 +8,6 @@ using System.Threading.Tasks; using System.Windows; using JetBrains.Annotations; -using Squirrel; using Flow.Launcher.Core.Resource; using Flow.Launcher.Plugin.SharedCommands; using Flow.Launcher.Infrastructure; @@ -17,6 +17,11 @@ using Flow.Launcher.Plugin; using System.Text.Json.Serialization; using System.Threading; +using System.Windows.Shapes; +using Velopack; +using Velopack.Locators; +using Velopack.Sources; +using Path = System.IO.Path; namespace Flow.Launcher.Core { @@ -40,15 +45,23 @@ public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true) api.ShowMsg(api.GetTranslation("pleaseWait"), api.GetTranslation("update_flowlauncher_update_check")); - using var updateManager = await GitHubUpdateManagerAsync(GitHubRepository).ConfigureAwait(false); + var updateManager = new UpdateManager(new GithubSource(GitHubRepository, null, false)); // UpdateApp CheckForUpdate will return value only if the app is squirrel installed - var newUpdateInfo = await updateManager.CheckForUpdate().NonNull().ConfigureAwait(false); + var newUpdateInfo = await updateManager.CheckForUpdatesAsync().ConfigureAwait(false); - var newReleaseVersion = Version.Parse(newUpdateInfo.FutureReleaseEntry.Version.ToString()); + if (newUpdateInfo == null) + { + if (!silentUpdate) + api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"), + api.GetTranslation("update_flowlauncher_check_connection")); + return; + } + + var newReleaseVersion = Version.Parse(newUpdateInfo.TargetFullRelease.Version.ToString()); var currentVersion = Version.Parse(Constant.Version); - Log.Info($"|Updater.UpdateApp|Future Release <{newUpdateInfo.FutureReleaseEntry.Formatted()}>"); + Log.Info($"|Updater.UpdateApp|Future Release <{newUpdateInfo.TargetFullRelease.Formatted()}>"); if (newReleaseVersion <= currentVersion) { @@ -61,40 +74,45 @@ public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true) api.ShowMsg(api.GetTranslation("update_flowlauncher_update_found"), api.GetTranslation("update_flowlauncher_updating")); - await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply).ConfigureAwait(false); - - await updateManager.ApplyReleases(newUpdateInfo).ConfigureAwait(false); + await updateManager.DownloadUpdatesAsync(newUpdateInfo).ConfigureAwait(false); if (DataLocation.PortableDataLocationInUse()) { - var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion.ToString()}\\{DataLocation.PortableFolderName}"; - FilesFolders.CopyAll(DataLocation.PortableDataPath, targetDestination); - if (!FilesFolders.VerifyBothFolderFilesEqual(DataLocation.PortableDataPath, targetDestination)) - MessageBox.Show(string.Format(api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"), + var targetDestination = Path.Combine(VelopackLocator.GetDefault(null).RootAppDir!, + DataLocation.PortableFolderName); + + DataLocation.PortableDataPath.CopyAll(targetDestination); + if (!DataLocation.PortableDataPath.VerifyBothFolderFilesEqual(targetDestination)) + MessageBox.Show(string.Format( + api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"), DataLocation.PortableDataPath, targetDestination)); } - else - { - await updateManager.CreateUninstallerRegistryEntry().ConfigureAwait(false); - } var newVersionTips = NewVersionTips(newReleaseVersion.ToString()); Log.Info($"|Updater.UpdateApp|Update success:{newVersionTips}"); - if (MessageBox.Show(newVersionTips, api.GetTranslation("update_flowlauncher_new_update"), MessageBoxButton.YesNo) == MessageBoxResult.Yes) + if (MessageBox.Show(newVersionTips, api.GetTranslation("update_flowlauncher_new_update"), + MessageBoxButton.YesNo) == MessageBoxResult.Yes) + { + updateManager.ApplyUpdatesAndRestart(newUpdateInfo); + } + else { - UpdateManager.RestartApp(Constant.ApplicationFileName); + updateManager.WaitExitThenApplyUpdates(newUpdateInfo); } } catch (Exception e) { - if ((e is HttpRequestException or WebException or SocketException || e.InnerException is TimeoutException)) - Log.Exception($"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", e); + if ((e is HttpRequestException or WebException or SocketException || + e.InnerException is TimeoutException)) + Log.Exception( + $"|Updater.UpdateApp|Check your connection and proxy settings to github-cloud.s3.amazonaws.com.", + e); else Log.Exception($"|Updater.UpdateApp|Error Occurred", e); - + if (!silentUpdate) api.ShowMsg(api.GetTranslation("update_flowlauncher_fail"), api.GetTranslation("update_flowlauncher_check_connection")); @@ -105,40 +123,19 @@ public async Task UpdateAppAsync(IPublicAPI api, bool silentUpdate = true) } } - [UsedImplicitly] - private class GithubRelease + public static void RecoverPortableData() { - [JsonPropertyName("prerelease")] - public bool Prerelease { get; [UsedImplicitly] set; } + var locator = VelopackLocator.GetDefault(null); - [JsonPropertyName("published_at")] - public DateTime PublishedAt { get; [UsedImplicitly] set; } + var portableDataLocation = Path.Combine(VelopackLocator.GetDefault(null).RootAppDir!, + DataLocation.PortableFolderName); - [JsonPropertyName("html_url")] - public string HtmlUrl { get; [UsedImplicitly] set; } - } - - // https://github.com/Squirrel/Squirrel.Windows/blob/master/src/Squirrel/UpdateManager.Factory.cs - private async Task GitHubUpdateManagerAsync(string repository) - { - var uri = new Uri(repository); - var api = $"https://api.github.com/repos{uri.AbsolutePath}/releases"; - - await using var jsonStream = await Http.GetStreamAsync(api).ConfigureAwait(false); - - var releases = await System.Text.Json.JsonSerializer.DeserializeAsync>(jsonStream).ConfigureAwait(false); - var latest = releases.Where(r => !r.Prerelease).OrderByDescending(r => r.PublishedAt).First(); - var latestUrl = latest.HtmlUrl.Replace("/tag/", "/download/"); - - var client = new WebClient + if (Path.Exists(portableDataLocation)) { - Proxy = Http.WebProxy - }; - var downloader = new FileDownloader(client); - - var manager = new UpdateManager(latestUrl, urlDownloader: downloader); + portableDataLocation.CopyAll(Path.Combine(locator.AppContentDir!, DataLocation.PortableFolderName)); + } - return manager; + Directory.Delete(portableDataLocation); } public string NewVersionTips(string version) diff --git a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs index e294f52b8c5..2631dc0852e 100644 --- a/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs +++ b/Flow.Launcher.Infrastructure/UserSettings/DataLocation.cs @@ -1,14 +1,37 @@ using System; using System.IO; +using Flow.Launcher.Plugin.SharedCommands; namespace Flow.Launcher.Infrastructure.UserSettings { public static class DataLocation { + static DataLocation() + { + // check whether the package locate in %LocalAppData% + // if not create the portable data folder + // Don't create the folder if the version is 1.0.0 (Dev) to allow potential debugging with data in the project folder + // It is still possible to create the UserData folder for dev version manually but we want to keep the current behavior + if (!Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData) + .PathContains(Constant.ProgramDirectory) + && !IsDevVersion) + { + Directory.CreateDirectory(PortableDataPath); + } + + PluginsDirectory = Path.Combine(DataDirectory(), Constant.Plugins); + PluginEnvironmentsPath = Path.Combine(DataDirectory(), PluginEnvironments); + } + public const string PortableFolderName = "UserData"; public const string DeletionIndicatorFile = ".dead"; public static string PortableDataPath = Path.Combine(Constant.ProgramDirectory, PortableFolderName); - public static string RoamingDataPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "FlowLauncher"); + + public static string RoamingDataPath = + Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "FlowLauncher"); + + public static bool IsDevVersion => Constant.Version == "1.0.0"; + public static string DataDirectory() { if (PortableDataLocationInUse()) @@ -25,12 +48,14 @@ public static bool PortableDataLocationInUse() return false; } - public static readonly string PluginsDirectory = Path.Combine(DataDirectory(), Constant.Plugins); - public static readonly string PluginSettingsDirectory = Path.Combine(DataDirectory(), "Settings", Constant.Plugins); + public static readonly string PluginsDirectory; + + public static readonly string PluginSettingsDirectory = + Path.Combine(DataDirectory(), "Settings", Constant.Plugins); public const string PythonEnvironmentName = "Python"; public const string NodeEnvironmentName = "Node.js"; public const string PluginEnvironments = "Environments"; - public static readonly string PluginEnvironmentsPath = Path.Combine(DataDirectory(), PluginEnvironments); + public static readonly string PluginEnvironmentsPath; } } diff --git a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj index 7b0880b676a..08ef1bb1b2e 100644 --- a/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj +++ b/Flow.Launcher.Plugin/Flow.Launcher.Plugin.csproj @@ -66,7 +66,7 @@ all runtime; build; native; contentfiles; analyzers; buildtransitive - + diff --git a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs index dd8c4b11232..1f34cd23e5d 100644 --- a/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs +++ b/Flow.Launcher.Plugin/SharedCommands/FilesFolders.cs @@ -4,6 +4,7 @@ using System.Linq; #pragma warning disable IDE0005 using System.Windows; + #pragma warning restore IDE0005 namespace Flow.Launcher.Plugin.SharedCommands @@ -17,7 +18,7 @@ public static class FilesFolders /// /// Copies the folder and all of its files and folders - /// including subfolders to the target location + /// including subFolders to the target location /// /// /// @@ -46,15 +47,15 @@ public static void CopyAll(this string sourcePath, string targetPath) FileInfo[] files = dir.GetFiles(); foreach (FileInfo file in files) { - string temppath = Path.Combine(targetPath, file.Name); - file.CopyTo(temppath, false); + string tempPath = Path.Combine(targetPath, file.Name); + file.CopyTo(tempPath, false); } // Recursively copy subdirectories by calling itself on each subdirectory until there are no more to copy - foreach (DirectoryInfo subdir in dirs) + foreach (DirectoryInfo subDir in dirs) { - string temppath = Path.Combine(targetPath, subdir.Name); - CopyAll(subdir.FullName, temppath); + string tempPath = Path.Combine(targetPath, subDir.Name); + CopyAll(subDir.FullName, tempPath); } } catch (Exception) @@ -66,7 +67,6 @@ public static void CopyAll(this string sourcePath, string targetPath) RemoveFolderIfExists(targetPath); #endif } - } /// @@ -83,10 +83,12 @@ public static bool VerifyBothFolderFilesEqual(this string fromPath, string toPat var fromDir = new DirectoryInfo(fromPath); var toDir = new DirectoryInfo(toPath); - if (fromDir.GetFiles("*", SearchOption.AllDirectories).Length != toDir.GetFiles("*", SearchOption.AllDirectories).Length) + if (fromDir.GetFiles("*", SearchOption.AllDirectories).Length != + toDir.GetFiles("*", SearchOption.AllDirectories).Length) return false; - if (fromDir.GetDirectories("*", SearchOption.AllDirectories).Length != toDir.GetDirectories("*", SearchOption.AllDirectories).Length) + if (fromDir.GetDirectories("*", SearchOption.AllDirectories).Length != + toDir.GetDirectories("*", SearchOption.AllDirectories).Length) return false; return true; @@ -100,7 +102,6 @@ public static bool VerifyBothFolderFilesEqual(this string fromPath, string toPat return false; #endif } - } /// @@ -152,9 +153,7 @@ public static void OpenPath(string fileOrFolderPath) { var psi = new ProcessStartInfo { - FileName = FileExplorerProgramName, - UseShellExecute = true, - Arguments = '"' + fileOrFolderPath + '"' + FileName = FileExplorerProgramName, UseShellExecute = true, Arguments = '"' + fileOrFolderPath + '"' }; try { @@ -289,7 +288,7 @@ public static string ReturnPreviousDirectoryIfIncompleteString(string path) /// Sub path /// If , when and are equal, returns /// - public static bool PathContains(string parentPath, string subPath, bool allowEqual = false) + public static bool PathContains(this string parentPath, string subPath, bool allowEqual = false) { var rel = Path.GetRelativePath(parentPath.EnsureTrailingSlash(), subPath); return (rel != "." || allowEqual) @@ -298,7 +297,7 @@ public static bool PathContains(string parentPath, string subPath, bool allowEqu && !rel.StartsWith(@"..\") && !Path.IsPathRooted(rel); } - + /// /// Returns path ended with "\" /// diff --git a/Flow.Launcher.Test/FilesFoldersTest.cs b/Flow.Launcher.Test/FilesFoldersTest.cs index 3dead99183e..2621fc2da1f 100644 --- a/Flow.Launcher.Test/FilesFoldersTest.cs +++ b/Flow.Launcher.Test/FilesFoldersTest.cs @@ -1,5 +1,6 @@ using Flow.Launcher.Plugin.SharedCommands; using NUnit.Framework; +using NUnit.Framework.Legacy; namespace Flow.Launcher.Test { @@ -35,7 +36,7 @@ public class FilesFoldersTest [TestCase(@"c:\barr", @"c:\foo\..\bar\baz", false)] public void GivenTwoPaths_WhenCheckPathContains_ThenShouldBeExpectedResult(string parentPath, string path, bool expectedResult) { - Assert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path)); + ClassicAssert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path)); } // Equality @@ -47,7 +48,7 @@ public void GivenTwoPaths_WhenCheckPathContains_ThenShouldBeExpectedResult(strin [TestCase(@"c:\foo", @"c:\foo\", true)] public void GivenTwoPathsAreTheSame_WhenCheckPathContains_ThenShouldBeExpectedResult(string parentPath, string path, bool expectedResult) { - Assert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path, allowEqual: expectedResult)); + ClassicAssert.AreEqual(expectedResult, FilesFolders.PathContains(parentPath, path, allowEqual: expectedResult)); } } } diff --git a/Flow.Launcher.Test/Flow.Launcher.Test.csproj b/Flow.Launcher.Test/Flow.Launcher.Test.csproj index 9f2b64134cc..696b85ece0e 100644 --- a/Flow.Launcher.Test/Flow.Launcher.Test.csproj +++ b/Flow.Launcher.Test/Flow.Launcher.Test.csproj @@ -49,7 +49,8 @@ - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Flow.Launcher.Test/FuzzyMatcherTest.cs b/Flow.Launcher.Test/FuzzyMatcherTest.cs index d7f1432184c..c22e21a81d0 100644 --- a/Flow.Launcher.Test/FuzzyMatcherTest.cs +++ b/Flow.Launcher.Test/FuzzyMatcherTest.cs @@ -6,6 +6,7 @@ using Flow.Launcher.Infrastructure; using Flow.Launcher.Plugin; using Flow.Launcher.Plugin.SharedModels; +using NUnit.Framework.Legacy; namespace Flow.Launcher.Test { @@ -71,10 +72,10 @@ public void MatchTest() results = results.Where(x => x.Score > 0).OrderByDescending(x => x.Score).ToList(); - Assert.IsTrue(results.Count == 3); - Assert.IsTrue(results[0].Title == "Inste"); - Assert.IsTrue(results[1].Title == "Install Package"); - Assert.IsTrue(results[2].Title == "file open in browser-test"); + ClassicAssert.IsTrue(results.Count == 3); + ClassicAssert.IsTrue(results[0].Title == "Inste"); + ClassicAssert.IsTrue(results[1].Title == "Install Package"); + ClassicAssert.IsTrue(results[2].Title == "file open in browser-test"); } [TestCase("Chrome")] @@ -84,7 +85,7 @@ public void WhenNotAllCharactersFoundInSearchString_ThenShouldReturnZeroScore(st var matcher = new StringMatcher(); var scoreResult = matcher.FuzzyMatch(searchString, compareString).RawScore; - Assert.True(scoreResult == 0); + ClassicAssert.True(scoreResult == 0); } [TestCase("chr")] @@ -125,7 +126,7 @@ public void GivenQueryString_WhenAppliedPrecisionFiltering_ThenShouldReturnGreat Debug.WriteLine("###############################################"); Debug.WriteLine(""); - Assert.IsFalse(filteredResult.Any(x => x.Score < precisionScore)); + ClassicAssert.IsFalse(filteredResult.Any(x => x.Score < precisionScore)); } } @@ -151,7 +152,7 @@ public void WhenGivenQueryString_ThenShouldReturn_TheDesiredScoring( var rawScore = matcher.FuzzyMatch(queryString, compareString).RawScore; // Should - Assert.AreEqual(expectedScore, rawScore, + ClassicAssert.AreEqual(expectedScore, rawScore, $"Expected score for compare string '{compareString}': {expectedScore}, Actual: {rawScore}"); } @@ -195,7 +196,7 @@ public void WhenGivenDesiredPrecision_ThenShouldReturn_AllResultsGreaterOrEqual( Debug.WriteLine(""); // Should - Assert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(), + ClassicAssert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(), $"Query: {queryString}{Environment.NewLine} " + $"Compare: {compareString}{Environment.NewLine}" + $"Raw Score: {matchResult.RawScore}{Environment.NewLine}" + @@ -246,7 +247,7 @@ public void WhenGivenQuery_ShouldReturnResults_ContainingAllQuerySubstrings( Debug.WriteLine(""); // Should - Assert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(), + ClassicAssert.AreEqual(expectedPrecisionResult, matchResult.IsSearchPrecisionScoreMet(), $"Query:{queryString}{Environment.NewLine} " + $"Compare:{compareString}{Environment.NewLine}" + $"Raw Score: {matchResult.RawScore}{Environment.NewLine}" + @@ -277,7 +278,7 @@ public void WhenGivenAQuery_Scoring_ShouldGiveMoreWeightToStartOfNewWord( Debug.WriteLine(""); // Should - Assert.True(compareString1Result.Score > compareString2Result.Score, + ClassicAssert.True(compareString1Result.Score > compareString2Result.Score, $"Query: \"{queryString}\"{Environment.NewLine} " + $"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" + $"Should be greater than{Environment.NewLine}" + @@ -310,7 +311,7 @@ public void WhenGivenTwoStrings_Scoring_ShouldGiveMoreWeightToTheStringCloserToI Debug.WriteLine(""); // Should - Assert.True(compareString1Result.Score > compareString2Result.Score, + ClassicAssert.True(compareString1Result.Score > compareString2Result.Score, $"Query: \"{queryString}\"{Environment.NewLine} " + $"CompareString1: \"{compareString1}\", Score: {compareString1Result.Score}{Environment.NewLine}" + $"Should be greater than{Environment.NewLine}" + @@ -335,8 +336,8 @@ public void WhenMultipleResults_ExactMatchingResult_ShouldHaveGreatestScore( var firstScore = new[] {firstNameMatch, firstDescriptionMatch, firstExecutableNameMatch}.Max(); var secondScore = new[] {secondNameMatch, secondDescriptionMatch, secondExecutableNameMatch}.Max(); - // Assert - Assert.IsTrue(firstScore > secondScore, + // ClassicAssert + ClassicAssert.IsTrue(firstScore > secondScore, $"Query: \"{queryString}\"{Environment.NewLine} " + $"Name of first: \"{firstName}\", Final Score: {firstScore}{Environment.NewLine}" + $"Should be greater than{Environment.NewLine}" + @@ -360,7 +361,7 @@ public void WhenGivenAnAcronymQuery_ShouldReturnAcronymScore(string queryString, { var matcher = new StringMatcher(); var score = matcher.FuzzyMatch(queryString, compareString).Score; - Assert.IsTrue(score == desiredScore, + ClassicAssert.IsTrue(score == desiredScore, $@"Query: ""{queryString}"" CompareString: ""{compareString}"" Score: {score} diff --git a/Flow.Launcher.Test/HttpTest.cs b/Flow.Launcher.Test/HttpTest.cs index e72ad7a6761..27c939634bd 100644 --- a/Flow.Launcher.Test/HttpTest.cs +++ b/Flow.Launcher.Test/HttpTest.cs @@ -2,6 +2,7 @@ using System; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Infrastructure.Http; +using NUnit.Framework.Legacy; namespace Flow.Launcher.Test { @@ -16,16 +17,16 @@ public void GivenHttpProxy_WhenUpdated_ThenWebProxyShouldAlsoBeUpdatedToTheSame( proxy.Enabled = true; proxy.Server = "127.0.0.1"; - Assert.AreEqual(Http.WebProxy.Address, new Uri($"http://{proxy.Server}:{proxy.Port}")); - Assert.IsNull(Http.WebProxy.Credentials); + ClassicAssert.AreEqual(Http.WebProxy.Address, new Uri($"http://{proxy.Server}:{proxy.Port}")); + ClassicAssert.IsNull(Http.WebProxy.Credentials); proxy.UserName = "test"; - Assert.NotNull(Http.WebProxy.Credentials); - Assert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").UserName, proxy.UserName); - Assert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, ""); + ClassicAssert.NotNull(Http.WebProxy.Credentials); + ClassicAssert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").UserName, proxy.UserName); + ClassicAssert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, ""); proxy.Password = "test password"; - Assert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, proxy.Password); + ClassicAssert.AreEqual(Http.WebProxy.Credentials.GetCredential(Http.WebProxy.Address, "Basic").Password, proxy.Password); } } } diff --git a/Flow.Launcher.Test/PluginLoadTest.cs b/Flow.Launcher.Test/PluginLoadTest.cs index d6ba48f1905..f41c10c189d 100644 --- a/Flow.Launcher.Test/PluginLoadTest.cs +++ b/Flow.Launcher.Test/PluginLoadTest.cs @@ -3,6 +3,7 @@ using Flow.Launcher.Plugin; using System.Collections.Generic; using System.Linq; +using NUnit.Framework.Legacy; namespace Flow.Launcher.Test { @@ -56,15 +57,15 @@ public void GivenDuplicatePluginMetadatasWhenLoadedThenShouldReturnOnlyUniqueLis (var unique, var duplicates) = PluginConfig.GetUniqueLatestPluginMetadata(duplicateList); // Then - Assert.True(unique.FirstOrDefault().ID == "CEA0TYUC6D3B4085823D60DC76F28855" && unique.FirstOrDefault().Version == "1.0.2"); - Assert.True(unique.Count() == 1); + ClassicAssert.True(unique.FirstOrDefault().ID == "CEA0TYUC6D3B4085823D60DC76F28855" && unique.FirstOrDefault().Version == "1.0.2"); + ClassicAssert.True(unique.Count() == 1); - Assert.False(duplicates.Any(x => x.Version == "1.0.2" && x.ID == "CEA0TYUC6D3B4085823D60DC76F28855")); - Assert.True(duplicates.Count() == 6); + ClassicAssert.False(duplicates.Any(x => x.Version == "1.0.2" && x.ID == "CEA0TYUC6D3B4085823D60DC76F28855")); + ClassicAssert.True(duplicates.Count() == 6); } [Test] - public void GivenDuplicatePluginMetadatasWithNoUniquePluginWhenLoadedThenShouldReturnEmptyList() + public void GivenDuplicatePluginMetaDataWithNoUniquePluginWhenLoadedThenShouldReturnEmptyList() { // Given var duplicateList = new List @@ -85,8 +86,8 @@ public void GivenDuplicatePluginMetadatasWithNoUniquePluginWhenLoadedThenShouldR (var unique, var duplicates) = PluginConfig.GetUniqueLatestPluginMetadata(duplicateList); // Then - Assert.True(unique.Count() == 0); - Assert.True(duplicates.Count() == 2); + ClassicAssert.True(unique.Count() == 0); + ClassicAssert.True(duplicates.Count() == 2); } } } diff --git a/Flow.Launcher.Test/Plugins/ExplorerTest.cs b/Flow.Launcher.Test/Plugins/ExplorerTest.cs index 80cb74729fc..7fd6512eb65 100644 --- a/Flow.Launcher.Test/Plugins/ExplorerTest.cs +++ b/Flow.Launcher.Test/Plugins/ExplorerTest.cs @@ -11,6 +11,7 @@ using System.Runtime.Versioning; using System.Threading; using System.Threading.Tasks; +using NUnit.Framework.Legacy; using static Flow.Launcher.Plugin.Explorer.Search.SearchManager; namespace Flow.Launcher.Test.Plugins @@ -57,7 +58,7 @@ public void GivenWindowsIndexSearch_WhenProvidedFolderPath_ThenQueryWhereRestric var result = QueryConstructor.TopLevelDirectoryConstraint(folderPath); // Then - Assert.IsTrue(result == expectedString, + ClassicAssert.IsTrue(result == expectedString, $"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " + $"Actual: {result}{Environment.NewLine}"); } @@ -74,7 +75,7 @@ public void GivenWindowsIndexSearch_WhenSearchTypeIsTopLevelDirectorySearch_Then var queryString = queryConstructor.Directory(folderPath); // Then - Assert.IsTrue(queryString.Replace(" ", " ") == expectedString.Replace(" ", " "), + ClassicAssert.IsTrue(queryString.Replace(" ", " ") == expectedString.Replace(" ", " "), $"Expected string: {expectedString}{Environment.NewLine} " + $"Actual string was: {queryString}{Environment.NewLine}"); } @@ -94,7 +95,7 @@ public void GivenWindowsIndexSearchTopLevelDirectory_WhenSearchingForSpecificIte var queryString = queryConstructor.Directory(folderPath, userSearchString); // Then - Assert.AreEqual(expectedString, queryString); + ClassicAssert.AreEqual(expectedString, queryString); } [SupportedOSPlatform("windows7.0")] @@ -105,7 +106,7 @@ public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryWhereR const string resultString = QueryConstructor.RestrictionsForAllFilesAndFoldersSearch; // Then - Assert.AreEqual(expectedString, resultString); + ClassicAssert.AreEqual(expectedString, resultString); } [SupportedOSPlatform("windows7.0")] @@ -128,7 +129,7 @@ public void GivenWindowsIndexSearch_WhenSearchAllFoldersAndFiles_ThenQueryShould var resultString = queryConstructor.FilesAndFolders(userSearchString); // Then - Assert.AreEqual(expectedString, resultString); + ClassicAssert.AreEqual(expectedString, resultString); } @@ -144,7 +145,7 @@ public void GivenWindowsIndexSearch_WhenQueryWhereRestrictionsIsForFileContentSe var resultString = QueryConstructor.RestrictionsForFileContentSearch(querySearchString); // Then - Assert.IsTrue(resultString == expectedString, + ClassicAssert.IsTrue(resultString == expectedString, $"Expected QueryWhereRestrictions string: {expectedString}{Environment.NewLine} " + $"Actual string was: {resultString}{Environment.NewLine}"); } @@ -162,7 +163,7 @@ public void GivenWindowsIndexSearch_WhenSearchForFileContent_ThenQueryShouldUseE var resultString = queryConstructor.FileContent(userSearchString); // Then - Assert.IsTrue(resultString == expectedString, + ClassicAssert.IsTrue(resultString == expectedString, $"Expected query string: {expectedString}{Environment.NewLine} " + $"Actual string was: {resultString}{Environment.NewLine}"); } @@ -181,7 +182,7 @@ public void GivenQuery_WhenActionKeywordForFileContentSearchExists_ThenFileConte var result = searchManager.IsFileContentSearch(query.ActionKeyword); // Then - Assert.IsTrue(result, + ClassicAssert.IsTrue(result, $"Expected True for file content search. {Environment.NewLine} " + $"Actual result was: {result}{Environment.NewLine}"); } @@ -206,7 +207,7 @@ public void WhenGivenQuerySearchString_ThenShouldIndicateIfIsLocationPathString( var result = FilesFolders.IsLocationPathString(querySearchString); //Then - Assert.IsTrue(result == expectedResult, + ClassicAssert.IsTrue(result == expectedResult, $"Expected query search string check result is: {expectedResult} {Environment.NewLine} " + $"Actual check result is {result} {Environment.NewLine}"); @@ -233,7 +234,7 @@ public void GivenAPartialPath_WhenPreviousLevelDirectoryExists_ThenShouldReturnT var previousDirectoryPath = FilesFolders.GetPreviousExistingDirectory(previousLocationExists, path); //Then - Assert.IsTrue(previousDirectoryPath == expectedString, + ClassicAssert.IsTrue(previousDirectoryPath == expectedString, $"Expected path string: {expectedString} {Environment.NewLine} " + $"Actual path string is {previousDirectoryPath} {Environment.NewLine}"); } @@ -246,7 +247,7 @@ public void WhenGivenAPath_ThenShouldReturnThePreviousDirectoryPathIfIncompleteO var returnedPath = FilesFolders.ReturnPreviousDirectoryIfIncompleteString(path); //Then - Assert.IsTrue(returnedPath == expectedString, + ClassicAssert.IsTrue(returnedPath == expectedString, $"Expected path string: {expectedString} {Environment.NewLine} " + $"Actual path string is {returnedPath} {Environment.NewLine}"); } @@ -260,7 +261,7 @@ public void GivenFilePath_WhenSearchPatternHotKeyIsSearchAll_ThenQueryWhereRestr var resultString = QueryConstructor.RecursiveDirectoryConstraint(path); // Then - Assert.AreEqual(expectedString, resultString); + ClassicAssert.AreEqual(expectedString, resultString); } [SupportedOSPlatform("windows7.0")] @@ -274,7 +275,7 @@ public void GivenDirectoryInfoSearch_WhenSearchPatternHotKeyIsSearchAll_ThenSear var resultString = DirectoryInfoSearch.ConstructSearchCriteria(path); // Then - Assert.AreEqual(expectedString, resultString); + ClassicAssert.AreEqual(expectedString, resultString); } [TestCase("c:\\somefolder\\someotherfolder", ResultType.Folder, "irrelevant", false, true, "c:\\somefolder\\someotherfolder\\")] @@ -305,7 +306,7 @@ public void GivenFolderResult_WhenGetPath_ThenPathShouldBeExpectedString( var result = ResultManager.GetPathWithActionKeyword(path, type, actionKeyword); // Then - Assert.AreEqual(result, expectedResult); + ClassicAssert.AreEqual(result, expectedResult); } [TestCase("c:\\somefolder\\somefile", ResultType.File, "irrelevant", false, true, "e c:\\somefolder\\somefile")] @@ -334,7 +335,7 @@ public void GivenFileResult_WhenGetPath_ThenPathShouldBeExpectedString( var result = ResultManager.GetPathWithActionKeyword(path, type, actionKeyword); // Then - Assert.AreEqual(result, expectedResult); + ClassicAssert.AreEqual(result, expectedResult); } [TestCase("somefolder", "c:\\somefolder\\", ResultType.Folder, "q", false, false, "q somefolder")] @@ -366,7 +367,7 @@ public void GivenQueryWithFolderTypeResult_WhenGetAutoComplete_ThenResultShouldB var result = ResultManager.GetAutoCompleteText(title, query, path, resultType); // Then - Assert.AreEqual(result, expectedResult); + ClassicAssert.AreEqual(result, expectedResult); } [TestCase("somefile", "c:\\somefolder\\somefile", ResultType.File, "q", false, false, "q somefile")] @@ -398,7 +399,7 @@ public void GivenQueryWithFileTypeResult_WhenGetAutoComplete_ThenResultShouldBeE var result = ResultManager.GetAutoCompleteText(title, query, path, resultType); // Then - Assert.AreEqual(result, expectedResult); + ClassicAssert.AreEqual(result, expectedResult); } [TestCase(@"c:\foo", @"c:\foo", true)] @@ -420,7 +421,7 @@ public void GivenTwoPaths_WhenCompared_ThenShouldBeExpectedSameOrDifferent(strin }; // When, Then - Assert.AreEqual(expectedResult, comparator.Equals(result1, result2)); + ClassicAssert.AreEqual(expectedResult, comparator.Equals(result1, result2)); } [TestCase(@"c:\foo\", @"c:\foo\")] @@ -444,7 +445,7 @@ public void GivenTwoPaths_WhenComparedHasCode_ThenShouldBeSame(string path1, str var hash2 = comparator.GetHashCode(result2); // When, Then - Assert.IsTrue(hash1 == hash2); + ClassicAssert.IsTrue(hash1 == hash2); } [TestCase(@"%appdata%", true)] @@ -461,7 +462,7 @@ public void GivenPath_WhenHavingEnvironmentVariableOrNot_ThenShouldBeExpected(st var result = EnvironmentVariables.HasEnvironmentVar(path); // Then - Assert.AreEqual(result, expectedResult); + ClassicAssert.AreEqual(result, expectedResult); } } } diff --git a/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs b/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs index 3d05e56796f..3a4952d7567 100644 --- a/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs +++ b/Flow.Launcher.Test/Plugins/JsonRPCPluginTest.cs @@ -29,62 +29,62 @@ protected override Task RequestAsync(JsonRPCRequestModel request, Cancel } [TestCase("{\"result\":[],\"DebugMessage\":null}", Description = "Empty Result")] - [TestCase("{\"result\":[{\"JsonRPCAction\":null,\"Title\":\"something\",\"SubTitle\":\"\",\"ActionKeywordAssigned\":null,\"IcoPath\":null}],\"DebugMessage\":null}", Description = "One Result with Pascal Case")] - [TestCase("{\"result\":[{\"jsonRPCAction\":null,\"title\":\"something\",\"subTitle\":\"\",\"actionKeywordAssigned\":null,\"icoPath\":null}],\"debugMessage\":null}", Description = "One Result with camel Case")] - [TestCase("{\"result\":[{\"JsonRPCAction\":null,\"Title\":\"iii\",\"SubTitle\":\"\",\"ActionKeywordAssigned\":null,\"IcoPath\":null},{\"JsonRPCAction\":null,\"Title\":\"iii\",\"SubTitle\":\"\",\"ActionKeywordAssigned\":null,\"IcoPath\":null}],\"DebugMessage\":null}", Description = "Two Result with Pascal Case")] - [TestCase("{\"result\":[{\"jsonrpcAction\":null,\"TItLE\":\"iii\",\"Subtitle\":\"\",\"Actionkeywordassigned\":null,\"icoPath\":null},{\"jsonRPCAction\":null,\"tiTle\":\"iii\",\"subTitle\":\"\",\"ActionKeywordAssigned\":null,\"IcoPath\":null}],\"DebugMessage\":null}", Description = "Two Result with Weird Case")] + [TestCase( + "{\"result\":[{\"JsonRPCAction\":null,\"Title\":\"something\",\"SubTitle\":\"\",\"ActionKeywordAssigned\":null,\"IcoPath\":null}],\"DebugMessage\":null}", + Description = "One Result with Pascal Case")] + [TestCase( + "{\"result\":[{\"jsonRPCAction\":null,\"title\":\"something\",\"subTitle\":\"\",\"actionKeywordAssigned\":null,\"icoPath\":null}],\"debugMessage\":null}", + Description = "One Result with camel Case")] + [TestCase( + "{\"result\":[{\"JsonRPCAction\":null,\"Title\":\"iii\",\"SubTitle\":\"\",\"ActionKeywordAssigned\":null,\"IcoPath\":null},{\"JsonRPCAction\":null,\"Title\":\"iii\",\"SubTitle\":\"\",\"ActionKeywordAssigned\":null,\"IcoPath\":null}],\"DebugMessage\":null}", + Description = "Two Result with Pascal Case")] + [TestCase( + "{\"result\":[{\"jsonrpcAction\":null,\"TItLE\":\"iii\",\"Subtitle\":\"\",\"Actionkeywordassigned\":null,\"icoPath\":null},{\"jsonRPCAction\":null,\"tiTle\":\"iii\",\"subTitle\":\"\",\"ActionKeywordAssigned\":null,\"IcoPath\":null}],\"DebugMessage\":null}", + Description = "Two Result with Weird Case")] public async Task GivenVariousJsonText_WhenVariousNamingCase_ThenExpectNotNullResults_Async(string resultText) { - var results = await QueryAsync(new Query - { - Search = resultText - }, default); + var results = await QueryAsync(new Query { Search = resultText }, default); - Assert.IsNotNull(results); + Assert.That(results, Is.Not.Null); foreach (var result in results) { - Assert.IsNotNull(result); - Assert.IsNotNull(result.AsyncAction); - Assert.IsNotNull(result.Title); + Assert.That(result, Is.Not.Null); + Assert.That(result.AsyncAction, Is.Not.Null); + Assert.That(result.Title, Is.Not.Null); } - } public static List ResponseModelsSource = new() { new JsonRPCQueryResponseModel(0, new List()), - new JsonRPCQueryResponseModel(0, new List - { - new JsonRPCResult - { - Title = "Test1", SubTitle = "Test2" - } - }) + new JsonRPCQueryResponseModel(0, + new List { new JsonRPCResult { Title = "Test1", SubTitle = "Test2" } }) }; [TestCaseSource(typeof(JsonRPCPluginTest), nameof(ResponseModelsSource))] - public async Task GivenModel_WhenSerializeWithDifferentNamingPolicy_ThenExpectSameResult_Async(JsonRPCQueryResponseModel reference) + public async Task GivenModel_WhenSerializeWithDifferentNamingPolicy_ThenExpectSameResult_Async( + JsonRPCQueryResponseModel reference) { - var camelText = JsonSerializer.Serialize(reference, new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); + var camelText = JsonSerializer.Serialize(reference, + new JsonSerializerOptions() { PropertyNamingPolicy = JsonNamingPolicy.CamelCase }); var pascalText = JsonSerializer.Serialize(reference); var results1 = await QueryAsync(new Query { Search = camelText }, default); var results2 = await QueryAsync(new Query { Search = pascalText }, default); - Assert.IsNotNull(results1); - Assert.IsNotNull(results2); + Assert.That(results1, Is.Not.Null); + Assert.That(results2, Is.Not.Null); foreach (var ((result1, result2), referenceResult) in results1.Zip(results2).Zip(reference.Result)) { - Assert.AreEqual(result1, result2); - Assert.AreEqual(result1, referenceResult); + Assert.That(result1, Is.EqualTo(result2)); + Assert.That(result1, Is.EqualTo(referenceResult)); - Assert.IsNotNull(result1); - Assert.IsNotNull(result1.AsyncAction); + Assert.That(result1, Is.Not.Null); + Assert.That(result1.AsyncAction, Is.Not.Null); } } - } } diff --git a/Flow.Launcher.Test/Plugins/UrlPluginTest.cs b/Flow.Launcher.Test/Plugins/UrlPluginTest.cs index 7ccac5bd59d..5cd75d16044 100644 --- a/Flow.Launcher.Test/Plugins/UrlPluginTest.cs +++ b/Flow.Launcher.Test/Plugins/UrlPluginTest.cs @@ -10,23 +10,23 @@ public class UrlPluginTest public void URLMatchTest() { var plugin = new Main(); - Assert.IsTrue(plugin.IsURL("http://www.google.com")); - Assert.IsTrue(plugin.IsURL("https://www.google.com")); - Assert.IsTrue(plugin.IsURL("http://google.com")); - Assert.IsTrue(plugin.IsURL("www.google.com")); - Assert.IsTrue(plugin.IsURL("google.com")); - Assert.IsTrue(plugin.IsURL("http://localhost")); - Assert.IsTrue(plugin.IsURL("https://localhost")); - Assert.IsTrue(plugin.IsURL("http://localhost:80")); - Assert.IsTrue(plugin.IsURL("https://localhost:80")); - Assert.IsTrue(plugin.IsURL("http://110.10.10.10")); - Assert.IsTrue(plugin.IsURL("110.10.10.10")); - Assert.IsTrue(plugin.IsURL("ftp://110.10.10.10")); + Assert.That(plugin.IsURL("http://www.google.com"), Is.True); + Assert.That(plugin.IsURL("https://www.google.com"), Is.True); + Assert.That(plugin.IsURL("http://google.com"), Is.True); + Assert.That(plugin.IsURL("www.google.com"), Is.True); + Assert.That(plugin.IsURL("google.com"), Is.True); + Assert.That(plugin.IsURL("http://localhost"), Is.True); + Assert.That(plugin.IsURL("https://localhost"), Is.True); + Assert.That(plugin.IsURL("http://localhost:80"), Is.True); + Assert.That(plugin.IsURL("https://localhost:80"), Is.True); + Assert.That(plugin.IsURL("http://110.10.10.10"), Is.True); + Assert.That(plugin.IsURL("110.10.10.10"), Is.True); + Assert.That(plugin.IsURL("ftp://110.10.10.10"), Is.True); - Assert.IsFalse(plugin.IsURL("wwww")); - Assert.IsFalse(plugin.IsURL("wwww.c")); - Assert.IsFalse(plugin.IsURL("wwww.c")); + Assert.That(plugin.IsURL("wwww"), Is.False); + Assert.That(plugin.IsURL("wwww.c"), Is.False); + Assert.That(plugin.IsURL("wwww.c"), Is.False); } } } diff --git a/Flow.Launcher.Test/QueryBuilderTest.cs b/Flow.Launcher.Test/QueryBuilderTest.cs index aa0c8da12b9..d4808a4c5a5 100644 --- a/Flow.Launcher.Test/QueryBuilderTest.cs +++ b/Flow.Launcher.Test/QueryBuilderTest.cs @@ -2,6 +2,7 @@ using NUnit.Framework; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Plugin; +using NUnit.Framework.Legacy; namespace Flow.Launcher.Test { @@ -17,17 +18,17 @@ public void ExclusivePluginQueryTest() Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins); - Assert.AreEqual("> ping google.com -n 20 -6", q.RawQuery); - Assert.AreEqual("ping google.com -n 20 -6", q.Search, "Search should not start with the ActionKeyword."); - Assert.AreEqual(">", q.ActionKeyword); + ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.RawQuery); + ClassicAssert.AreEqual("ping google.com -n 20 -6", q.Search, "Search should not start with the ActionKeyword."); + ClassicAssert.AreEqual(">", q.ActionKeyword); - Assert.AreEqual(5, q.SearchTerms.Length, "The length of SearchTerms should match."); + ClassicAssert.AreEqual(5, q.SearchTerms.Length, "The length of SearchTerms should match."); - Assert.AreEqual("ping", q.FirstSearch); - Assert.AreEqual("google.com", q.SecondSearch); - Assert.AreEqual("-n", q.ThirdSearch); + ClassicAssert.AreEqual("ping", q.FirstSearch); + ClassicAssert.AreEqual("google.com", q.SecondSearch); + ClassicAssert.AreEqual("-n", q.ThirdSearch); - Assert.AreEqual("google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters"); + ClassicAssert.AreEqual("google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters"); } [Test] @@ -40,11 +41,11 @@ public void ExclusivePluginQueryIgnoreDisabledTest() Query q = QueryBuilder.Build("> ping google.com -n 20 -6", nonGlobalPlugins); - Assert.AreEqual("> ping google.com -n 20 -6", q.Search); - Assert.AreEqual(q.Search, q.RawQuery, "RawQuery should be equal to Search."); - Assert.AreEqual(6, q.SearchTerms.Length, "The length of SearchTerms should match."); - Assert.AreNotEqual(">", q.ActionKeyword, "ActionKeyword should not match that of a disabled plugin."); - Assert.AreEqual("ping google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters"); + ClassicAssert.AreEqual("> ping google.com -n 20 -6", q.Search); + ClassicAssert.AreEqual(q.Search, q.RawQuery, "RawQuery should be equal to Search."); + ClassicAssert.AreEqual(6, q.SearchTerms.Length, "The length of SearchTerms should match."); + ClassicAssert.AreNotEqual(">", q.ActionKeyword, "ActionKeyword should not match that of a disabled plugin."); + ClassicAssert.AreEqual("ping google.com -n 20 -6", q.SecondToEndSearch, "SecondToEndSearch should be trimmed of multiple whitespace characters"); } [Test] @@ -52,13 +53,13 @@ public void GenericPluginQueryTest() { Query q = QueryBuilder.Build("file.txt file2 file3", new Dictionary()); - Assert.AreEqual("file.txt file2 file3", q.Search); - Assert.AreEqual("", q.ActionKeyword); + ClassicAssert.AreEqual("file.txt file2 file3", q.Search); + ClassicAssert.AreEqual("", q.ActionKeyword); - Assert.AreEqual("file.txt", q.FirstSearch); - Assert.AreEqual("file2", q.SecondSearch); - Assert.AreEqual("file3", q.ThirdSearch); - Assert.AreEqual("file2 file3", q.SecondToEndSearch); + ClassicAssert.AreEqual("file.txt", q.FirstSearch); + ClassicAssert.AreEqual("file2", q.SecondSearch); + ClassicAssert.AreEqual("file3", q.ThirdSearch); + ClassicAssert.AreEqual("file2 file3", q.SecondToEndSearch); } } } diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index 765a1a5593a..020a38fb564 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -16,6 +16,7 @@ using Flow.Launcher.Infrastructure.Logger; using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.ViewModel; +using Velopack; using Stopwatch = Flow.Launcher.Infrastructure.Stopwatch; namespace Flow.Launcher @@ -36,6 +37,12 @@ public partial class App : IDisposable, ISingleInstanceApp [STAThread] public static void Main() { + VelopackApp.Build() + .WithAfterUpdateFastCallback(x => + { + Updater.RecoverPortableData(); + }).Run(); + if (SingleInstance.InitializeAsFirstInstance(Unique)) { using (var application = new App()) @@ -58,7 +65,7 @@ await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => RegisterAppDomainExceptions(); RegisterDispatcherUnhandledException(); - var imageLoadertask = ImageLoader.InitializeAsync(); + var imageLoaderTask = ImageLoader.InitializeAsync(); _settingsVM = new SettingWindowViewModel(_updater, _portable); _settings = _settingsVM.Settings; @@ -80,8 +87,7 @@ await Stopwatch.NormalAsync("|App.OnStartup|Startup cost", async () => Http.Proxy = _settings.Proxy; await PluginManager.InitializePluginsAsync(API); - await imageLoadertask; - + await imageLoaderTask; var window = new MainWindow(_settings, _mainVM); Log.Info($"|App.OnStartup|Dependencies Info:{ErrorReporting.DependenciesInfo()}"); @@ -179,8 +185,8 @@ private static void RegisterAppDomainExceptions() public void Dispose() { - // if sessionending is called, exit proverbially be called when log off / shutdown - // but if sessionending is not called, exit won't be called when log off / shutdown + // if session-ending is called, exit proverbially be called when log off / shutdown + // but if session-ending is not called, exit won't be called when log off / shutdown if (!_disposed) { API.SaveAppAllSettings(); diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj index 9003e6c52d9..112ada2238e 100644 --- a/Flow.Launcher/Flow.Launcher.csproj +++ b/Flow.Launcher/Flow.Launcher.csproj @@ -4,7 +4,7 @@ WinExe net7.0-windows10.0.19041.0 true - false + 1.0.0 Flow.Launcher.App Resources\app.ico app.manifest @@ -95,6 +95,7 @@ + diff --git a/Flow.Launcher/Helper/SingleInstance.cs b/Flow.Launcher/Helper/SingleInstance.cs index 739fed378e0..d46051126ec 100644 --- a/Flow.Launcher/Helper/SingleInstance.cs +++ b/Flow.Launcher/Helper/SingleInstance.cs @@ -1,17 +1,20 @@ using System; using System.Collections.Generic; using System.ComponentModel; +using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; using System.IO.Pipes; +using System.Reflection; using System.Security; using System.Text; using System.Threading; using System.Threading.Tasks; using System.Windows; +using Velopack; // http://blogs.microsoft.co.il/arik/2010/05/28/wpf-single-instance-application/ -// modified to allow single instace restart +// modified to allow single instance restart namespace Flow.Launcher.Helper { internal enum WM @@ -119,8 +122,10 @@ internal enum WM DWMWINDOWMAXIMIZEDCHANGE = 0x0321, #region Windows 7 + DWMSENDICONICTHUMBNAIL = 0x0323, DWMSENDICONICLIVEPREVIEWBITMAP = 0x0326, + #endregion USER = 0x0400, @@ -140,7 +145,8 @@ internal static class NativeMethods public delegate IntPtr MessageHandler(WM uMsg, IntPtr wParam, IntPtr lParam, out bool handled); [DllImport("shell32.dll", EntryPoint = "CommandLineToArgvW", CharSet = CharSet.Unicode)] - private static extern IntPtr _CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string cmdLine, out int numArgs); + private static extern IntPtr _CommandLineToArgvW([MarshalAs(UnmanagedType.LPWStr)] string cmdLine, + out int numArgs); [DllImport("kernel32.dll", EntryPoint = "LocalFree", SetLastError = true)] @@ -159,6 +165,7 @@ public static string[] CommandLineToArgvW(string cmdLine) { throw new Win32Exception(); } + var result = new string[numArgs]; for (int i = 0; i < numArgs; i++) @@ -171,19 +178,17 @@ public static string[] CommandLineToArgvW(string cmdLine) } finally { - IntPtr p = _LocalFree(argv); // Otherwise LocalFree failed. // Assert.AreEqual(IntPtr.Zero, p); } } + } - } - - public interface ISingleInstanceApp - { - void OnSecondAppStarted(); - } + public interface ISingleInstanceApp + { + void OnSecondAppStarted(); + } /// /// This class checks to make sure that only one instance of @@ -196,9 +201,9 @@ public interface ISingleInstanceApp /// running as Administrator, can activate it with command line arguments. /// For most apps, this will not be much of an issue. /// - public static class SingleInstance - where TApplication: Application , ISingleInstanceApp - + public static class SingleInstance + where TApplication : Application, ISingleInstanceApp + { #region Private Fields @@ -215,39 +220,64 @@ public static class SingleInstance /// /// Application mutex. /// - internal static Mutex singleInstanceMutex; + private static Mutex singleInstanceMutex; #endregion - #region Public Properties + #region Public Methods - #endregion + public static void Restart() + { + singleInstanceMutex?.ReleaseMutex(); + StopRemoteServiceTokenSource?.Cancel(); + + while (RemoteServiceRunning) + { + // busy wait + Thread.Sleep(10); + } + + var info = new ProcessStartInfo + { + Arguments = "/C choice /C Y /N /D Y /T 1 & START \"\" \"" + Environment.ProcessPath + "\"", + WindowStyle = ProcessWindowStyle.Hidden, + CreateNoWindow = true, + FileName = "cmd.exe" + }; + Process.Start(info); + Application.Current.Shutdown(); + } + + // this is always going to run only once + private static CancellationTokenSource StopRemoteServiceTokenSource { get; set; } + + private static volatile bool RemoteServiceRunning = true; - #region Public Methods /// /// Checks if the instance of the application attempting to start is the first instance. /// If not, activates the first instance. /// /// True if this is the first instance of the application. - public static bool InitializeAsFirstInstance( string uniqueName ) + public static bool InitializeAsFirstInstance(string uniqueName) { + StopRemoteServiceTokenSource = new CancellationTokenSource(); + // Build unique application Id and the IPC channel name. string applicationIdentifier = uniqueName + Environment.UserName; string channelName = String.Concat(applicationIdentifier, Delimiter, ChannelNameSuffix); // Create mutex based on unique application Id to check if this is the first instance of the application. - bool firstInstance; - singleInstanceMutex = new Mutex(true, applicationIdentifier, out firstInstance); + singleInstanceMutex = new Mutex(true, applicationIdentifier, out var firstInstance); if (firstInstance) { - _ = CreateRemoteService(channelName); + _ = Task.Run(() => StartRemoteServiceAsync(channelName, StopRemoteServiceTokenSource.Token)); return true; } else { - _ = SignalFirstInstance(channelName); + _ = Task.Run(() => SignalFirstInstanceAsync(channelName)); return false; } } @@ -258,6 +288,7 @@ public static bool InitializeAsFirstInstance( string uniqueName ) public static void Cleanup() { singleInstanceMutex?.ReleaseMutex(); + StopRemoteServiceTokenSource?.Cancel(); } #endregion @@ -268,22 +299,21 @@ public static void Cleanup() /// Gets command line args - for ClickOnce deployed applications, command line args may not be passed directly, they have to be retrieved. /// /// List of command line arg strings. - private static IList GetCommandLineArgs( string uniqueApplicationName ) + private static IList GetCommandLineArgs(string uniqueApplicationName) { string[] args = null; try { - // The application was not clickonce deployed, get args from standard API's + // The application was not ClickOnce deployed, get args from standard API's args = Environment.GetCommandLineArgs(); } catch (NotSupportedException) { - - // The application was clickonce deployed - // Clickonce deployed apps cannot recieve traditional commandline arguments - // As a workaround commandline arguments can be written to a shared location before - // the app is launched and the app can obtain its commandline arguments from the + // The application was ClickOnce deployed + // ClickOnce deployed apps cannot receive traditional command line arguments + // As a workaround command line arguments can be written to a shared location before + // the app is launched and the app can obtain its command line arguments from the // shared location string appFolderPath = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), uniqueApplicationName); @@ -293,10 +323,8 @@ private static IList GetCommandLineArgs( string uniqueApplicationName ) { try { - using (TextReader reader = new StreamReader(cmdLinePath, Encoding.Unicode)) - { - args = NativeMethods.CommandLineToArgvW(reader.ReadToEnd()); - } + using TextReader reader = new StreamReader(cmdLinePath, Encoding.Unicode); + args = NativeMethods.CommandLineToArgvW(reader.ReadToEnd()); File.Delete(cmdLinePath); } @@ -306,10 +334,7 @@ private static IList GetCommandLineArgs( string uniqueApplicationName ) } } - if (args == null) - { - args = new string[] { }; - } + args ??= Array.Empty(); return new List(args); } @@ -319,40 +344,48 @@ private static IList GetCommandLineArgs( string uniqueApplicationName ) /// Once receives signal from client, will activate first instance. /// /// Application's IPC channel name. - private static async Task CreateRemoteService(string channelName) + /// Cancellation token + private static async Task StartRemoteServiceAsync(string channelName, CancellationToken token = default) { - using (NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In)) + try { - while(true) + await using NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In); + while (true) { + if (token.IsCancellationRequested) + { + break; + } + // Wait for connection to the pipe - await pipeServer.WaitForConnectionAsync(); + await pipeServer.WaitForConnectionAsync(token); if (Application.Current != null) { // Do an asynchronous call to ActivateFirstInstance function Application.Current.Dispatcher.Invoke(ActivateFirstInstance); } - // Disconect client + + // Disconnect client pipeServer.Disconnect(); } } + finally + { + RemoteServiceRunning = false; + } } /// /// Creates a client pipe and sends a signal to server to launch first instance /// /// Application's IPC channel name. - /// - /// Command line arguments for the second instance, passed to the first instance to take appropriate action. - /// - private static async Task SignalFirstInstance(string channelName) + private static async Task SignalFirstInstanceAsync(string channelName) { // Create a client pipe connected to server - using (NamedPipeClientStream pipeClient = new NamedPipeClientStream(".", channelName, PipeDirection.Out)) - { - // Connect to the available pipe - await pipeClient.ConnectAsync(0); - } + await using NamedPipeClientStream pipeClient = + new NamedPipeClientStream(".", channelName, PipeDirection.Out); + // Connect to the available pipe + await pipeClient.ConnectAsync(0); } /// diff --git a/Flow.Launcher/Properties/AssemblyInfo.cs b/Flow.Launcher/Properties/AssemblyInfo.cs index 93ce37f2b5c..c2ee533d1ef 100644 --- a/Flow.Launcher/Properties/AssemblyInfo.cs +++ b/Flow.Launcher/Properties/AssemblyInfo.cs @@ -1,4 +1,5 @@ -using System.Windows; +using System.Reflection; +using System.Windows; [assembly: ThemeInfo( ResourceDictionaryLocation.None, diff --git a/Flow.Launcher/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index b49bf39d3c5..d53b7088da7 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -4,7 +4,6 @@ using System.Net; using System.Threading.Tasks; using System.Windows; -using Squirrel; using Flow.Launcher.Core.Plugin; using Flow.Launcher.Core.Resource; using Flow.Launcher.Helper; @@ -41,7 +40,7 @@ public PublicAPIInstance(SettingWindowViewModel settingsVM, MainViewModel mainVM _settingsVM = settingsVM; _mainVM = mainVM; _alphabet = alphabet; - GlobalHotkey.hookedKeyboardCallback = KListener_hookedKeyboardCallback; + GlobalHotkey.hookedKeyboardCallback = KListenerHookedKeyboardCallback; WebRequest.RegisterPrefix("data", new DataWebRequestFactory()); } @@ -62,11 +61,8 @@ public void RestartApp() // UpdateManager.RestartApp() will call Environment.Exit(0) // which will cause ungraceful exit SaveAppAllSettings(); - - // Restart requires Squirrel's Update.exe to be present in the parent folder, - // it is only published from the project's release pipeline. When debugging without it, - // the project may not restart or just terminates. This is expected. - UpdateManager.RestartApp(Constant.ApplicationFileName); + + SingleInstance.Restart(); } public void ShowMainWindow() => _mainVM.Show(); @@ -321,12 +317,12 @@ public bool IsGameModeOn() #region Private Methods - private bool KListener_hookedKeyboardCallback(KeyEvent keyevent, int vkcode, SpecialKeyState state) + private bool KListenerHookedKeyboardCallback(KeyEvent keyEvent, int vkCode, SpecialKeyState state) { var continueHook = true; foreach (var x in _globalKeyboardHandlers) { - continueHook &= x((int)keyevent, vkcode, state); + continueHook &= x((int)keyEvent, vkCode, state); } return continueHook; diff --git a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml index 30e065b1601..8c072f9aedd 100644 --- a/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml +++ b/Flow.Launcher/SettingPages/Views/SettingsPaneGeneral.xaml @@ -161,6 +161,7 @@ Sub="{DynamicResource portableModeToolTIp}"> diff --git a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs index a48d70f2d46..bb8cec137e0 100644 --- a/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.BrowserBookmark/Main.cs @@ -15,7 +15,7 @@ namespace Flow.Launcher.Plugin.BrowserBookmark; public class Main : ISettingProvider, IPlugin, IReloadable, IPluginI18n, IContextMenu, IDisposable { - private static PluginInitContext _context; + internal static PluginInitContext _context { get; private set; } = null!; private static List _cachedBookmarks = new List(); diff --git a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs index 5077e60614d..b65f5652e7f 100644 --- a/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs +++ b/Plugins/Flow.Launcher.Plugin.Calculator/Main.cs @@ -28,7 +28,7 @@ public class Main : IPlugin, IPluginI18n, ISettingProvider private const string comma = ","; private const string dot = "."; - private PluginInitContext Context { get; set; } + internal static PluginInitContext Context { get; set; } private static Settings _settings; private static SettingsViewModel _viewModel; diff --git a/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs b/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs index 9800fa0209c..94f808ec9ff 100644 --- a/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs +++ b/Plugins/Flow.Launcher.Plugin.PluginsManager/Utilities.cs @@ -1,6 +1,4 @@ using Flow.Launcher.Core.ExternalPlugins; -using Flow.Launcher.Infrastructure.UserSettings; -using ICSharpCode.SharpZipLib.Zip; using Newtonsoft.Json; using System.IO; using System.IO.Compression; @@ -18,29 +16,7 @@ internal static class Utilities /// overwrite internal static void UnZip(string zipFilePath, string strDirectory, bool overwrite) { - if (strDirectory == "") - strDirectory = Directory.GetCurrentDirectory(); - - using var zipStream = new ZipInputStream(File.OpenRead(zipFilePath)); - - ZipEntry theEntry; - - while ((theEntry = zipStream.GetNextEntry()) != null) - { - var pathToZip = theEntry.Name; - var directoryName = string.IsNullOrEmpty(pathToZip) ? "" : Path.GetDirectoryName(pathToZip); - var fileName = Path.GetFileName(pathToZip); - var destinationDir = Path.Combine(strDirectory, directoryName); - var destinationFile = Path.Combine(destinationDir, fileName); - - Directory.CreateDirectory(destinationDir); - - if (string.IsNullOrEmpty(fileName) || (File.Exists(destinationFile) && !overwrite)) - continue; - - using var streamWriter = File.Create(destinationFile); - zipStream.CopyTo(streamWriter); - } + ZipFile.ExtractToDirectory(zipFilePath, strDirectory, overwrite); } internal static string GetContainingFolderPathAfterUnzip(string unzippedParentFolderPath) diff --git a/README.md b/README.md index c563eafc964..3dcb85019ed 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ scoop install Flow-Launcher choco install Flow-Launcher ``` -> When installing for the first time Windows may raise an issue about security due to code not being signed, if you downloaded from this repo then you are good to continue the set up. +> When installing for the first time Windows may raise an issue about security due to code not being signed, if you downloaded from this repo, then you are good to continue the set up. Or download the [early access version](https://github.com/Flow-Launcher/Prereleases/releases). @@ -122,7 +122,7 @@ Or download the [early access version](https://github.com/Flow-Launcher/Prerelea - Drag a file/folder to File Explorer, or even Discord. -- Copy/move behavior can be change via Ctrl or Shift, and the operation is displayed on the mouse cursor. +- Copy/move behavior can be changed via Ctrl or Shift, and the operation is displayed on the mouse cursor. ### Windows & Control Panel Settings @@ -204,7 +204,7 @@ Or download the [early access version](https://github.com/Flow-Launcher/Prerelea - Fully portable. - Type `flow user data` to open your saved user settings folder. They are located at: - If using roaming: `%APPDATA%\FlowLauncher` - - If using portable, by default: `%localappdata%\FlowLauncher\app-\UserData` + - If using portable, by default: `%LocalAppData%\FlowLauncher\app-\UserData` - Type `open log location` to open your logs folder, they are saved along with your user settings folder. ### 🎮 Game Mode @@ -377,7 +377,7 @@ Each of the pull requests will be marked with a milestone indicating the planned ### Contributing -Contributions are very welcome, in addition to the main project(C#) there are also [documentation](https://github.com/Flow-Launcher/docs)(md), [website](https://github.com/Flow-Launcher/flow-launcher.github.io)(html/css) and [others](https://github.com/Flow-Launcher) that can be contributed to. If you are unsure of a change you want to make, let us know in the [Discussions](https://github.com/Flow-Launcher/Flow.Launcher/discussions/categories/ideas), otherwise feel free to put in a pull request. +Contributions are very welcome, in addition to the main project(C#) there are also [documentation](https://github.com/Flow-Launcher/docs)(md), [website](https://github.com/Flow-Launcher/flow-launcher.github.io)(html/css) and [others](https://github.com/Flow-Launcher) that can be contributed to. If you are unsure of a change you want to make, let us know in the [Discussions](https://github.com/Flow-Launcher/Flow.Launcher/discussions/categories/ideas), otherwise, please consider submitting a pull request. You will find the main goals of flow placed under the [Projects board](https://github.com/Flow-Launcher/Flow.Launcher/projects), so feel free to contribute on that. If you would like to make small incremental changes, feel free to do so as well. diff --git a/Scripts/post_build.ps1 b/Scripts/post_build.ps1 index 1757ed99e22..a6c31949404 100644 --- a/Scripts/post_build.ps1 +++ b/Scripts/post_build.ps1 @@ -1,27 +1,42 @@ param( - [string]$config = "Release", - [string]$solution = (Join-Path $PSScriptRoot ".." -Resolve) + [string]$config = "Release", + [string]$solution = (Join-Path $PSScriptRoot ".." -Resolve), + [string]$channel = "win-x64-prerelease", + [string]$flowVersion = "" ) Write-Host "Config: $config" - -function Build-Version { - if ([string]::IsNullOrEmpty($env:flowVersion)) { +Write-Host "Solution: $solution" +Write-Host "Flow Version: $flowVersion" +Write-Host "Channel: $channel" + +function Build-Version +{ + if ( [string]::IsNullOrEmpty($flowVersion)) + { $targetPath = Join-Path $solution "Output/Release/Flow.Launcher.dll" -Resolve $v = (Get-Command ${targetPath}).FileVersionInfo.FileVersion - } else { - $v = $env:flowVersion + } + else + { + $v = $flowVersion } Write-Host "Build Version: $v" return $v } -function Build-Path { - if (![string]::IsNullOrEmpty($env:APPVEYOR_BUILD_FOLDER)) { +function Build-Path +{ + if (![string]::IsNullOrEmpty($env:APPVEYOR_BUILD_FOLDER)) + { $p = $env:APPVEYOR_BUILD_FOLDER - } elseif (![string]::IsNullOrEmpty($solution)) { + } + elseif (![string]::IsNullOrEmpty($solution)) + { $p = $solution - } else { + } + else + { $p = Get-Location } @@ -31,106 +46,112 @@ function Build-Path { return $p } -function Copy-Resources ($path) { - # making version static as multiple versions can exist in the nuget folder and in the case a breaking change is introduced. - Copy-Item -Force $env:USERPROFILE\.nuget\packages\squirrel.windows\1.5.2\tools\Squirrel.exe $path\Output\Update.exe -} -function Delete-Unused ($path, $config) { +function Delete-Unused($path, $config) +{ $target = "$path\Output\$config" $included = Get-ChildItem $target -Filter "*.dll" - foreach ($i in $included){ - $deleteList = Get-ChildItem $target\Plugins -Include $i -Recurse | Where { $_.VersionInfo.FileVersion -eq $i.VersionInfo.FileVersion -And $_.Name -eq "$i" } - $deleteList | ForEach-Object{ Write-Host Deleting duplicated $_.Name with version $_.VersionInfo.FileVersion at location $_.Directory.FullName } - $deleteList | Remove-Item + + $hashset = @{ } + + foreach ($i in $included) + { + $item = if ($i.VersionInfo.FileVersion -eq $null) + { + [ValueTuple]::Create($i.Name, "") + } + else + { + [ValueTuple]::Create($i.Name, $i.VersionInfo.FileVersion) + } + + $key = $hashset.Add($item, $true) } - Remove-Item -Path $target -Include "*.xml" -Recurse -} -function Remove-CreateDumpExe ($path, $config) { - $target = "$path\Output\$config" + $deleteList = Get-ChildItem $target\Plugins -Filter *.dll -Recurse | Where { + $item = if ($_.VersionInfo.FileVersion -eq $null) + { + [ValueTuple]::Create($_.Name, "") + } + else + { + [ValueTuple]::Create($_.Name, $_.VersionInfo.FileVersion) + } + $hashset.ContainsKey($item) + } - $depjson = Get-Content $target\Flow.Launcher.deps.json -raw - $depjson -replace '(?s)(.createdump.exe": {.*?}.*?\n)\s*', "" | Out-File $target\Flow.Launcher.deps.json -Encoding UTF8 - Remove-Item -Path $target -Include "*createdump.exe" -Recurse + foreach ($i in $deleteList) + { + write "Deleting duplicated $($i.Name) with version $($i.VersionInfo.FileVersion) at location $($i.Directory.FullName)" + Remove-Item -Path $i.FullName + } + Remove-Item -Path $target -Include "*.xml" -Recurse } -function Validate-Directory ($output) { +function Validate-Directory($output) +{ New-Item $output -ItemType Directory -Force } -function Pack-Squirrel-Installer ($path, $version, $output) { +function Pack-Velopack-Installer($path, $version, $output) +{ # msbuild based installer generation is not working in appveyor, not sure why Write-Host "Begin pack squirrel installer" $spec = "$path\Scripts\flowlauncher.nuspec" - $input = "$path\Output\Release" + $input = "$path\Output\$config" Write-Host "Packing: $spec" Write-Host "Input path: $input" + Write-Host "Output path: $output" + + $repoUrl = "https://github.com/Flow-Launcher/Prereleases" + + if ($channel -eq "stable") + { + $repoUrl = "https://github.com/Flow-Launcher/Flow.Launcher" + } - # dotnet pack is not used because ran into issues, need to test installation and starting up if to use it. - nuget pack $spec -Version $version -BasePath $input -OutputDirectory $output -Properties Configuration=Release - - $nupkg = "$output\FlowLauncher.$version.nupkg" - Write-Host "nupkg path: $nupkg" - $icon = "$path\Flow.Launcher\Resources\app.ico" - Write-Host "icon: $icon" - # Squirrel.com: https://github.com/Squirrel/Squirrel.Windows/issues/369 - New-Alias Squirrel $env:USERPROFILE\.nuget\packages\squirrel.windows\1.5.2\tools\Squirrel.exe -Force - # why we need Write-Output: https://github.com/Squirrel/Squirrel.Windows/issues/489#issuecomment-156039327 - # directory of releaseDir in squirrel can't be same as directory ($nupkg) in releasify - $temp = "$output\Temp" - - Squirrel --releasify $nupkg --releaseDir $temp --setupIcon $icon --no-msi | Write-Output - Move-Item $temp\* $output -Force - Remove-Item $temp + Set-Alias vpk "~/.dotnet/tools/vpk.exe" - $file = "$output\Flow-Launcher-Setup.exe" - Write-Host "Filename: $file" + if (!(Get-Command vpk -ErrorAction SilentlyContinue)) + { + dotnet tool install --global vpk + } - Move-Item "$output\Setup.exe" $file -Force + # Create UserData folder before Packing + # FIXME userdata should not be created in installer version + # New-Item -ItemType Directory -Force -Path "$input\UserData" - Write-Host "End pack squirrel installer" + vpk pack --packVersion $version --packDir $input --packId FlowLauncher --mainExe Flow.Launcher.exe --channel $channel --outputDir $output --packTitle "Flow Launcher" --icon "$input\Images\app.ico" --packAuthors "Flow-Launcher Team" } -function Publish-Self-Contained ($p) { - - $csproj = Join-Path "$p" "Flow.Launcher/Flow.Launcher.csproj" -Resolve +function Publish-Self-Contained($p) +{ + $csproj = Join-Path "$p" "Flow.Launcher/Flow.Launcher.csproj" -Resolve $profile = Join-Path "$p" "Flow.Launcher/Properties/PublishProfiles/Net7.0-SelfContained.pubxml" -Resolve # we call dotnet publish on the main project. # The other projects should have been built in Release at this point. - dotnet publish -c Release $csproj /p:PublishProfile=$profile + dotnet publish $csproj /p:PublishProfile=$profile } -function Publish-Portable ($outputLocation, $version) { - - & $outputLocation\Flow-Launcher-Setup.exe --silent | Out-Null - mkdir "$env:LocalAppData\FlowLauncher\app-$version\UserData" - Compress-Archive -Path $env:LocalAppData\FlowLauncher -DestinationPath $outputLocation\Flow-Launcher-Portable.zip -} - -function Main { +function Main +{ $p = Build-Path $v = Build-Version - Copy-Resources $p - if ($config -eq "Release"){ - + if ($config -eq "Release") + { Delete-Unused $p $config Publish-Self-Contained $p - Remove-CreateDumpExe $p $config - - $o = "$p\Output\Packages" + $o = "$p\Releases" Validate-Directory $o - Pack-Squirrel-Installer $p $v $o - - Publish-Portable $o $v + Pack-Velopack-Installer $p $v $o } } diff --git a/SolutionAssemblyInfo.cs b/SolutionAssemblyInfo.cs index 2938bcbbedd..8d8007afef7 100644 --- a/SolutionAssemblyInfo.cs +++ b/SolutionAssemblyInfo.cs @@ -18,4 +18,3 @@ [assembly: ComVisible(false)] [assembly: AssemblyVersion("1.0.0")] [assembly: AssemblyFileVersion("1.0.0")] -[assembly: AssemblyInformationalVersion("1.0.0")] diff --git a/appveyor.yml b/appveyor.yml index 2503abce3a7..eaffd580cfd 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,12 +7,20 @@ init: if ($env:APPVEYOR_REPO_BRANCH -eq "dev") { $env:prereleaseTag = "{0}.{1}.{2}.{3}" -f $version.Major, $version.Minor, $version.Build, $version.Revision + $env:channel = "win-x64-prerelease" + } else { + $env:channel = "win-x64-stable" } + dotnet tool update -g vpk + $env:APPVEYOR_CACHE_ENTRY_ZIP_ARGS = "-t7z -m0=lzma -mx=9" + - sc config WSearch start= auto # Starts Windows Search service- Needed for running ExplorerTest - net start WSearch -cache: - - '%USERPROFILE%\.nuget\packages -> **.sln, **.csproj' # preserve nuget folder (packages) unless the solution or projects change +#cache: +# - '%USERPROFILE%\.nuget\packages -> **.sln, **.csproj' # preserve nuget folder (packages) unless the solution or projects change + +skip_branch_with_pr: true assembly_info: @@ -33,19 +41,19 @@ build: test_script: - dotnet test --no-build -c Release after_test: - - ps: .\Scripts\post_build.ps1 + - ps: .\Scripts\post_build.ps1 -channel $env:channel artifacts: - path: 'Output\Release\Flow.Launcher.Plugin.*.nupkg' name: Plugin nupkg -- path: 'Output\Packages\Flow-Launcher-*.exe' +- path: 'Releases\FlowLauncher-*.exe' name: Squirrel Installer -- path: Output\Packages\Flow-Launcher-Portable.zip +- path: 'Releases\FlowLauncher-*-Portable.zip' name: Portable Version -- path: 'Output\Packages\FlowLauncher-*-full.nupkg' +- path: 'Releases\FlowLauncher-*-full.nupkg' name: Squirrel nupkg -- path: 'Output\Packages\RELEASES' - name: Squirrel RELEASES +- path: 'Releases\RELEASES*' + name: Velopack RELEASES deploy: - provider: NuGet