mirror of
https://github.com/Flow-Launcher/Flow.Launcher.git
synced 2026-03-11 08:54:32 +00:00
first commit
This commit is contained in:
parent
0ab26d6f4f
commit
1cc8b9dfe8
6 changed files with 105 additions and 98 deletions
|
|
@ -57,8 +57,8 @@
|
|||
<PackageReference Include="FSharp.Core" Version="7.0.401" />
|
||||
<PackageReference Include="Meziantou.Framework.Win32.Jobs" Version="3.2.1" />
|
||||
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" Version="3.0.0" />
|
||||
<PackageReference Include="squirrel.windows" Version="1.5.2" NoWarn="NU1701" />
|
||||
<PackageReference Include="StreamJsonRpc" Version="2.17.11" />
|
||||
<PackageReference Include="Velopack" Version="0.0.359" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
|
|
|||
|
|
@ -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<UpdateManager> 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<List<GithubRelease>>(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)
|
||||
|
|
|
|||
|
|
@ -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<App>.InitializeAsFirstInstance(Unique))
|
||||
{
|
||||
using (var application = new App())
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@
|
|||
<PackageReference Include="NHotkey.Wpf" Version="2.1.1" />
|
||||
<PackageReference Include="PropertyChanged.Fody" Version="3.4.0" />
|
||||
<PackageReference Include="SharpVectors" Version="1.8.2" />
|
||||
<PackageReference Include="Velopack" Version="0.0.359" />
|
||||
<PackageReference Include="VirtualizingWrapPanel" Version="1.5.8" />
|
||||
</ItemGroup>
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </remarks>
|
||||
public static class SingleInstance<TApplication>
|
||||
where TApplication: Application , ISingleInstanceApp
|
||||
|
||||
public static class SingleInstance<TApplication>
|
||||
where TApplication : Application, ISingleInstanceApp
|
||||
|
||||
{
|
||||
#region Private Fields
|
||||
|
||||
|
|
@ -215,22 +217,40 @@ namespace Flow.Launcher.Helper
|
|||
/// <summary>
|
||||
/// Application mutex.
|
||||
/// </summary>
|
||||
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;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the instance of the application attempting to start is the first instance.
|
||||
/// If not, activates the first instance.
|
||||
/// </summary>
|
||||
/// <returns>True if this is the first instance of the application.</returns>
|
||||
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.
|
||||
/// </summary>
|
||||
/// <returns>List of command line arg strings.</returns>
|
||||
private static IList<string> GetCommandLineArgs( string uniqueApplicationName )
|
||||
private static IList<string> 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<string>();
|
||||
|
||||
return new List<string>(args);
|
||||
}
|
||||
|
|
@ -319,40 +333,42 @@ namespace Flow.Launcher.Helper
|
|||
/// Once receives signal from client, will activate first instance.
|
||||
/// </summary>
|
||||
/// <param name="channelName">Application's IPC channel name.</param>
|
||||
private static async Task CreateRemoteService(string channelName)
|
||||
/// <param name="token">Cancellation token</param>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a client pipe and sends a signal to server to launch first instance
|
||||
/// </summary>
|
||||
/// <param name="channelName">Application's IPC channel name.</param>
|
||||
/// <param name="args">
|
||||
/// Command line arguments for the second instance, passed to the first instance to take appropriate action.
|
||||
/// </param>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -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<App>.Restart();
|
||||
}
|
||||
|
||||
public void ShowMainWindow() => _mainVM.Show();
|
||||
|
|
|
|||
Loading…
Reference in a new issue