diff --git a/MainForm.cs b/MainForm.cs index 3c773e4..dfca223 100644 --- a/MainForm.cs +++ b/MainForm.cs @@ -572,6 +572,23 @@ namespace TimerApp this.Activate(); } + public void ActivateFromExternal() + { + if (InvokeRequired) + { + BeginInvoke(new Action(ActivateFromExternal)); + return; + } + + this.Show(); + this.WindowState = FormWindowState.Normal; + this.ShowInTaskbar = true; + this.BringToFront(); + this.Activate(); + this.TopMost = true; + this.TopMost = false; + } + private void toolStripMenuItemExit_Click(object sender, EventArgs e) { _monitor.Stop(); diff --git a/Program.cs b/Program.cs index 8dc5d8b..67ceea0 100644 --- a/Program.cs +++ b/Program.cs @@ -8,11 +8,21 @@ static class Program [STAThread] static void Main() { - TaskbarIntegration.InitializeProcess(); - // To customize application configuration such as set high DPI settings or default font, - // see https://aka.ms/applicationconfiguration. - ApplicationConfiguration.Initialize(); - TaskbarIntegration.InitializeShortcuts(); - Application.Run(new MainForm()); + if (!SingleInstanceManager.TryAcquire(out var instance) || instance is null) + { + SingleInstanceManager.SignalExistingInstance(); + return; + } + + using (instance) + { + TaskbarIntegration.InitializeProcess(); + ApplicationConfiguration.Initialize(); + TaskbarIntegration.InitializeShortcuts(); + + var mainForm = new MainForm(); + instance.StartServer(mainForm.ActivateFromExternal); + Application.Run(mainForm); + } } } diff --git a/SingleInstanceManager.cs b/SingleInstanceManager.cs new file mode 100644 index 0000000..21115d7 --- /dev/null +++ b/SingleInstanceManager.cs @@ -0,0 +1,118 @@ +using System; +using System.IO; +using System.IO.Pipes; +using System.Threading; +using System.Threading.Tasks; + +namespace TimerApp +{ + internal sealed class SingleInstanceManager : IDisposable + { + private const string MutexName = "Local\\TimerApp.SingleInstance"; + private const string PipeName = "TimerApp.SingleInstancePipe"; + + private readonly Mutex _mutex; + private readonly CancellationTokenSource _cts = new(); + + private SingleInstanceManager(Mutex mutex) + { + _mutex = mutex; + } + + public static bool TryAcquire(out SingleInstanceManager? manager) + { + manager = null; + + try + { + var mutex = new Mutex(true, MutexName, out bool createdNew); + if (!createdNew) + { + mutex.Dispose(); + return false; + } + + manager = new SingleInstanceManager(mutex); + return true; + } + catch + { + return true; + } + } + + public static void SignalExistingInstance() + { + try + { + using var client = new NamedPipeClientStream(".", PipeName, PipeDirection.Out); + client.Connect(200); + using var writer = new StreamWriter(client) { AutoFlush = true }; + writer.WriteLine("show"); + } + catch + { + } + } + + public void StartServer(Action onShowRequested) + { + Task.Run(() => ServerLoop(onShowRequested, _cts.Token)); + } + + private static async Task ServerLoop(Action onShowRequested, CancellationToken cancellationToken) + { + while (!cancellationToken.IsCancellationRequested) + { + try + { + using var server = new NamedPipeServerStream( + PipeName, + PipeDirection.In, + 1, + PipeTransmissionMode.Byte, + PipeOptions.Asynchronous + ); + + await server.WaitForConnectionAsync(cancellationToken).ConfigureAwait(false); + + using var reader = new StreamReader(server); + string? line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false); + if (string.Equals(line, "show", StringComparison.OrdinalIgnoreCase)) + onShowRequested(); + } + catch (OperationCanceledException) + { + return; + } + catch + { + await Task.Delay(150, cancellationToken).ConfigureAwait(false); + } + } + } + + public void Dispose() + { + try + { + _cts.Cancel(); + } + catch + { + } + + try + { + _mutex.ReleaseMutex(); + } + catch + { + } + + _mutex.Dispose(); + _cts.Dispose(); + } + } +} +