mirror of
https://github.com/Hofknecht/SystemTrayMenu.git
synced 2024-06-02 18:44:56 +12:00
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:
parent
dda3d94e7b
commit
0e812d207b
161
Business/IpcPipe.cs
Normal file
161
Business/IpcPipe.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@
|
|||
// Copyright (c) PlaceholderCompany. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace SystemTrayMenu.Handler
|
||||
namespace SystemTrayMenu.Business
|
||||
{
|
||||
using System;
|
||||
using System.Windows.Input;
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -56,6 +56,7 @@ namespace SystemTrayMenu
|
|||
}
|
||||
finally
|
||||
{
|
||||
SingleAppInstance.Unload();
|
||||
Log.Close();
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
// Copyright (c) PlaceholderCompany. All rights reserved.
|
||||
// </copyright>
|
||||
|
||||
namespace SystemTrayMenu.Handler
|
||||
namespace SystemTrayMenu.Business
|
||||
{
|
||||
using System;
|
||||
using System.Windows.Threading;
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue