sources/Google.Solutions.Platform/Dispatch/Win32ProcessFactory.cs (363 lines of code) (raw):
//
// Copyright 2023 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 Google.Solutions.Common.Interop;
using Google.Solutions.Common.Util;
using Google.Solutions.Platform.IO;
using Microsoft.Win32.SafeHandles;
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net;
using System.Runtime.InteropServices;
namespace Google.Solutions.Platform.Dispatch
{
/// <summary>
/// Factory for Win32 processes.
/// </summary>
public interface IWin32ProcessFactory
{
/// <summary>
/// Start a new process.
/// </summary>
/// <returns>Suspended process</returns>
IWin32Process CreateProcess(
string executable,
string? arguments);
/// <summary>
/// Start a new process and attach a pseudo-console.
/// </summary>
/// <returns>Suspended process</returns>
IWin32Process CreateProcessWithPseudoConsole(
string executable,
string? arguments,
PseudoTerminalSize pseudoConsoleSize);
/// <summary>
/// Start a new process as a different user.
/// <returns>Suspended process</returns>
IWin32Process CreateProcessAsUser(
string executable,
string? arguments,
LogonFlags flags,
NetworkCredential credential);
}
[Flags]
public enum LogonFlags : uint
{
WithProfile = 1, // LOGON_WITH_PROFILE
NetCredentialsOnly = 2 // LOGON_NETCREDENTIALS_ONLY
}
public class Win32ProcessFactory : IWin32ProcessFactory
{
private const int ExitCodeForFailedProcessCreation = 250;
private static string Quote(string s)
{
return $"\"{s}\"";
}
/// <summary>
/// Allow deriving classes to do something with the process
/// before the factory returns it.
/// </summary>
private protected virtual void OnProcessCreated(Win32Process process)
{ }
private void InvokeOnProcessCreated(Win32Process process)
{
try
{
OnProcessCreated(process);
}
catch (Exception e)
{
PlatformTraceSource.Log.TraceError(e);
process.Terminate(ExitCodeForFailedProcessCreation);
process.Dispose();
throw;
}
}
//---------------------------------------------------------------------
// IWin32ProcessFactory.
//---------------------------------------------------------------------
public IWin32Process CreateProcess(
string executable,
string? arguments)
{
executable.ExpectNotEmpty(nameof(executable));
using (PlatformTraceSource.Log.TraceMethod()
.WithParameters(executable, arguments))
{
var startupInfo = new NativeMethods.STARTUPINFO()
{
cb = Marshal.SizeOf<NativeMethods.STARTUPINFO>()
};
if (!NativeMethods.CreateProcess(
null,
$"{Quote(executable)} {arguments}",
IntPtr.Zero,
IntPtr.Zero,
false,
NativeMethods.CREATE_SUSPENDED,
IntPtr.Zero,
null,
ref startupInfo,
out var processInfo))
{
throw DispatchException.FromLastWin32Error(
$"Launching process for {executable} failed");
}
var process = new Win32Process(
new FileInfo(executable).Name,
processInfo.dwProcessId,
new SafeProcessHandle(processInfo.hProcess, true),
new SafeThreadHandle(processInfo.hThread, true));
InvokeOnProcessCreated(process);
return process;
}
}
public IWin32Process CreateProcessWithPseudoConsole(
string executable,
string? arguments,
PseudoTerminalSize pseudoConsoleSize)
{
using (PlatformTraceSource.Log.TraceMethod()
.WithParameters(executable, arguments))
{
//
// Create a STARTUPINFOEX as described in
// https://docs.microsoft.com/en-us/windows/console/creating-a-pseudoconsole-session
//
var size = IntPtr.Zero;
NativeMethods.InitializeProcThreadAttributeList(
IntPtr.Zero,
1,
0,
ref size);
if (size == IntPtr.Zero)
{
throw DispatchException.FromLastWin32Error(
"Calculating the number of bytes for the " +
"thread attribute list failed");
}
using (var attributeListHandle =
GlobalAllocSafeHandle.GlobalAlloc((uint)size.ToInt32()))
{
var startupInfo = new NativeMethods.STARTUPINFOEX();
startupInfo.StartupInfo.cb =
Marshal.SizeOf<NativeMethods.STARTUPINFOEX>();
startupInfo.lpAttributeList =
attributeListHandle.DangerousGetHandle();
if (!NativeMethods.InitializeProcThreadAttributeList(
startupInfo.lpAttributeList,
1,
0,
ref size))
{
throw DispatchException.FromLastWin32Error(
"Creating the thread attribute list failed");
}
var pseudoConsole = new Win32PseudoConsole(pseudoConsoleSize);
try
{
if (!NativeMethods.UpdateProcThreadAttribute(
startupInfo.lpAttributeList,
0,
(IntPtr)NativeMethods.PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE,
pseudoConsole.Handle.DangerousGetHandle(),
(IntPtr)IntPtr.Size,
IntPtr.Zero,
IntPtr.Zero))
{
throw DispatchException.FromLastWin32Error(
"Attaching the pseudo-console failed");
}
var processSecurityAttributes =
new NativeMethods.SECURITY_ATTRIBUTES
{
nLength = Marshal
.SizeOf<NativeMethods.SECURITY_ATTRIBUTES>()
};
var threadSecurityAttributes =
new NativeMethods.SECURITY_ATTRIBUTES
{
nLength = Marshal
.SizeOf<NativeMethods.SECURITY_ATTRIBUTES>()
};
if (!NativeMethods.CreateProcess(
null,
$"{Quote(executable)} {arguments}",
ref processSecurityAttributes,
ref threadSecurityAttributes,
false,
NativeMethods.CREATE_SUSPENDED | NativeMethods.EXTENDED_STARTUPINFO_PRESENT,
IntPtr.Zero,
null,
ref startupInfo,
out var processInfo))
{
throw DispatchException.FromLastWin32Error(
$"Launching process for {executable} failed");
}
var process = new Win32Process(
new FileInfo(executable).Name,
processInfo.dwProcessId,
new SafeProcessHandle(processInfo.hProcess, true),
new SafeThreadHandle(processInfo.hThread, true))
{
PseudoTerminal = pseudoConsole
};
process.Exited += (_, __) =>
{
//
// Close pseudo console stream to unblock readers.
//
_ = pseudoConsole.CloseAsync();
};
InvokeOnProcessCreated(process);
return process;
}
catch
{
pseudoConsole.Dispose();
throw;
}
finally
{
NativeMethods.DeleteProcThreadAttributeList(startupInfo.lpAttributeList);
}
}
}
}
public IWin32Process CreateProcessAsUser(
string executable,
string? arguments,
LogonFlags flags,
NetworkCredential credential)
{
executable.ExpectNotEmpty(nameof(executable));
credential.ExpectNotNull(nameof(credential));
using (PlatformTraceSource.Log.TraceMethod()
.WithParameters(executable, arguments, flags, credential.UserName))
{
Debug.Assert(
credential.UserName.Contains("@") ||
credential.Domain != null);
var startupInfo = new NativeMethods.STARTUPINFO()
{
cb = Marshal.SizeOf<NativeMethods.STARTUPINFO>()
};
//
// NB. CreateProcessWithLogonW does not accept the
// DOMAIN\user format.
//
if (credential.UserName.Contains('\\'))
{
var usernameParts = credential.UserName.Split('\\');
credential = new NetworkCredential(
usernameParts[1],
credential.SecurePassword,
usernameParts[0]);
}
if (!NativeMethods.CreateProcessWithLogonW(
credential.UserName,
credential.Domain,
credential.Password,
flags,
null,
$"{Quote(executable)} {arguments}",
NativeMethods.CREATE_SUSPENDED,
IntPtr.Zero,
null,
ref startupInfo,
out var processInfo))
{
throw DispatchException.FromLastWin32Error(
$"Launching process for {executable} failed");
}
var process = new Win32Process(
new FileInfo(executable).Name,
processInfo.dwProcessId,
new SafeProcessHandle(processInfo.hProcess, true),
new SafeThreadHandle(processInfo.hThread, true));
InvokeOnProcessCreated(process);
return process;
}
}
//---------------------------------------------------------------------
// P/Invoke.
//---------------------------------------------------------------------
private static class NativeMethods
{
internal const uint CREATE_SUSPENDED = 0x00000004;
internal const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
internal const uint STARTF_USESTDHANDLES = 0x00000100;
internal const uint PROC_THREAD_ATTRIBUTE_PSEUDOCONSOLE = 0x00020016;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct STARTUPINFO
{
public int cb;
public string lpReserved;
public string lpDesktop;
public string lpTitle;
public int dwX;
public int dwY;
public int dwXSize;
public int dwYSize;
public int dwXCountChars;
public int dwYCountChars;
public int dwFillAttribute;
public uint dwFlags;
public short wShowWindow;
public short cbReserved2;
public IntPtr lpReserved2;
public IntPtr hStdInput;
public IntPtr hStdOutput;
public IntPtr hStdError;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
internal struct STARTUPINFOEX
{
public STARTUPINFO StartupInfo;
public IntPtr lpAttributeList;
}
[StructLayout(LayoutKind.Sequential)]
internal struct SECURITY_ATTRIBUTES
{
public int nLength;
public IntPtr lpSecurityDescriptor;
public bool bInheritHandle;
}
[StructLayout(LayoutKind.Sequential)]
internal struct PROCESS_INFORMATION
{
public IntPtr hProcess;
public IntPtr hThread;
public uint dwProcessId;
public uint dwThreadId;
}
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool InitializeProcThreadAttributeList(
IntPtr lpAttributeList,
int dwAttributeCount,
int dwFlags,
ref IntPtr lpSize);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool DeleteProcThreadAttributeList(
IntPtr lpAttributeList);
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool UpdateProcThreadAttribute(
IntPtr lpAttributeList,
uint dwFlags,
IntPtr attribute,
IntPtr lpValue,
IntPtr cbSize,
IntPtr lpPreviousValue,
IntPtr lpReturnSize);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool CreateProcess(
string? lpApplicationName,
string lpCommandLine,
IntPtr lpProcessAttributes,
IntPtr lpThreadAttributes,
bool bInheritHandles,
uint dwCreationFlags,
IntPtr lpEnvironment,
string? lpCurrentDirectory,
[In] ref STARTUPINFO lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static extern bool CreateProcess(
string? lpApplicationName,
string lpCommandLine,
ref SECURITY_ATTRIBUTES lpProcessAttributes,
ref SECURITY_ATTRIBUTES lpThreadAttributes,
bool bInheritHandles,
uint dwCreationFlags,
IntPtr lpEnvironment,
string? lpCurrentDirectory,
[In] ref STARTUPINFOEX lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
[DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool CreateProcessWithLogonW(
string userName,
string? domain,
string password,
LogonFlags logonFlags,
string? applicationName,
string commandLine,
uint dwCreationFlags,
IntPtr environment,
string? currentDirectory,
[In] ref STARTUPINFO lpStartupInfo,
out PROCESS_INFORMATION lpProcessInformation);
}
}
}