in src/InstrumentationEngine.Attach/CommandHandler/AttachCommandHandler.cs [58:291]
private static int Attach(int processId,
FileInfo[] configs,
LoggingFlags logLevel,
LoggingFlags logFileLevel,
FileSystemInfo? logFilePath)
{
#region Target Process
Process process;
try
{
process = Process.GetProcessById(processId);
}
catch (Exception ex)
{
WriteError(ex.Message);
return ExitCodeFailure;
}
#endregion
#region Configuration Sources
var sourceInfos = new List<ConfigurationSourceInfo>();
// Schema file embedded name is the same that the full type name that was generated from the schema file
string schemaResourceName = typeof(InstrumentationConfigurationSources).FullName + ".xsd";
foreach (var configFile in configs)
{
var sourceInfo = new ConfigurationSourceInfo()
{
ConfigSourceFilePath = configFile.FullName
};
// Attempt to resolve fullpath
string? configFileDirectory = Path.GetDirectoryName(sourceInfo.ConfigSourceFilePath);
if (null == configFileDirectory || !Directory.Exists(configFileDirectory))
{
WriteError(Invariant($"Could not find directory '{configFileDirectory}'."));
return ExitCodeFailure;
}
sourceInfo.ConfigSourceDirectory = configFileDirectory;
#region Parse Configuration Sources
// Read schema file
using (Stream? schemaStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(schemaResourceName))
using (XmlReader schemaReader = XmlReader.Create(schemaStream, new XmlReaderSettings() { XmlResolver = null, DtdProcessing = DtdProcessing.Prohibit }))
{
// Create reader settings that conducts validation
XmlReaderSettings readerSettings = new XmlReaderSettings() { XmlResolver = null, DtdProcessing = DtdProcessing.Prohibit };
readerSettings.Schemas.Add(string.Empty, schemaReader);
readerSettings.ValidationType = ValidationType.Schema;
// Read configuration source file
// XmlReader handles filepaths as URI and escapes widechars so we convert it to a stream first.
using (var stream = new StreamReader(sourceInfo.ConfigSourceFilePath))
using (XmlReader reader = XmlReader.Create(stream, readerSettings))
{
XmlSerializer serializer = new XmlSerializer(typeof(InstrumentationConfigurationSources));
try
{
sourceInfo.Sources = (InstrumentationConfigurationSources)serializer.Deserialize(reader);
}
// InvalidOperationException is thrown when XML does not conform to the schema
catch (InvalidOperationException ex)
{
// Log the location of the error
WriteError($"Error: {ex.Message}");
if (null != ex.InnerException)
{
// Log the detailed information of why the XML does not conform to the schema
WriteError(ex.InnerException.Message);
}
return ExitCodeFailure;
}
}
}
#endregion
sourceInfos.Add(sourceInfo);
}
#endregion
#region Engine RootPath
// This executable should be in the [Root]\Tools\Attach directory. Get the root directory
// and build back to the instrumentation engine file path.
// CONSIDER: Is there a better way to get the engine path instead of building the path
// relative to the current executable? For example, requiring the root path of the engine
// to be passed as a parameter (which increases the difficulty of using this executable).
string? rootDirectory = Environment.GetEnvironmentVariable("MicrosoftInstrumentationEngine_InstallationRoot");
if (string.IsNullOrEmpty(rootDirectory))
{
string? attachDirectory = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
if (null == attachDirectory || !Directory.Exists(attachDirectory))
{
WriteError(Invariant($"Directory '{attachDirectory}' does not exist."));
return ExitCodeFailure;
}
string? toolsDirectory = Path.GetDirectoryName(attachDirectory);
if (null == toolsDirectory || !Directory.Exists(toolsDirectory))
{
WriteError(Invariant($"Directory '{toolsDirectory}' does not exist."));
return ExitCodeFailure;
}
rootDirectory = Path.GetDirectoryName(toolsDirectory);
}
else
{
rootDirectory = Environment.ExpandEnvironmentVariables(rootDirectory);
}
if (null == rootDirectory || !Directory.Exists(rootDirectory))
{
WriteError(Invariant($"Directory '{rootDirectory}' does not exist."));
return ExitCodeFailure;
}
#endregion
#region Target Process Architecture
bool isTargetProcess32Bit = true;
if (RuntimeInformation.OSArchitecture == Architecture.X64)
{
isTargetProcess32Bit = false;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
try
{
// Returns true for 32-bit applications running on a 64-bit OS
if (!NativeMethods.IsWow64Process(process.Handle, out isTargetProcess32Bit))
{
Exception? ex = Marshal.GetExceptionForHR(Marshal.GetHRForLastWin32Error());
if (null != ex)
{
WriteError(Invariant($"Failed to check process bitness: {ex.Message}"));
}
else
{
WriteError(Invariant($"Failed to check process bitness."));
}
return ExitCodeFailure;
}
}
catch (Win32Exception ex)
{
WriteError(Invariant($"Failed to check process bitness (code: {ex.NativeErrorCode}): {ex.Message}"));
return ExitCodeFailure;
}
}
}
#endregion
#region Generate Engine Configuration
// Create CLRIE configuration object for XML serialization
var configuration = new InstrumentationEngineConfiguration();
configuration.InstrumentationEngine = GenerateEngineConfiguration(
logLevel,
logFileLevel,
logFilePath);
if (TryParseInstrumentationMethodConfiguration(
sourceInfos,
isTargetProcess32Bit ? ChipType.x86 : ChipType.x64,
out InstrumentationMethodsTypeAddInstrumentationMethod[]? methods) &&
methods != null)
{
configuration.InstrumentationMethods = methods;
}
else
{
WriteError("Unable to parse Instrumentation Methods from configuration sources.");
return ExitCodeFailure;
}
XmlSerializer engineConfigSerializer = new XmlSerializer(typeof(InstrumentationEngineConfiguration));
byte[] bytes;
using (var memStream = new MemoryStream())
{
engineConfigSerializer.Serialize(memStream, configuration);
bytes = memStream.ToArray();
}
#endregion
#region InstrumentationEngine Path
string enginePath;
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
enginePath = isTargetProcess32Bit ?
Path.Combine(rootDirectory, Instrumentation32FolderName, InstrumentationEngineX86WindowsName) :
Path.Combine(rootDirectory, Instrumentation64FolderName, InstrumentationEngineX64WindowsName);
}
else if (!isTargetProcess32Bit)
{
enginePath = Path.Combine(rootDirectory, Instrumentation64FolderName, InstrumentationEngineX64LinuxName);
}
else
{
WriteError("Only the 64 bit engine is supported on non-Windows platforms.");
return ExitCodeFailure;
}
if (!File.Exists(enginePath))
{
WriteError(Invariant($"Engine path '{enginePath}' does not exist."));
return ExitCodeFailure;
}
WriteMessage($"Engine path: {enginePath}");
#endregion
#region Profiler Attach
// CONSIDER: Should the engine be validated before attempting to attach it to the CLR?
// For example, could check the signature on the assembly (which wouldn't work on Linux).
DiagnosticsClient client = new DiagnosticsClient(processId);
try
{
client.AttachProfiler(TimeSpan.FromSeconds(10), InstrumentationEngineClsid, enginePath, bytes);
}
catch (ServerErrorException ex)
{
WriteError(Invariant($"Could not attach engine to process: '{ex.Message}'."));
return ExitCodeFailure;
}
#endregion
WriteMessage($"Successfully attached the engine.");
return ExitCodeSuccess;
}