Allow waking up another process when started a second time.

This was not possible before and was solved by sending hotkey in version 1.
In v2 it will use named pipes for IPC communication, so it don't rely on hotkeys any more.
This commit is contained in:
Peter Kirmeier 2023-06-03 20:18:10 +02:00
parent dda3d94e7b
commit 0e812d207b
8 changed files with 223 additions and 143 deletions

161
Business/IpcPipe.cs Normal file
View file

@ -0,0 +1,161 @@
// <copyright file="IpcPipe.cs" company="PlaceholderCompany">
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
//
// Copyright (c) 2023-2023 Peter Kirmeier
// Based on example: https://learn.microsoft.com/en-us/dotnet/standard/io/how-to-use-named-pipes-for-network-interprocess-communication
namespace SystemTrayMenu.Business
{
using System;
using System.IO;
using System.IO.Pipes;
using System.Reflection;
using System.Text;
using System.Threading;
using SystemTrayMenu.Utilities;
internal class IpcPipe : IDisposable
{
private readonly string pipeName;
private readonly string pipeHello;
private NamedPipeServerStream? serverPipe;
private Thread? serverThread;
private Func<string, string>? serverHandler;
private bool isStopped;
internal IpcPipe(byte version, string name)
{
pipeName = version.ToString() + "." + name + "." + (Assembly.GetExecutingAssembly().GetName().Name ?? "local");
pipeHello = "Hello " + nameof(IpcPipe) + " from " + pipeName + "!";
}
public void Dispose()
{
isStopped = true;
serverPipe?.Disconnect();
serverThread?.Join(250);
serverPipe?.Dispose();
serverThread = null;
}
internal void StartServer(Func<string, string> handler)
{
serverHandler = handler;
serverThread = new(ServerThread);
serverThread?.Start(this);
}
internal string? SendToServer(string request)
{
string? response = null;
NamedPipeClientStream pipeClient = new(pipeName);
// Wait a bit in case server is restarting
pipeClient.Connect(500);
IpcPipeStream stream = new (pipeClient);
string hello = stream.ReadString();
// Check who there is, just for sanity
if (hello == pipeHello)
{
stream.WriteString(request);
response = stream.ReadString();
}
else
{
Log.Info($"IPC client pipe error: Invalid hello: \"" + hello + "\"");
}
pipeClient.Dispose();
return response;
}
private static void ServerThread(object? data)
{
IpcPipe ipc = (IpcPipe)data!;
Func<string, string> handler = ipc.serverHandler ?? (_ => string.Empty);
while (!ipc.isStopped)
{
NamedPipeServerStream pipe = ipc.serverPipe = new(ipc.pipeName);
try
{
pipe.WaitForConnection();
IpcPipeStream stream = new(pipe);
// Send indicator who we are, just for sanity checking
stream.WriteString(ipc.pipeHello);
string request = stream.ReadString();
string respone = handler.Invoke(request);
stream.WriteString(respone);
}
catch (Exception ex)
{
Log.Warn($"IPC server pipe error: ", ex);
}
ipc.serverPipe = null;
pipe.Dispose();
}
}
// Defines the data protocol for reading and writing strings on our stream
private class IpcPipeStream
{
private readonly Stream ioStream;
private readonly UnicodeEncoding streamEncoding = new();
internal IpcPipeStream(Stream ioStream)
{
this.ioStream = ioStream;
}
internal string ReadString()
{
int len = 0;
// Receive 32 bit message size
len += ioStream.ReadByte() >> 24;
len += ioStream.ReadByte() >> 16;
len += ioStream.ReadByte() >> 8;
len += ioStream.ReadByte() >> 0;
// Receive message
byte[] inBuffer = new byte[len];
ioStream.Read(inBuffer, 0, len);
return streamEncoding.GetString(inBuffer);
}
internal int WriteString(string outString)
{
byte[] outBuffer = streamEncoding.GetBytes(outString);
int len = outBuffer.Length;
if (len > int.MaxValue)
{
throw new InternalBufferOverflowException("More data than available space withing an IPC message");
}
// Send 32 bit message size
ioStream.WriteByte((byte)((len >> 24) & 0xFF));
ioStream.WriteByte((byte)((len >> 16) & 0xFF));
ioStream.WriteByte((byte)((len >> 8) & 0xFF));
ioStream.WriteByte((byte)((len >> 0) & 0xFF));
// Send message
ioStream.Write(outBuffer, 0, len);
ioStream.Flush();
return outBuffer.Length + 2;
}
}
}
}

View file

@ -2,7 +2,7 @@
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace SystemTrayMenu.Handler
namespace SystemTrayMenu.Business
{
using System;
using System.Windows.Input;

View file

@ -17,7 +17,6 @@ namespace SystemTrayMenu.Business
using Microsoft.Win32;
using SystemTrayMenu.DataClasses;
using SystemTrayMenu.DllImports;
using SystemTrayMenu.Handler;
using SystemTrayMenu.Helpers;
using SystemTrayMenu.Properties;
using SystemTrayMenu.UserInterface;
@ -44,6 +43,7 @@ namespace SystemTrayMenu.Business
public Menus()
{
SingleAppInstance.Wakeup += SwitchOpenCloseByKey;
menuNotifyIcon.Click += () => SwitchOpenClose(true, false);
if (!keyboardInput.RegisterHotKey(Settings.Default.HotKey))
@ -52,7 +52,7 @@ namespace SystemTrayMenu.Business
Settings.Default.Save();
}
keyboardInput.HotKeyPressed += () => SwitchOpenClose(false, false);
keyboardInput.HotKeyPressed += SwitchOpenCloseByKey;
keyboardInput.RowSelectionChanged += waitToOpenMenu.RowSelectionChanged;
keyboardInput.EnterPressed += waitToOpenMenu.OpenSubMenuByKey;
@ -130,6 +130,7 @@ namespace SystemTrayMenu.Business
public void Dispose()
{
SingleAppInstance.Wakeup -= SwitchOpenCloseByKey;
SystemEvents.DisplaySettingsChanged -= SystemEvents_DisplaySettingsChanged;
workerMainMenu.Dispose();
foreach (BackgroundWorker worker in workersSubMenu)
@ -350,7 +351,7 @@ namespace SystemTrayMenu.Business
if (menuData.DirectoryState != MenuDataDirectoryState.Undefined)
{
// Sub Menu (completed)
menu.AddItemsToMenu(menuData.RowDatas, menuData.DirectoryState, true);
menu.AddItemsToMenu(menuData.RowDatas, menuData.DirectoryState);
AdjustMenusSizeAndLocation(menu.Level);
}
else
@ -365,7 +366,7 @@ namespace SystemTrayMenu.Business
private void InitializeMenu(Menu menu, List<RowData> rowDatas)
{
menu.AddItemsToMenu(rowDatas, null, false);
menu.AddItemsToMenu(rowDatas, null);
menu.MenuScrolled += () => AdjustMenusSizeAndLocation(menu.Level + 1); // TODO: Only update vertical location while scrolling?
menu.MouseLeave += (_, _) =>
@ -512,6 +513,8 @@ namespace SystemTrayMenu.Business
}
}
private void SwitchOpenCloseByKey() => mainMenu.Dispatcher.Invoke(() => SwitchOpenClose(false, false));
private void SystemEvents_DisplaySettingsChanged(object? sender, EventArgs e) =>
mainMenu.Dispatcher.Invoke(() => mainMenu.RelocateOnNextShow = true);
@ -710,7 +713,7 @@ namespace SystemTrayMenu.Business
rowDatas = DirectoryHelpers.SortItems(rowDatas);
menu.SelectedItem = null;
menu.AddItemsToMenu(rowDatas, null, true);
menu.AddItemsToMenu(rowDatas, null);
menu.OnWatcherUpdate();
}
catch (Exception ex)
@ -776,7 +779,7 @@ namespace SystemTrayMenu.Business
rowDatas = DirectoryHelpers.SortItems(rowDatas);
menu.SelectedItem = null;
menu.AddItemsToMenu(rowDatas, null, true);
menu.AddItemsToMenu(rowDatas, null);
menu.OnWatcherUpdate();
}
catch (Exception ex)

