sources/Google.Solutions.IapDesktop.Application/Host/SingletonApplicationBase.cs (253 lines of code) (raw):
//
// Copyright 2020 Google LLC
//
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
//
using Google.Solutions.Common.Diagnostics;
using System;
using System.Diagnostics;
using System.IO;
using System.IO.Pipes;
using System.Runtime.InteropServices;
using System.Security.AccessControl;
using System.Security.Principal;
using System.Threading;
using System.Threading.Tasks;
#if DEBUG
using System.Windows.Forms;
#endif
#pragma warning disable CA1031 // catch Exception
#pragma warning disable VSTHRD002 // Avoid problematic synchronous waits
namespace Google.Solutions.IapDesktop.Application.Host
{
public abstract class SingletonApplicationBase
{
private const int E_PIPE_BUSY = unchecked((int)0x800700e7);
internal uint SessionId { get; }
public string Name { get; }
internal string MutexName { get; }
internal string PipeName { get; }
protected SingletonApplicationBase(string name)
{
this.Name = name;
var processId = (uint)Process.GetCurrentProcess().Id;
if (UnsafeNativeMethods.ProcessIdToSessionId(
processId,
out var sessionId))
{
this.SessionId = sessionId;
}
else
{
//
// Use a fake session ID that's high enough to be unlikely
// to ever conflict with real session IDs.
//
this.SessionId = 0xF0000000 | (uint)processId;
}
//
// NB. Mutex names are case-sensitive, but pipes aren't.
// Normalize casing to prevent a situation where a second
// instance can claim the mutex (because it uses a different
// casing) but then can't open another pipe server because
// the pipe name is taken (with a different casing).
//
// Named pipes are always global, and there's no equivalent
// for Local\. We therefore incorporate the session ID into
// the name.
//
var uniqueName = $"{this.Name.ToLower()}_{this.SessionId:X}_{Environment.UserName.ToLower()}";
this.MutexName = $"Local\\{uniqueName}";
this.PipeName = uniqueName;
}
protected abstract int HandleFirstInvocation(string[] args);
protected abstract int HandleSubsequentInvocation(string[] args);
protected abstract void HandleSubsequentInvocationException(Exception e);
public int Run(string[] args)
{
//
// Create a mutex to distinguish whether this is the first process
// or a subsequent process.
//
// The mutex is locked down so that it is only visible within the
// current session and can only be accessed by the current user.
//
var mutexSecurity = new MutexSecurity();
mutexSecurity.AddAccessRule(
new MutexAccessRule(
WindowsIdentity.GetCurrent().Owner,
MutexRights.Synchronize | MutexRights.Modify,
AccessControlType.Allow));
try
{
using (var mutex = new Mutex(
true, // Try to claim ownership.
this.MutexName,
out var ownsMutex,
mutexSecurity))
{
if (ownsMutex)
{
//
// Successfully took ownership of mutex, so this is the first process.
//
// Start named pipe server in background.
//
using (var cts = new CancellationTokenSource())
{
var serverTask = Task.Factory.StartNew(
async () => await RunNamedPipeServerAsync(cts.Token).ConfigureAwait(false),
TaskCreationOptions.LongRunning);
// Run main invocation in foreground and wait for it to finish.
var result = HandleFirstInvocation(args);
// Stop the server.
cts.Cancel();
serverTask.Wait();
return result;
}
}
else
{
//
// Failed to take ownership of mutex, so this is a subsequent process.
//
var returnCode = PostCommandToNamedPipeServer(args);
if (returnCode >= 0)
{
//
// Activation was successful.
//
return returnCode;
}
else
{
ApplicationTraceSource.Log.TraceError(
"Activating the first process failed with return code {0}",
returnCode);
//
// Ignore and start a new instance.
//
return HandleFirstInvocation(args);
}
}
}
}
catch (IOException e)
{
ApplicationTraceSource.Log.TraceError(
"Singleton: Failed to communicate with mutex owner ({0})",
e.Message);
//
// Ignore the existing instance and start a new instance.
//
return HandleFirstInvocation(args);
}
catch (UnauthorizedAccessException)
{
ApplicationTraceSource.Log.TraceError(
"Singleton: Failed to access mutex");
//
// Failed to access mutex. Most likely, that's because the Mutex
// has been created at a different integrity level (for ex, the first
// process was launched elevated, but this process is non-elevared).
//
// Ignore the existing instance and start a new instance.
//
return HandleFirstInvocation(args);
}
}
protected static void TrySetForegroundWindow(int processId)
{
// Try to pass focus to other instance. Note that the
// main instance's process cannot claim the focus because
// Windows does not allow that.
try
{
var mainProcess = Process.GetProcessById(processId);
var mainHwnd = mainProcess.MainWindowHandle;
UnsafeNativeMethods.SetForegroundWindow(mainHwnd);
if (UnsafeNativeMethods.IsIconic(mainHwnd))
{
UnsafeNativeMethods.ShowWindow(mainHwnd, UnsafeNativeMethods.SW_RESTORE);
}
}
catch (Exception e)
{
#if DEBUG
MessageBox.Show(
e.Message,
"Failed to pass focus to main instance",
MessageBoxButtons.OK,
MessageBoxIcon.Warning);
#else
_ = e;
#endif
// Nevermind.
}
}
private int PostCommandToNamedPipeServer(string[] args)
{
using (var pipe = new NamedPipeClientStream(
".",
this.PipeName,
PipeDirection.InOut))
{
pipe.Connect();
using (var reader = new BinaryReader(pipe))
using (var writer = new BinaryWriter(pipe))
{
writer.Write(args.Length);
for (var i = 0; i < args.Length; i++)
{
writer.Write(args[i]);
}
var returnCode = reader.ReadInt32();
var processIdOfMainInstance = reader.ReadInt32();
TrySetForegroundWindow(processIdOfMainInstance);
return returnCode;
}
}
}
private async Task RunNamedPipeServerAsync(CancellationToken token)
{
var pipeSecurity = new PipeSecurity();
pipeSecurity.AddAccessRule(
new PipeAccessRule(
WindowsIdentity.GetCurrent().Owner,
PipeAccessRights.FullControl,
AccessControlType.Allow));
while (true)
{
try
{
//
// Sequentially dispatch client connections.
//
using (var pipe = new NamedPipeServerStream(
this.PipeName,
PipeDirection.InOut,
1, // Translates to FILE_FLAG_FIRST_PIPE_INSTANCE
PipeTransmissionMode.Message,
PipeOptions.None,
0,
0,
pipeSecurity))
{
await pipe
.WaitForConnectionAsync(token)
.ConfigureAwait(false);
//
// The server expects:
// IN: <int32> number of arguments
// IN: <string> argument #n
// IN: (repeat...)
// OUT: return code
// OUT: process ID
//
try
{
var reader = new BinaryReader(pipe);
var writer = new BinaryWriter(pipe);
var argsCount = reader.ReadInt32();
var args = new string[argsCount];
for (var i = 0; i < argsCount; i++)
{
args[i] = reader.ReadString();
}
try
{
var result = HandleSubsequentInvocation(args);
writer.Write(result);
}
catch (TimeoutException)
{
//
// Notify other instance, but don't crash.
//
writer.Write(-2);
}
catch (Exception e)
{
//
// Something bad happened, crash.
//
HandleSubsequentInvocationException(e);
writer.Write(-1);
}
writer.Write(Process.GetCurrentProcess().Id);
}
catch (IOException)
{
//
// The client closed the pipe early, ignore.
//
}
finally
{
try
{
pipe.WaitForPipeDrain();
}
catch (IOException)
{
//
// The client closed the pipe early, ignore.
//
}
pipe.Disconnect();
}
}
}
catch (TaskCanceledException)
{
return;
}
catch (IOException e) when (e.HResult == E_PIPE_BUSY)
{
//
// Because we always disconnect the pipe, this shouldn't
// normally happen.
//
// Back off and retry.
//
ApplicationTraceSource.Log.TraceWarning(
"Pipe {0} is busy, retrying", this.PipeName);
await Task.Delay(500);
}
catch (IOException e)
{
HandleSubsequentInvocationException(e);
}
}
}
//---------------------------------------------------------------------
// P/Invoke definitions.
//---------------------------------------------------------------------
private static class UnsafeNativeMethods
{
[DllImport("user32.dll")]
public static extern bool SetForegroundWindow(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern bool IsIconic(IntPtr hWnd);
[DllImport("user32.dll")]
public static extern bool ShowWindow(IntPtr hWnd, int nCmdShow);
public const int SW_RESTORE = 9;
[DllImport("kernel32.dll")]
public static extern bool ProcessIdToSessionId(
uint dwProcessId,
out uint pSessionId);
}
}
}