From 1cc8b9dfe86685abc55a82804b37354a591df58b Mon Sep 17 00:00:00 2001 From: Hongtao Zhang Date: Sat, 23 Mar 2024 18:00:32 -0500 Subject: [PATCH] first commit --- Flow.Launcher.Core/Flow.Launcher.Core.csproj | 2 +- Flow.Launcher.Core/Updater.cs | 70 +++++------ Flow.Launcher/App.xaml.cs | 3 + Flow.Launcher/Flow.Launcher.csproj | 1 + Flow.Launcher/Helper/SingleInstance.cs | 120 +++++++++++-------- Flow.Launcher/PublicAPIInstance.cs | 7 +- 6 files changed, 105 insertions(+), 98 deletions(-) diff --git a/Flow.Launcher.Core/Flow.Launcher.Core.csproj b/Flow.Launcher.Core/Flow.Launcher.Core.csproj index 18101ccf0..046c75645 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/Updater.cs b/Flow.Launcher.Core/Updater.cs index 3f64b273e..3a8e7ec7f 100644 --- a/Flow.Launcher.Core/Updater.cs +++ b/Flow.Launcher.Core/Updater.cs @@ -17,6 +17,8 @@ using Flow.Launcher.Infrastructure.UserSettings; using Flow.Launcher.Plugin; using System.Text.Json.Serialization; using System.Threading; +using Velopack; +using Velopack.Sources; namespace Flow.Launcher.Core { @@ -40,15 +42,23 @@ namespace Flow.Launcher.Core 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,16 +71,18 @@ namespace Flow.Launcher.Core api.ShowMsg(api.GetTranslation("update_flowlauncher_update_found"), api.GetTranslation("update_flowlauncher_updating")); - await updateManager.DownloadReleases(newUpdateInfo.ReleasesToApply).ConfigureAwait(false); + await updateManager.DownloadUpdatesAsync(newUpdateInfo).ConfigureAwait(false); await updateManager.ApplyReleases(newUpdateInfo).ConfigureAwait(false); if (DataLocation.PortableDataLocationInUse()) { - var targetDestination = updateManager.RootAppDirectory + $"\\app-{newReleaseVersion.ToString()}\\{DataLocation.PortableFolderName}"; + 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"), + MessageBox.Show(string.Format( + api.GetTranslation("update_flowlauncher_fail_moving_portable_user_profile_data"), DataLocation.PortableDataPath, targetDestination)); } @@ -83,18 +95,22 @@ namespace Flow.Launcher.Core 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.RestartApp(Constant.ApplicationFileName); } } 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")); @@ -108,37 +124,11 @@ namespace Flow.Launcher.Core [UsedImplicitly] private class GithubRelease { - [JsonPropertyName("prerelease")] - public bool Prerelease { get; [UsedImplicitly] set; } + [JsonPropertyName("prerelease")] public bool Prerelease { get; [UsedImplicitly] set; } - [JsonPropertyName("published_at")] - public DateTime PublishedAt { get; [UsedImplicitly] set; } + [JsonPropertyName("published_at")] public DateTime PublishedAt { get; [UsedImplicitly] set; } - [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 - { - Proxy = Http.WebProxy - }; - var downloader = new FileDownloader(client); - - var manager = new UpdateManager(latestUrl, urlDownloader: downloader); - - return manager; + [JsonPropertyName("html_url")] public string HtmlUrl { get; [UsedImplicitly] set; } } public string NewVersionTips(string version) diff --git a/Flow.Launcher/App.xaml.cs b/Flow.Launcher/App.xaml.cs index f4a17761f..fbc7e7f31 100644 --- a/Flow.Launcher/App.xaml.cs +++ b/Flow.Launcher/App.xaml.cs @@ -16,6 +16,7 @@ using Flow.Launcher.Infrastructure.Image; 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,8 @@ namespace Flow.Launcher [STAThread] public static void Main() { + VelopackApp.Build().Run(); + if (SingleInstance.InitializeAsFirstInstance(Unique)) { using (var application = new App()) diff --git a/Flow.Launcher/Flow.Launcher.csproj b/Flow.Launcher/Flow.Launcher.csproj index 53c1bafc7..893d9515f 100644 --- a/Flow.Launcher/Flow.Launcher.csproj +++ b/Flow.Launcher/Flow.Launcher.csproj @@ -96,6 +96,7 @@ + diff --git a/Flow.Launcher/Helper/SingleInstance.cs b/Flow.Launcher/Helper/SingleInstance.cs index 739fed378..b5cd67a00 100644 --- a/Flow.Launcher/Helper/SingleInstance.cs +++ b/Flow.Launcher/Helper/SingleInstance.cs @@ -119,8 +119,10 @@ namespace Flow.Launcher.Helper DWMWINDOWMAXIMIZEDCHANGE = 0x0321, #region Windows 7 + DWMSENDICONICTHUMBNAIL = 0x0323, DWMSENDICONICLIVEPREVIEWBITMAP = 0x0326, + #endregion USER = 0x0400, @@ -140,7 +142,8 @@ namespace Flow.Launcher.Helper 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 +162,7 @@ namespace Flow.Launcher.Helper { throw new Win32Exception(); } + var result = new string[numArgs]; for (int i = 0; i < numArgs; i++) @@ -171,19 +175,17 @@ namespace Flow.Launcher.Helper } 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 +198,9 @@ namespace Flow.Launcher.Helper /// 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,22 +217,40 @@ namespace Flow.Launcher.Helper /// /// Application mutex. /// - internal static Mutex singleInstanceMutex; - - #endregion - - #region Public Properties + private static Mutex singleInstanceMutex; #endregion #region Public Methods + public static void Restart() + { + singleInstanceMutex?.ReleaseMutex(); + + StopRemoteServiceTokenSource?.Cancel(); + + while (RemoteServiceRunning) + { + // busy wait + Thread.Sleep(10); + } + + System.Diagnostics.Process.Start(Application.ResourceAssembly.Location); + Application.Current.Shutdown(); + } + + // this is always going to run only once + private static CancellationTokenSource StopRemoteServiceTokenSource { get; set; } + + private static volatile bool RemoteServiceRunning = false; + + /// /// 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) { // Build unique application Id and the IPC channel name. string applicationIdentifier = uniqueName + Environment.UserName; @@ -238,16 +258,15 @@ namespace Flow.Launcher.Helper 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); + _ = StartRemoteServiceAsync(channelName, StopRemoteServiceTokenSource.Token); return true; } else { - _ = SignalFirstInstance(channelName); + _ = SignalFirstInstanceAsync(channelName); return false; } } @@ -258,6 +277,7 @@ namespace Flow.Launcher.Helper public static void Cleanup() { singleInstanceMutex?.ReleaseMutex(); + StopRemoteServiceTokenSource?.Cancel(); } #endregion @@ -268,7 +288,7 @@ namespace Flow.Launcher.Helper /// 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; @@ -279,7 +299,6 @@ namespace Flow.Launcher.Helper } 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 @@ -293,10 +312,8 @@ namespace Flow.Launcher.Helper { 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 +323,7 @@ namespace Flow.Launcher.Helper } } - if (args == null) - { - args = new string[] { }; - } + args ??= Array.Empty(); return new List(args); } @@ -319,40 +333,42 @@ namespace Flow.Launcher.Helper /// 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)) + await using NamedPipeServerStream pipeServer = new NamedPipeServerStream(channelName, PipeDirection.In); + while (true) { - while(true) + if (token.IsCancellationRequested) { - // Wait for connection to the pipe - await pipeServer.WaitForConnectionAsync(); - if (Application.Current != null) - { - // Do an asynchronous call to ActivateFirstInstance function - Application.Current.Dispatcher.Invoke(ActivateFirstInstance); - } - // Disconect client - pipeServer.Disconnect(); + break; } + + // Wait for connection to the pipe + await pipeServer.WaitForConnectionAsync(token); + if (Application.Current != null) + { + // Do an asynchronous call to ActivateFirstInstance function + Application.Current.Dispatcher.Invoke(ActivateFirstInstance); + } + + // Disconnect client + pipeServer.Disconnect(); } + + RemoteServiceRunning = true; } /// /// 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/PublicAPIInstance.cs b/Flow.Launcher/PublicAPIInstance.cs index 36309a22a..00e7df3d0 100644 --- a/Flow.Launcher/PublicAPIInstance.cs +++ b/Flow.Launcher/PublicAPIInstance.cs @@ -62,11 +62,8 @@ namespace Flow.Launcher // 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();