View file

@ -56,6 +56,7 @@ namespace SystemTrayMenu
}
finally
{
SingleAppInstance.Unload();
Log.Close();
}

View file

@ -2,7 +2,7 @@
// Copyright (c) PlaceholderCompany. All rights reserved.
// </copyright>
namespace SystemTrayMenu.Handler
namespace SystemTrayMenu.Business
{
using System;
using System.Windows.Threading;

View file

@ -338,91 +338,5 @@ namespace SystemTrayMenu.UserInterface
/// Without this, a "A" key press would appear as "aControl, Alt + A".
/// </summary>
private void HandlePreviewTextInput(object sender, TextCompositionEventArgs e) => e.Handled = true;
#if TODO // HOTKEY
/// <summary>
/// Helper method to cleanly register a hotkey.
/// </summary>
/// <param name="failedKeys">failedKeys.</param>
/// <param name="hotkeyString">hotkeyString.</param>
/// <param name="handler">handler.</param>
/// <returns>bool success.</returns>
private static bool RegisterHotkey(StringBuilder failedKeys, string hotkeyString, HotKeyHandler handler)
{
Keys modifierKeyCode = HotkeyModifiersFromString(hotkeyString);
Keys virtualKeyCode = HotkeyFromString(hotkeyString);
if (!Keys.None.Equals(virtualKeyCode))
{
if (RegisterHotKey(modifierKeyCode, virtualKeyCode, handler) < 0)
{
if (failedKeys.Length > 0)
{
failedKeys.Append(", ");
}
failedKeys.Append(hotkeyString);
return false;
}
}
return true;
}
/// <summary>
/// Registers all hotkeys as configured, displaying a dialog in case of hotkey conflicts with other tools.
/// </summary>
/// <param name="ignoreFailedRegistration">if true, a failed hotkey registration will not be reported to the user - the hotkey will simply not be registered.</param>
/// <returns>Whether the hotkeys could be registered to the users content. This also applies if conflicts arise and the user decides to ignore these (i.e. not to register the conflicting hotkey).</returns>
private static bool RegisterHotkeys(bool ignoreFailedRegistration)
{
bool success = true;
StringBuilder failedKeys = new();
if (!RegisterHotkey(
failedKeys,
Settings.Default.HotKey,
handler))
{
success = false;
}
if (!success)
{
if (!ignoreFailedRegistration)
{
success = HandleFailedHotkeyRegistration(failedKeys.ToString());
}
}
return success || ignoreFailedRegistration;
}
/// <summary>
/// Displays a dialog for the user to choose how to handle hotkey registration failures:
/// retry (allowing to shut down the conflicting application before),
/// ignore (not registering the conflicting hotkey and resetting the respective config to "None", i.e. not trying to register it again on next startup)
/// abort (do nothing about it).
/// </summary>
/// <param name="failedKeys">comma separated list of the hotkeys that could not be registered, for display in dialog text.</param>
/// <returns>bool success.</returns>
private static bool HandleFailedHotkeyRegistration(string failedKeys)
{
bool success = false;
string warningTitle = Translator.GetText("Warning");
string message = Translator.GetText("Could not register the hot key.") + failedKeys;
DialogResult dr = MessageBox.Show(message, warningTitle, MessageBoxButtons.AbortRetryIgnore, MessageBoxIcon.Exclamation);
if (dr == DialogResult.Retry)
{
UnregisterHotKey();
success = RegisterHotkeys(false); // TODO: This may end up in endless recursion, better use a loop at caller
}
else if (dr == DialogResult.Ignore)
{
UnregisterHotKey();
success = RegisterHotkeys(true);
}
return success;
}
#endif
}
}

View file

@ -415,7 +415,7 @@ namespace SystemTrayMenu.UserInterface
return true;
}
internal void AddItemsToMenu(List<RowData> data, MenuDataDirectoryState? state, bool startIconLoading)
internal void AddItemsToMenu(List<RowData> data, MenuDataDirectoryState? state)
{
int foldersCount = 0;
int filesCount = 0;

View file

@ -7,66 +7,45 @@ namespace SystemTrayMenu.Utilities
using System;
using System.Diagnostics;
using System.Linq;
using SystemTrayMenu.Helpers;
using SystemTrayMenu.Business;
internal static class SingleAppInstance
{
private const string IpcServiceName = nameof(SingleAppInstance);
private const string IpcWakeupCmd = "wakeup";
private const string IpcWakeupResponseOK = "OK";
private static IpcPipe? ipcPipe;
internal static event Action? Wakeup;
internal static bool Initialize()
{
bool success = true;
try
{
foreach (Process p in Process.GetProcessesByName(
Process.GetCurrentProcess().ProcessName).
foreach (Process p in
Process.GetProcessesByName(Process.GetCurrentProcess().ProcessName).
Where(s => s.Id != Environment.ProcessId))
{
if (Properties.Settings.Default.SendHotkeyInsteadKillOtherInstances)
try
{
string hotKeyString = Properties.Settings.Default.HotKey;
var (modifiers, key) = GlobalHotkeys.ModifiersAndKeyFromInvariantString(hotKeyString);
try
if (Properties.Settings.Default.SendHotkeyInsteadKillOtherInstances)
{
#if TODO // HOTKEY - Maybe replace with sockets or pipes?
// E.g. https://learn.microsoft.com/en-us/dotnet/standard/io/how-to-use-named-pipes-for-network-interprocess-communication
List<VirtualKeyCode> virtualKeyCodesModifiers = new();
foreach (string key in modifiers.ToString().ToUpperInvariant().Split(", "))
// Instead of using hotkeys we use IPC via pipes
string pipeName = IpcServiceName + "-" + p.Id.ToString();
ipcPipe = new(1, pipeName);
string response = ipcPipe.SendToServer(IpcWakeupCmd) ?? string.Empty;
if (!string.Equals(response, IpcWakeupResponseOK))
{
if (key == "NONE")
{
continue;
}
VirtualKeyCode virtualKeyCode = VirtualKeyCode.LWIN;
virtualKeyCode = key switch
{
"ALT" => VirtualKeyCode.MENU,
_ => (VirtualKeyCode)Enum.Parse(
typeof(VirtualKeyCode), key.ToUpperInvariant()),
};
virtualKeyCodesModifiers.Add(virtualKeyCode);
throw new Exception("Error at IPC pipe \"" + pipeName + "\": \"" + response + "\"");
}
VirtualKeyCode virtualKeyCodeHotkey = 0;
if (Enum.IsDefined(typeof(VirtualKeyCode), (int)key))
{
virtualKeyCodeHotkey = (VirtualKeyCode)(int)key;
}
new InputSimulator().Keyboard.ModifiedKeyStroke(virtualKeyCodesModifiers, virtualKeyCodeHotkey);
success = false;
#endif
ipcPipe.Dispose();
ipcPipe = null;
success = false; // This is "success" but it means we cannot start this instance -> false
}
catch (Exception ex)
{
Log.Warn($"Send hoktey {hotKeyString} to other instance failed", ex);
}
}
else
{
try
else
{
if (!p.CloseMainWindow())
{
@ -76,20 +55,42 @@ namespace SystemTrayMenu.Utilities
p.WaitForExit();
p.Close();
}
catch (Exception ex)
{
Log.Error("Run as single instance failed", ex);
success = false;
}
}
catch (Exception ex)
{
Log.Error("Run as single instance failed", ex);
success = false;
}
}
}
catch (Exception ex)
{
Log.Error("Run as single instance failed", ex);
success = false;
}
if (success)
{
// We are the only process running, so we are responsible for the IPC server
ipcPipe = new(1, IpcServiceName + "-" + Environment.ProcessId.ToString());
ipcPipe.StartServer((request) =>
{
if (string.Equals(request, IpcWakeupCmd))
{
Wakeup?.Invoke();
return IpcWakeupResponseOK;
}
return string.Empty;
});
}
return success;
}
internal static void Unload()
{
ipcPipe?.Dispose();
}
}
}