From: Mike McLaughlin Date: Sat, 11 Feb 2023 02:19:30 +0000 (-0800) Subject: Add service manager and reflection based service discovery (#3448) X-Git-Tag: accepted/tizen/unified/riscv/20231226.055542~43^2~2^2~105 X-Git-Url: http://review.tizen.org/git/?a=commitdiff_plain;h=a6e568cc64ee056343a41842609607ea29a3bf2e;p=platform%2Fcore%2Fdotnet%2Fdiagnostics.git Add service manager and reflection based service discovery (#3448) * Add service manager and reflection based service discovery Add GetTargetId to native ITarget Load service and command assemblies from directory. Add LoadExtensions Add IRuntimeProvider interface that allows IRuntime implementations to be added to the IRuntimeService. Support multiple service instances for IRuntimeProvider. Added IServiceProvider.GetServices() extension method to get them. Added the ContextService.ContextServiceProvider to deal with the context service's special needs. Added the ServiceImportAttribute support. There are Utilities helper functions that process this attribute. Add IHost.OnTargetCreate event. Removed IHost.DestroyTarget and add ITarget.Destroy. Allows the context and other services to managed state when targets come, go and flush. Makes target destruction more explicit by the implementations. Use the ServiceImportAttribute in commands. Add "Optional" ServiceImportAttribute flag. Add IServiceProviderManager interface to abstract the ServiceProvider functions. Add as a service the PEModule wrapper around PEReader, ElfModule around ElfFile and MachOModule around MachOFile. Pass IServiceProvider to constructors instead of using ITarget.Services. This removes the recursion from the ImageMappingMemoryService and MetdataMappingMemoryService. Update extensibility doc Add starting runtime id to runtime providers Add native type/field interfaces/impls Add WriteLine() to CommandBase and console service extensions. Fix module Version property. Fix testasset xml generation and repackaging scripts Update symstore version Update test assets version * Add switch by id to `runtimes` command * Catch load exceptions * Change to loading extensions from the tool/host install directory * Code review feedback * Undo some of the code review feedback * Code review feedback Removed weird this parameter from IServiceContainer.DisposeServices(). Use RemoveServices() to prevent recursion. Removed IServiceContainer.GetCachedService extension method and call IServiceContainer.TryGetCachedService directly. * More Code review feedback * More code review feedback * More code review changes * Add provider registration support to service manager Instead of supporting multiple instances of the same service type, this change allows services to get "provider" factories registered up the Provider scope. This simplifies the service container implementation. Add EnumerateProviderFactories to IServiceManager. * Allow ServiceImportAttribute on method and constructor parameters to control the Optional metadata flag. * Code review feedback * CR feedback: Better PEModule, ELFModule and MachOModule service implementation The service container no longer exposes itself to services. * CR feedback: cleanup service manager's global/context container support * Refactor IServiceContainer to ServiceContainerFactory and ServiceContainer Remove interface and put implementations in DebugServices. * Code review feedback * Code review --- diff --git a/documentation/design-docs/dotnet-dump-extensibility.md b/documentation/design-docs/dotnet-dump-extensibility.md index 64bf62224..3648bea98 100644 --- a/documentation/design-docs/dotnet-dump-extensibility.md +++ b/documentation/design-docs/dotnet-dump-extensibility.md @@ -2,7 +2,7 @@ This document describes a mechanism to allow first and third party users to add custom commands and services to `dotnet-dump` and `SOS` on the supported debuggers. Such extensibility has been a frequent ask from companies like Criteo and some teams at Microsoft. The goal is to write the code for a command once and have it run under all the supported debuggers, including dotnet-dump. -Internally, the ability to host commands like the future `gcheapdiff` under dotnet-dump, lldb and cdb/windbg will be invaluable for the productivity of developers in the ecosystem. Implementing new commands and features in C# is far easier and more productive for the interested parties. Other people on .NET team and in the community are more likely to contribute improvements to our tools, similar to what Stephen did with `dumpasync`. Unlike the plugin situation, if they contribute directly to our repo then the improvements will automatically flow to all customers and provide broader value. +The ability to host commands like the future `gcheapdiff` under dotnet-dump, lldb, cdb/windbg and Visual Studio will be invaluable for the productivity of developers in the ecosystem. Implementing new commands and features in C# is far easier and more productive for the interested parties. Other people on .NET team and in the community are more likely to contribute improvements to our tools, similar to what Stephen did with `dumpasync`. Unlike the plugin situation, if they contribute directly to our repo then the improvements will automatically flow to all customers and provide broader value. This effort is part of the "unified extensiblity" models - where various teams are coming together to define a common abstraction across all debuggers and debugger like hosts (dotnet-dump). Services such as Azure Watson could use this infrastructure to write a commands akin to `!analyze` and other analysis tools using a subset of the DAC - provided as a service - to do some unhandled exception and stack trace triage. @@ -17,7 +17,7 @@ This effort is part of the "unified extensiblity" models - where various teams a - Visual Studio - Create various "target" types from the command line or a command from: - Windows minidumps - - Linux coredumps + - Linux and MacOS coredumps - Live process snapshots ## Customer Value @@ -26,7 +26,7 @@ This effort is part of the "unified extensiblity" models - where various teams a - Commands that CSS devs would find useful in Visual Studio that can't be done in VS any other way: - !GCHandles - Provides statistics about GCHandles in the process. - !ThreadPool - This command lists basic information about the ThreadPool, including the number of work requests in the queue, number of completion port threads, and number of timers. - - !SaveModule - This command allows you to take a image loaded in memory and write it to a file. This is especially useful if you are debugging a full memory dump, and saves a PE module to a file from a dump, and don't have the original DLLs or EXEs. + - !SaveModule or !SaveAllModules ([#3138](https://github.com/dotnet/diagnostics/issues/3138)) - This command allows you to take a image loaded in memory and write it to a file. This is especially useful if you are debugging a full memory dump, and saves a PE module to a file from a dump, and don't have the original DLLs or EXEs. - rest of list TBD. - Enables support for Azure Geneva diagnostics which is using the Native AOT corert based runtime. This infrastructure will allow the necessary set of SOS commands to be written and executed across the support platforms (Windows windbg and Linux lldb). @@ -47,16 +47,11 @@ This effort is part of the "unified extensiblity" models - where various teams a - [#1031](https://github.com/dotnet/diagnostics/issues/1031) "Ability to load extensions in dotnet-dump analyze". This refers to loading "sosex" and "mex" in dotnet-dump. This plan would make it easier to do this but does not actually include it. - [#194](https://github.com/dotnet/diagnostics/issues/194) "Implement `gcheapdiff` dotnet-dump analyze command". We haven't had a lot of feedback on whether this purposed command is useful. This issue did inspired the "multi-target" part of this plan i.e. the ability to load/analyze two dumps in dotnet-dump at the same time. -## Road Map - -1. Create a VS host/target package allowing SOS and the extension commands to be run from VS using the Concord API. -2. Add Linux, MacOS and Windows live snapshot targets. ClrMD already has support for them; just need to implement the target factory. - ## Design The design consists of abstractions for the debugger or code hosting this infrastructure, one or more targets that represent the dump or process being targeted, one or more .NET runtimes in the target process (both desktop framework and .NET Core runtimes are supported) and various services that are avaiiable for commands, other services and the infrastructure itself. -Each hosting environment will have varing set requirements for the services needed. Other than the basic set of target, memory, and module (TBD), services can be optional. For example, hosting the native SOS code requires a richer set of interfaces than ClrMD based commands. +Each hosting environment will have varing set requirements for the services needed. Other than the basic set of target, memory, thread and module, most services can be optional. For example, hosting the native SOS code requires a richer set of interfaces than ClrMD based commands. - ClrMD commands and possible analysis engine - Basic target info like architecture, etc. @@ -88,17 +83,22 @@ The threading model is single-threaded mainly because native debuggers like dbge - IHost - Global services - IConsoleService + - IConsoleFileLoggingService - ICommandService - ISymbolService + - IContextService - IDumpTargetFactory - - IProcessSnapshotTargetFactory + - IProcessAttachTargetFactory + - IDiagnositcLoggingService + - IServiceManager/IServiceContainer - ITarget - Per-target services - IRuntimeService - - IRuntime - - ClrInfo - - ClrRuntime - - Runtime Snapshot Parse instance + - IRuntimeProvider + - IRuntime + - ClrInfo + - ClrRuntime + - Runtime Snapshot Parse instance - IMemoryService - IModuleService - IModule @@ -109,7 +109,9 @@ The threading model is single-threaded mainly because native debuggers like dbge ## Hosts -The host is the debugger or program the command and the infrastructure runs on. The goal is to allow the same code for a command to run under different programs like the dotnet-dump REPL, lldb and Windows debuggers. Under Visual Studio the host will be a VS extension package. +The host is the debugger or program the command and the infrastructure runs on. The goal is to allow the same code for a command to run under different programs like the dotnet-dump REPL, lldb and Windows debuggers. Under Visual Studio the host will be a VS extension package. + +When the host starts, the service manager loads command and service extension assemblies from the DOTNET_DIAGNOSTIC_EXTENSIONS environment variable (assembly paths separated by ';') or from the $HOME/.dotnet/extensions directory on Linux or MacOS or %USERPROFILE%\.dotnet\extensions directory on Windows. #### IHost @@ -131,19 +133,27 @@ The "gcdump" target is a possible target that allows gcdump specific services an #### ITarget -This interface abstracts the data target and adds value with things like per-target services and the context related state like the current thread. +This interface abstracts the data target, contains process architecture and platform info and adds value with things like per-target services like IMemoryService, IModuleService, IThreadService, IRuntimeService, etc. See [ITarget.cs](../../src/Microsoft.Diagnostics.DebugServices/ITarget.cs) and [target.h](../../src/SOS/inc/target.h) for details. ## Services -Everything a command or another service needs are provided via a service. There are global, per-target and per-command invocation services. Services like the command, console and target service and target factories are global. Services like the thread, memory and module services are per-target. Services like the current ClrRuntime instance are per-command invocation because it could change when there are multiple runtimes in a process. +Everything a command or another service needs are provided via a service. There are global, target, module, thread, runtime and context services. Services like the command, console and target service and target factories are global. Services like the thread, memory and module services are per-target. Services like the current ClrRuntime instance are per-runtime because it could change when there are multiple runtimes in a process. + +For Windbg/cdb and lldb, these services are implemented on the dbgeng API via the DebuggerServices pinvoke wrapper and interfaces. + +For Visual Studio, the base memory, module, thread services will be implemented on the Concord API in VS package. The rest of the services are implemented by the common code in Microsoft.Diagnostics.DebugServices.Implementation. The hardest part of this work is loading/running the native SOS and DAC modules in a 64bit environment. + +Services can be registered to contain common code for commands like [ClrMDHelper](../../src/Microsoft.Diagnostics.ExtensionCommands/ClrMDHelper.cs) or host or target functionality like ISymbolService or IMemoryService. + +The [ServiceExport](../../src/Microsoft.Diagnostics.DebugServices/ServiceExportAttribute.cs) attribute is used to mark classes, class constructors and factory methods to be registered as services. The ServiceScope defines where in the service hierarchy (global, context, target, runtime, thread, or module) this instance is available to commands and other services. -For Windbg/cdb, these services will be implemented on the dbgeng API. +The [ServiceImport](../../src/Microsoft.Diagnostics.DebugServices/ServiceImportAttribute.cs) attribute is used to mark public or interal fields, properties and methods in commands and other services to receive a service instance. -For lldb, these services will be implemented on the lldb extension API via some new pinvoke wrappers. +The internal [ServiceManager](../../src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceManager.cs) loads extension assemblies, provides the dependency injection using reflection (via the above attributes) and manages the various service factories. It creates the [IServiceContainer](../../src/Microsoft.Diagnostics.DebugServices/IServiceContainer.cs) instances for the extension points globally, in targets, modules, threads and runtimes (i.e. the IRuntime.ServiceProvider property). The public [IServiceManager](../../src/Microsoft.Diagnostics.DebugServices/IServiceManager.cs) interface exposes the public methods of the manager. -For Visual Studio, these services will be implemented on the Concord API in VS package. The hardest part of this work is loading/running the native SOS and DAC modules in a 64bit environment. +The IServiceProvider/IServiceContainer implementation allows multiple instances of the same service type to be registered. They are queried by getting the IEnumerable of the service type (i.e. calling `IServiceProvider.GetService(typeof(IEnumerable)`). If the non-enumerable service type is queried and there are multiple instances, an exception is thrown. The IRuntimeService implementation uses this feature to enumerate all the IRuntimeProvider instances registered in the system. ### IDumpTargetFactory @@ -163,16 +173,24 @@ Abstracts the console output across all the platforms. See [IConsoleService.cs](../../src/Microsoft.Diagnostics.DebugServices/IConsoleService.cs). +### IConsoleFileLoggingService + +This service controls how the console service output is logged to a file. + +See [IConsoleFileLoggingService.cs](../../src/Microsoft.Diagnostics.DebugServices/IConsoleFileLoggingService.cs). + ### IMemoryService Abstracts the memory related functions. -There are two helper IMemoryService implementations PEImageMappingMemoryService and MetadataMappingMemoryService. They are used to wrap the base native debugger memory service implementation. +There are two helper IMemoryService implementations ImageMappingMemoryService and MetadataMappingMemoryService. They are used to wrap the base native debugger memory service implementation. -PEImageMappingMemoryService is used in dotnet-dump for Windows targets to mapping PE images like coreclr.dll into the memory address space the module's memory isn't present. It downloads and loads the actual module and performs the necessary fix ups. +ImageMappingMemoryService is used in dotnet-dump for Windows targets to mapping PE, ELF and MachO images like coreclr.dll, libcoreclr.so, etc. into the memory address space the module's memory isn't present. It downloads and loads the actual module and performs the necessary fix ups. MetadataMappingMemoryService is only used for core dumps when running under lldb to map the managed assemblies metadata into address space. This is needed because the way lldb returns zero's for invalid memory for dumps generated with createdump on older runtimes (< 5.0). +To prevent recursion in these mapping services, the target service container is cloned and the memory service being wrapped replaces the base target memory service. + The address sign extension plan for 32 bit processors (arm32/x86) is that address are masked on entry to the managed infrastructure from DAC or DBI callbacks or from the native SOS code in SOS.Hosting. If the native debugger that the infrastructure is hosted needs addresses to be signed extended like dbgeng, it will happen in the debugger services layer (IDebuggerService). See [IMemoryService.cs](../../src/Microsoft.Diagnostics.DebugServices/IMemoryService.cs). @@ -191,25 +209,25 @@ One issues that may need to be addressed is that some platforms (MacOS) have non See [IModuleService.cs](../../src/Microsoft.Diagnostics.DebugServices/IModuleService.cs) and [IModule.cs](../../src/Microsoft.Diagnostics.DebugServices/IModule.cs). -### ISymbolService +### ISymbolService and ISymbolFile This service provides the symbol store services like the functionality that the static APIs in SOS.NETCore's SymbolReader.cs does now. The SOS.NETCore assembly will be removed and replaced with this symbol service implementation. Instead of directly creating delegates to static functions in this assembly, there will be a symbol service wrapper that provides these functions to the native SOS. The current implementation of the symbol downloading support in SOS.NETCore uses sync over async calls which could cause problems in more async hosts (like VS) but it hasn't caused problems in dotnet-dump so far. To fix this there may be work in the Microsoft.SymbolStore (in the symstore repo) component to expose synchronous APIs. -See [ISymbolService.cs](../../src/Microsoft.Diagnostics.DebugServices/ISymbolService.cs) for more details. +See [ISymbolService.cs](../../src/Microsoft.Diagnostics.DebugServices/ISymbolService.cs) and [ISymbolFile.cs](../../src/Microsoft.Diagnostics.DebugServices/ISymbolFile.cs)) for more details. -### IRuntimeService/IRuntime +### IRuntimeService/IRuntimeProvider/IRuntime -This service provides the runtime instances in the target process. The IRuntime abstracts the runtime providing the ClrInfo and ClrRuntime instances from ClrMD and the Native AOT runtime snapshot parser instance in the future. +This service provides the runtime instances in the target process. The IRuntimeService gathers all the runtimes from the possibility multiple IRuntimeProvider's in the system. There is a IRuntimeProvider for the runtimes found with CLRMD and will be one for the Native AOT snapshot parser. The IRuntime abstracts the runtime providing the ClrInfo and ClrRuntime instances from CLRMD and snapshot parser instance for Native AOT. -See [IRuntimeService.cs](../../src/Microsoft.Diagnostics.DebugServices/IRuntimeService.cs), [IRuntime.cs](../../src/Microsoft.Diagnostics.DebugServices/IRuntime.cs) and [runtime.h](../../src/SOS/inc/runtime.h) for details. +See [IRuntimeService.cs](../../src/Microsoft.Diagnostics.DebugServices/IRuntimeService.cs), [IRuntimeProvider](../../src/Microsoft.Diagnostics.DebugServices/IRuntimeProvider.cs), [IRuntime.cs](../../src/Microsoft.Diagnostics.DebugServices/IRuntime.cs) and [runtime.h](../../src/SOS/inc/runtime.h) for details. -### SOSHost +### SOSHost/SOSLibrary -This service allows native SOS commands to be executed under hosts like dotnet-dump and VS. This should probably have an interface to abstract it (TBD). +This service allows native SOS commands to be executed under hosts like dotnet-dump and VS. It provides all the native SOS hosting pinvokes and interfaces to run the native SOS commands under these debuggers. SOSLibrary is the global portion and manages loading the native SOS module and SOSHost is the per-target portion that does the actual work. -Some of the native SOS's "per-target" globals will need to be queried from this infrastructure instead being set once on initialization. This includes things like the DAC module path, DBI module path, the temp directory path (since it contains the process id), etc. It will provide native versions of the IHost, ITarget and IRuntime interfaces to the native SOS to do this. +[SOSHost](../../src/SOS/SOS.Hosting/SOSHost.cs) and [SOSLibrary](../../src/SOS/SOS.Hosting/SOSLibrary.cs) for details. ### IHostServices @@ -225,11 +243,11 @@ This native interface is what the SOS.Extensions host uses to implement the abov ## Projects and Assemblies -### SOS.Extensions (new) +### SOS.Extensions This assembly implements the host, target and services for the native debuggers (dbgeng, lldb). It provides the IHostServices to the native "extensions" library which registers the IDebuggerService used by the service implementations. -### The "extensions" native library (new) +### The "extensions" native library This is the native code that interops with the managed SOS.Extensions to host the native debuggers. It sets up the managed runtime (.NET Core on Linux/MacOS or desktop on Windows) and calls the SOS.Extensions initialization entry point. It is linked into the lldbplugin on Linux/MacOS and into SOS.dll on Windows. @@ -237,25 +255,21 @@ This is the native code that interops with the managed SOS.Extensions to host th This contains the hosting support used by the dotnet-dump REPL and an eventual Visual Studio package to run native SOS commands without a native debugger like dbgeng or lldb. -### SOS.NETCore (going away) - -This currently contains the symbol download and portable PDB source/line number support for the native SOS code. It will be replaced by the symbol service and wrappers (see ISymbolService). - ### Microsoft.Diagnostics.DebugServices Contains definations and abstractions for the various services interfaces. -### Microsoft.Diagnostics.DebugServices.Implementation (new) +### Microsoft.Diagnostics.DebugServices.Implementation -Contains the common debug services implementations used by dotnet-dump and SOS.Extensions (dbgeng/lldb) hosts. +Contains the common debug services implementations used by hosts like dotnet-dump and SOS.Extensions (dbgeng/lldb) hosts. -### Microsoft.Diagnostics.ExtensionCommands (new) +### Microsoft.Diagnostics.ExtensionCommands -Contains the common commands shared with dotnet-dump and the SOS.Extensions hosts. +Contains the common commands shared with dotnet-dump, VS and the SOS.Extensions hosts. ### Microsoft.Diagnostics.Repl -The command and console service implemenations. +The command REPL and console service implementations. ### dotnet-dump @@ -271,3 +285,47 @@ Native SOS commands and code. On Windows, it provides the debugger services (IDebuggerServices) to SOS.Extensions and initializes the managed hosting layer via the "extensions" native library. +## How to write a command + +Writing a new SOS command is a lot easier now that they are written C# with easy access to various services and the CLRMD API. + +The first step is to decide whether you want the new command to be part of the existing set of "built-in" commands (part of the Microsoft.Diagnostics.ExtensionCommands assembly) or in your own command assembly that can be loaded when the host starts by the service manager (set the Hosts section on the details). + +Command and service assembly must be have a netstandard2.0 TargetFramework to run on .NET Framework hosts like VS. + +The next step is to create a public class that inherits from the CommandBase helper class like: + +```C# +namespace Microsoft.Diagnostics.ExtensionCommands +{ + [Command(Name = "clrmodules", Help = "Lists the managed modules in the process.")] + public class ClrModulesCommand : CommandBase + { + [ServiceImport(Optional = true)] + public ClrRuntime Runtime { get; set; } + + [ServiceImport] + public IModuleService ModuleService { get; set; } + + [Option(Name = "--name", Aliases = new string[] { "-n" }, Help = "RegEx filter on module name (path not included).")] + public string ModuleName { get; set; } + + [Option(Name = "--verbose", Aliases = new string[] { "-v" }, Help = "Displays detailed information about the modules.")] + public bool Verbose { get; set; } + + public override void Invoke() + { + } + } +} +``` + +The "Command" attribute on the class provides the command name and help. The "ServiceImport" attribute on the properties indicates what services are needed by the command. It can be marked as optional. The default is that the service is required. The Option attributes define the various command line option names, aliases and help. When the command is executed the service, argument and option properties are set and the "Invoke" function is called. + +## How to write a service + +TBD + +```C# +``` + diff --git a/eng/testassets/rebuild/TestAssets.Linux.arm64.3.1.nuspec b/eng/testassets/rebuild/TestAssets.Linux.arm64.3.1.nuspec index 67f1b7d1a..792ebb608 100644 --- a/eng/testassets/rebuild/TestAssets.Linux.arm64.3.1.nuspec +++ b/eng/testassets/rebuild/TestAssets.Linux.arm64.3.1.nuspec @@ -20,6 +20,6 @@ Copyright 2021 - + diff --git a/eng/testassets/rebuild/TestAssets.Linux.arm64.5.0.nuspec b/eng/testassets/rebuild/TestAssets.Linux.arm64.5.0.nuspec index 2bddf3ab8..a589284bb 100644 --- a/eng/testassets/rebuild/TestAssets.Linux.arm64.5.0.nuspec +++ b/eng/testassets/rebuild/TestAssets.Linux.arm64.5.0.nuspec @@ -20,7 +20,7 @@ Copyright 2021 - - + + diff --git a/eng/testassets/rebuild/TestAssets.Linux.arm64.6.0.nuspec b/eng/testassets/rebuild/TestAssets.Linux.arm64.6.0.nuspec index 14af24c53..67da884ec 100644 --- a/eng/testassets/rebuild/TestAssets.Linux.arm64.6.0.nuspec +++ b/eng/testassets/rebuild/TestAssets.Linux.arm64.6.0.nuspec @@ -20,7 +20,7 @@ Copyright 2021 - - + + diff --git a/eng/testassets/rebuild/TestAssets.Linux.x64.3.1.nuspec b/eng/testassets/rebuild/TestAssets.Linux.x64.3.1.nuspec index ed962c4a8..332938528 100644 --- a/eng/testassets/rebuild/TestAssets.Linux.x64.3.1.nuspec +++ b/eng/testassets/rebuild/TestAssets.Linux.x64.3.1.nuspec @@ -20,6 +20,6 @@ Copyright 2021 - + diff --git a/eng/testassets/rebuild/TestAssets.Linux.x64.5.0.nuspec b/eng/testassets/rebuild/TestAssets.Linux.x64.5.0.nuspec index 1fffb4b31..00e7b69af 100644 --- a/eng/testassets/rebuild/TestAssets.Linux.x64.5.0.nuspec +++ b/eng/testassets/rebuild/TestAssets.Linux.x64.5.0.nuspec @@ -20,7 +20,7 @@ Copyright 2021 - - + + diff --git a/eng/testassets/rebuild/TestAssets.Linux.x64.6.0.nuspec b/eng/testassets/rebuild/TestAssets.Linux.x64.6.0.nuspec index 1dc30e4b7..7df3711cb 100644 --- a/eng/testassets/rebuild/TestAssets.Linux.x64.6.0.nuspec +++ b/eng/testassets/rebuild/TestAssets.Linux.x64.6.0.nuspec @@ -20,7 +20,7 @@ Copyright 2021 - - + + diff --git a/eng/testassets/rebuild/TestAssets.Windows.x64.3.1.nuspec b/eng/testassets/rebuild/TestAssets.Windows.x64.3.1.nuspec index 8d98a46b8..e482b5d26 100644 --- a/eng/testassets/rebuild/TestAssets.Windows.x64.3.1.nuspec +++ b/eng/testassets/rebuild/TestAssets.Windows.x64.3.1.nuspec @@ -20,6 +20,6 @@ Copyright 2021 - + diff --git a/eng/testassets/rebuild/TestAssets.Windows.x64.5.0.nuspec b/eng/testassets/rebuild/TestAssets.Windows.x64.5.0.nuspec index 4c3957a9e..e5388d5f9 100644 --- a/eng/testassets/rebuild/TestAssets.Windows.x64.5.0.nuspec +++ b/eng/testassets/rebuild/TestAssets.Windows.x64.5.0.nuspec @@ -20,8 +20,8 @@ Copyright 2021 - - - + + + diff --git a/eng/testassets/rebuild/TestAssets.Windows.x64.6.0.nuspec b/eng/testassets/rebuild/TestAssets.Windows.x64.6.0.nuspec index bcd07ee1d..db21dde2a 100644 --- a/eng/testassets/rebuild/TestAssets.Windows.x64.6.0.nuspec +++ b/eng/testassets/rebuild/TestAssets.Windows.x64.6.0.nuspec @@ -20,7 +20,7 @@ Copyright 2021 - - + + diff --git a/eng/testassets/rebuild/TestAssets.Windows.x86.3.1.nuspec b/eng/testassets/rebuild/TestAssets.Windows.x86.3.1.nuspec index 2a8a45139..9cef5a47a 100644 --- a/eng/testassets/rebuild/TestAssets.Windows.x86.3.1.nuspec +++ b/eng/testassets/rebuild/TestAssets.Windows.x86.3.1.nuspec @@ -20,6 +20,6 @@ Copyright 2021 - + diff --git a/eng/testassets/rebuild/TestAssets.Windows.x86.5.0.nuspec b/eng/testassets/rebuild/TestAssets.Windows.x86.5.0.nuspec index 53c389472..4c7b5fb62 100644 --- a/eng/testassets/rebuild/TestAssets.Windows.x86.5.0.nuspec +++ b/eng/testassets/rebuild/TestAssets.Windows.x86.5.0.nuspec @@ -20,8 +20,8 @@ Copyright 2021 - - - + + + diff --git a/eng/testassets/rebuild/TestAssets.Windows.x86.6.0.nuspec b/eng/testassets/rebuild/TestAssets.Windows.x86.6.0.nuspec index e6b27c53d..b9533bf60 100644 --- a/eng/testassets/rebuild/TestAssets.Windows.x86.6.0.nuspec +++ b/eng/testassets/rebuild/TestAssets.Windows.x86.6.0.nuspec @@ -20,7 +20,7 @@ Copyright 2021 - - + + diff --git a/eng/testassets/rebuild/rewritexml.cmd b/eng/testassets/rebuild/rewritexml.cmd index 235aa325d..7ec50c2ea 100644 --- a/eng/testassets/rebuild/rewritexml.cmd +++ b/eng/testassets/rebuild/rewritexml.cmd @@ -1,5 +1,5 @@ setlocal -set ORIGINAL_VERSION=1.0.246501 +set ORIGINAL_VERSION=1.0.257801 call ..\writexml.cmd %USERPROFILE%\.nuget\packages\testassets.linux.arm64.3.1\%ORIGINAL_VERSION%\content\SymbolTestApp\SOS.StackAndOtherTests.Heap.portable.dmp %USERPROFILE%\.nuget\packages\testassets.linux.arm64.3.1\%ORIGINAL_VERSION%\content\SymbolTestApp call ..\writexml.cmd %USERPROFILE%\.nuget\packages\testassets.linux.arm64.5.0\%ORIGINAL_VERSION%\content\LineNums\SOS.LineNums.Heap.dmp %USERPROFILE%\.nuget\packages\testassets.linux.arm64.5.0\%ORIGINAL_VERSION%\content\LineNums call ..\writexml.cmd %USERPROFILE%\.nuget\packages\testassets.linux.arm64.5.0\%ORIGINAL_VERSION%\content\SymbolTestApp\SOS.StackAndOtherTests.Heap.portable.dmp %USERPROFILE%\.nuget\packages\testassets.linux.arm64.5.0\%ORIGINAL_VERSION%\content\SymbolTestApp diff --git a/eng/testassets/writexml.cmd b/eng/testassets/writexml.cmd index 217cfc933..03eaa8969 100644 --- a/eng/testassets/writexml.cmd +++ b/eng/testassets/writexml.cmd @@ -3,6 +3,6 @@ set _REPOROOT_=%~dp0..\.. set _DUMPPATH_=%1 set _XMLPATH_=%1.xml set _DEBUGGEEPATH_=%2 -set DOTNET_DIAGNOSTIC_EXTENSIONS=%_REPOROOT_%\artifacts\bin\Microsoft.Diagnostics.DebugServices.UnitTests\Debug\netcoreapp3.1\Microsoft.Diagnostics.DebugServices.UnitTests.dll -%_REPOROOT_%\.dotnet\dotnet.exe %_REPOROOT_%\artifacts\bin\dotnet-dump\Debug\netcoreapp3.1\publish\dotnet-dump.dll analyze %_DUMPPATH_% -c "setsymbolserver -directory %_DEBUGGEEPATH_%" -c "writetestdata %_XMLPATH_%" -c "exit" +set DOTNET_DIAGNOSTIC_EXTENSIONS=%_REPOROOT_%\artifacts\bin\Microsoft.Diagnostics.DebugServices.UnitTests\Debug\net6.0\Microsoft.Diagnostics.DebugServices.UnitTests.dll +%_REPOROOT_%\.dotnet\dotnet.exe --fx-version 6.0.8 %_REPOROOT_%\artifacts\bin\dotnet-dump\Debug\netcoreapp3.1\publish\dotnet-dump.dll analyze %_DUMPPATH_% -c "setsymbolserver -directory %_DEBUGGEEPATH_%" -c "writetestdata %_XMLPATH_%" -c "exit" diff --git a/eng/testassets/writexml.sh b/eng/testassets/writexml.sh index 24d673a57..88665fc6f 100755 --- a/eng/testassets/writexml.sh +++ b/eng/testassets/writexml.sh @@ -2,6 +2,6 @@ _REPOROOT_=../.. _DUMPPATH_=$1 _XMLPATH_=$1.xml _DEBUGGEEPATH_=$2 -export DOTNET_DIAGNOSTIC_EXTENSIONS=$_REPOROOT_/artifacts/bin/Microsoft.Diagnostics.DebugServices.UnitTests/Debug/netcoreapp3.1/Microsoft.Diagnostics.DebugServices.UnitTests.dll -$_REPOROOT_/.dotnet/dotnet $_REPOROOT_/artifacts/bin/dotnet-dump/Debug/netcoreapp3.1/publish/dotnet-dump.dll analyze $_DUMPPATH_ -c "setsymbolserver -directory $_DEBUGGEEPATH_" -c "writetestdata $_XMLPATH_" -c "exit" +export DOTNET_DIAGNOSTIC_EXTENSIONS=$_REPOROOT_/artifacts/bin/Microsoft.Diagnostics.DebugServices.UnitTests/Debug/net6.0/Microsoft.Diagnostics.DebugServices.UnitTests.dll +$_REPOROOT_/.dotnet/dotnet --fx-version 6.0.8 $_REPOROOT_/artifacts/bin/dotnet-dump/Debug/netcoreapp3.1/publish/dotnet-dump.dll analyze $_DUMPPATH_ -c "setsymbolserver -directory $_DEBUGGEEPATH_" -c "writetestdata $_XMLPATH_" -c "exit" diff --git a/eng/testassets/writexml_x86.cmd b/eng/testassets/writexml_x86.cmd index f8198b89d..63cf2dd35 100644 --- a/eng/testassets/writexml_x86.cmd +++ b/eng/testassets/writexml_x86.cmd @@ -3,6 +3,6 @@ set _REPOROOT_=%~dp0..\.. set _DUMPPATH_=%1 set _XMLPATH_=%1.xml set _DEBUGGEEPATH_=%2 -set DOTNET_DIAGNOSTIC_EXTENSIONS=%_REPOROOT_%\artifacts\bin\Microsoft.Diagnostics.DebugServices.UnitTests\Debug\netcoreapp3.1\Microsoft.Diagnostics.DebugServices.UnitTests.dll -%_REPOROOT_%\.dotnet\x86\dotnet.exe %_REPOROOT_%\artifacts\bin\dotnet-dump\Debug\netcoreapp3.1\publish\dotnet-dump.dll analyze %_DUMPPATH_% -c "setsymbolserver -directory %_DEBUGGEEPATH_%" -c "writetestdata %_XMLPATH_%" -c "exit" +set DOTNET_DIAGNOSTIC_EXTENSIONS=%_REPOROOT_%\artifacts\bin\Microsoft.Diagnostics.DebugServices.UnitTests\Debug\net6.0\Microsoft.Diagnostics.DebugServices.UnitTests.dll +%_REPOROOT_%\.dotnet\x86\dotnet.exe --fx-version 6.0.8 %_REPOROOT_%\artifacts\bin\dotnet-dump\Debug\netcoreapp3.1\publish\dotnet-dump.dll analyze %_DUMPPATH_% -c "setsymbolserver -directory %_DEBUGGEEPATH_%" -c "writetestdata %_XMLPATH_%" -c "exit" diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/CommandService.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/CommandService.cs index 7585f33b9..26449ca64 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/CommandService.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/CommandService.cs @@ -162,6 +162,12 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// public IEnumerable<(string name, string help, IEnumerable aliases)> Commands => _commandHandlers.Select((keypair) => (keypair.Value.Name, keypair.Value.Help, keypair.Value.Aliases)); + /// + /// Add the commands and aliases attributes found in the type. + /// + /// Command type to search + public void AddCommands(Type type) => AddCommands(type, factory: null); + /// /// Add the commands and aliases attributes found in the type. /// @@ -184,7 +190,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation { if (factory == null) { - factory = (services) => Utilities.InvokeConstructor(type, services, optional: true); + factory = (services) => Utilities.CreateInstance(type, services); } CreateCommand(baseType, commandAttribute, factory); } @@ -239,11 +245,6 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation option.AddAlias(alias); } } - else - { - // If not an option, add as just a settable properties - properties.Add((property, null)); - } } } @@ -373,11 +374,11 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation private void Invoke(MethodInfo methodInfo, InvocationContext context, Parser parser, IServiceProvider services) { object instance = _factory(services); - SetProperties(context, parser, services, instance); - Utilities.Invoke(methodInfo, instance, services, optional: true); + SetProperties(context, parser, instance); + Utilities.Invoke(methodInfo, instance, services); } - private void SetProperties(InvocationContext context, Parser parser, IServiceProvider services, object instance) + private void SetProperties(InvocationContext context, Parser parser, object instance) { ParseResult defaultParseResult = null; @@ -409,14 +410,6 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation } } } - else - { - Type propertyType = property.Property.PropertyType; - object service = services.GetService(propertyType); - if (service != null) { - value = service; - } - } property.Property.SetValue(instance, value); } @@ -510,8 +503,8 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation public LocalConsole(IServiceProvider services) { Services = services; - Out = new StandardStreamWriter((text) => ConsoleService.Write(text)); - Error = new StandardStreamWriter((text) => ConsoleService.WriteError(text)); + Out = new StandardStreamWriter(ConsoleService.Write); + Error = new StandardStreamWriter(ConsoleService.WriteError); } internal readonly IServiceProvider Services; diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/ContextService.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/ContextService.cs index a2836da09..36ebfca08 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/ContextService.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/ContextService.cs @@ -13,28 +13,23 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// public class ContextService : IContextService { - protected readonly IHost Host; + protected readonly IHost _host; + protected readonly ServiceContainer _serviceContainer; private ITarget _currentTarget; private IThread _currentThread; private IRuntime _currentRuntime; - public readonly ServiceProvider ServiceProvider; - public ContextService(IHost host) { - Host = host; + _host = host; + var parent = new ContextServiceProvider(this); + _serviceContainer = host.Services.GetService().CreateServiceContainer(ServiceScope.Context, parent); - ServiceProvider = new ServiceProvider(new Func[] { - // First check the current runtime for the service - () => GetCurrentRuntime()?.Services, - // If there is no target, then provide just the global services - () => GetCurrentTarget()?.Services ?? host.Services + // Clear the current context when a target is flushed or destroyed + host.OnTargetCreate.Register((target) => { + target.OnFlushEvent.Register(() => ClearCurrentTarget(target)); + target.OnDestroyEvent.Register(() => ClearCurrentTarget(target)); }); - - // These services depend on no caching - ServiceProvider.AddServiceFactoryWithNoCaching(GetCurrentTarget); - ServiceProvider.AddServiceFactoryWithNoCaching(GetCurrentThread); - ServiceProvider.AddServiceFactoryWithNoCaching(GetCurrentRuntime); } #region IContextService @@ -43,7 +38,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// Current context service provider. Contains the current ITarget, IThread /// and IRuntime instances along with all per target and global services. /// - public IServiceProvider Services => ServiceProvider; + public IServiceProvider Services => _serviceContainer; /// /// Fires anytime the current context changes. @@ -57,7 +52,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// invalid target id public void SetCurrentTarget(int targetId) { - ITarget target = Host.EnumerateTargets().FirstOrDefault((target) => target.Id == targetId); + ITarget target = _host.EnumerateTargets().SingleOrDefault((target) => target.Id == targetId); if (target is null) { throw new DiagnosticsException($"Invalid target id {targetId}"); } @@ -88,7 +83,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// invalid runtime id public void SetCurrentRuntime(int runtimeId) { - IRuntime runtime = RuntimeService?.EnumerateRuntimes().FirstOrDefault((runtime) => runtime.Id == runtimeId); + IRuntime runtime = RuntimeService?.EnumerateRuntimes().SingleOrDefault((runtime) => runtime.Id == runtimeId); if (runtime is null) { throw new DiagnosticsException($"Invalid runtime id {runtimeId}"); } @@ -105,20 +100,32 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// /// Returns the current target. /// - public ITarget GetCurrentTarget() => _currentTarget ??= Host.EnumerateTargets().FirstOrDefault(); + protected virtual ITarget GetCurrentTarget() => _currentTarget ??= _host.EnumerateTargets().FirstOrDefault(); + + /// + /// Clears the context service state if the target is current + /// + /// + private void ClearCurrentTarget(ITarget target) + { + if (IsTargetEqual(target, _currentTarget)) + { + SetCurrentTarget(null); + } + } /// - /// Allows hosts to set the initial current target + /// Allows hosts to set the current target. Fires the context change event if the current target has changed. /// /// - public void SetCurrentTarget(ITarget target) + public virtual void SetCurrentTarget(ITarget target) { if (!IsTargetEqual(target, _currentTarget)) { _currentTarget = target; _currentThread = null; _currentRuntime = null; - ServiceProvider.FlushServices(); + _serviceContainer.DisposeServices(); OnContextChange.Fire(); } } @@ -126,10 +133,10 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// /// Returns the current thread. /// - public virtual IThread GetCurrentThread() => _currentThread ??= ThreadService?.EnumerateThreads().FirstOrDefault(); + protected virtual IThread GetCurrentThread() => _currentThread ??= ThreadService?.EnumerateThreads().FirstOrDefault(); /// - /// Allows hosts to set the initial current thread + /// Allows hosts to set the current thread. Fires the context change event if the current thread has changed. /// /// public virtual void SetCurrentThread(IThread thread) @@ -137,7 +144,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation if (!IsThreadEqual(thread, _currentThread)) { _currentThread = thread; - ServiceProvider.FlushServices(); + _serviceContainer.DisposeServices(); OnContextChange.Fire(); } } @@ -145,7 +152,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// /// Find the current runtime. /// - public IRuntime GetCurrentRuntime() + protected virtual IRuntime GetCurrentRuntime() { if (_currentRuntime is null) { @@ -184,14 +191,14 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation } /// - /// Allows hosts to set the initial current runtime + /// Allows hosts to set the current runtime. Fires the context change event if the current thread has changed. /// - public void SetCurrentRuntime(IRuntime runtime) + public virtual void SetCurrentRuntime(IRuntime runtime) { if (!IsRuntimeEqual(runtime, _currentRuntime)) { _currentRuntime = runtime; - ServiceProvider.FlushServices(); + _serviceContainer.DisposeServices(); OnContextChange.Fire(); } } @@ -223,5 +230,77 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation protected IThreadService ThreadService => GetCurrentTarget()?.Services.GetService(); protected IRuntimeService RuntimeService => GetCurrentTarget()?.Services.GetService(); + + /// + /// Special context service parent forwarding wrapper + /// + private class ContextServiceProvider : IServiceProvider + { + private readonly ContextService _contextService; + + /// + /// Create a special context service provider parent that forwards to the current runtime, target or host + /// + public ContextServiceProvider(ContextService contextService) + { + _contextService = contextService; + } + + /// + /// Returns the instance of the service or returns null if service doesn't exist + /// + /// service type + /// service instance or null + public object GetService(Type type) + { + if (type == typeof(IRuntime)) + { + return _contextService.GetCurrentRuntime(); + } + else if (type == typeof(IThread)) + { + return _contextService.GetCurrentThread(); + } + else if (type == typeof(ITarget)) + { + return _contextService.GetCurrentTarget(); + } + // Check the current runtime (if exists) for the service. + IRuntime currentRuntime = _contextService.GetCurrentRuntime(); + if (currentRuntime is not null) + { + // This will chain to the target then the global services if not found in the current runtime + object service = currentRuntime.Services.GetService(type); + if (service is not null) + { + return service; + } + } + // Check the current thread (if exists) for the service. + IThread currentThread = _contextService.GetCurrentThread(); + if (currentThread is not null) + { + // This will chain to the target then the global services if not found in the current thread + object service = currentThread.Services.GetService(type); + if (service is not null) + { + return service; + } + } + // Check the current target (if exists) for the service. + ITarget currentTarget = _contextService.GetCurrentTarget(); + if (currentTarget is not null) + { + // This will chain to the global services if not found in the current target + object service = currentTarget.Services.GetService(type); + if (service is not null) + { + return service; + } + } + // Check with the global host services. + return _contextService._host.Services.GetService(type); + } + } } } diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/DataReader.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/DataReader.cs index 27fe81310..f7b998e39 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/DataReader.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/DataReader.cs @@ -16,13 +16,20 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// /// ClrMD runtime service implementation /// - internal class DataReader : IDataReader + [ServiceExport(Type = typeof(IDataReader), Scope = ServiceScope.Target)] + public class DataReader : IDataReader { private readonly ITarget _target; private IEnumerable _modules; - private IModuleService _moduleService; - private IThreadService _threadService; - private IMemoryService _memoryService; + + [ServiceImport] + private IModuleService ModuleService { get; set; } + + [ServiceImport] + private IMemoryService MemoryService { get; set; } + + [ServiceImport] + private IThreadService ThreadService { get; set; } public DataReader(ITarget target) { @@ -106,12 +113,6 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation #endregion - private IModuleService ModuleService => _moduleService ??= _target.Services.GetService(); - - private IMemoryService MemoryService => _memoryService ??= _target.Services.GetService(); - - private IThreadService ThreadService => _threadService ??= _target.Services.GetService(); - private class DataReaderModule : ModuleInfo { private readonly IModule _module; diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/ElfModule.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/ElfModule.cs index 584f26cc3..07bf824a2 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/ElfModule.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/ElfModule.cs @@ -2,50 +2,61 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.FileFormats; using Microsoft.FileFormats.ELF; using System; -using System.Diagnostics; -using System.IO; +using System.Runtime.InteropServices; namespace Microsoft.Diagnostics.DebugServices.Implementation { /// - /// Disposable ELFFile wrapper around the module file. + /// ELFModule service that provides downloaded module ELFFile wrapper. /// - public class ELFModule : ELFFile + public class ELFModule : IDisposable { + private readonly IModule _module; + private readonly ISymbolService _symbolService; + private readonly IDisposable _onChangeEvent; + private ELFFile _elfFile; + /// - /// Opens and returns an ELFFile instance from the local file path + /// Creates a ELFModule service instance of the downloaded or local (if exists) module file. /// - /// ELF file to open - /// ELFFile instance or null - public static ELFModule OpenFile(string filePath) + [ServiceExport(Scope = ServiceScope.Module)] + public static ELFModule CreateELFModule(IModule module, ISymbolService symbolService) { - Stream stream = Utilities.TryOpenFile(filePath); - if (stream is not null) + if (module.Target.OperatingSystem == OSPlatform.Linux) { - try - { - ELFModule elfModule = new(stream); - if (!elfModule.IsValid()) - { - Trace.TraceError($"OpenELFFile: not a valid file"); - return null; - } - return elfModule; - } - catch (Exception ex) when (ex is InvalidVirtualAddressException || ex is BadInputFormatException || ex is IOException) + if (!module.BuildId.IsDefaultOrEmpty) { - Trace.TraceError($"OpenELFFile: exception {ex.Message}"); + return new ELFModule(module, symbolService); } } return null; } - public ELFModule(Stream stream) : - base(new StreamAddressSpace(stream), position: 0, isDataSourceVirtualAddressSpace: false) + private ELFModule(IModule module, ISymbolService symbolService) + { + _module = module; + _symbolService = symbolService; + _onChangeEvent = symbolService.OnChangeEvent.Register(() => { + _elfFile?.Dispose(); + _elfFile = null; + }); + } + + public ELFFile GetELFFile() + { + if (_elfFile == null) + { + _elfFile = Utilities.OpenELFFile(_symbolService.DownloadModuleFile(_module)); + } + return _elfFile; + } + + public void Dispose() { + _elfFile?.Dispose(); + _onChangeEvent.Dispose(); } } -} \ No newline at end of file +} diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/ImageMappingMemoryService.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/ImageMappingMemoryService.cs index 1ceffe604..5fcf99e6b 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/ImageMappingMemoryService.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/ImageMappingMemoryService.cs @@ -15,30 +15,54 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// /// Memory service wrapper that maps and fixes up PE module on read memory errors. /// - internal class ImageMappingMemoryService : IMemoryService + public class ImageMappingMemoryService : IMemoryService, IDisposable { + private readonly ServiceContainer _serviceContainer; private readonly IMemoryService _memoryService; private readonly IModuleService _moduleService; private readonly MemoryCache _memoryCache; + private readonly IDisposable _onChangeEvent; private readonly HashSet _recursionProtection; /// - /// The PE and ELF image mapping memory service. This service assumes that the managed PE - /// assemblies are in the module service's list. This is true for dbgeng, dotnet-dump but not - /// true for lldb (only native modules are provided). + /// The PE, ELF and MacOS image mapping memory service. For the dotnet-dump linux dump reader and + /// dbgeng the native module service providers managed and modules, but under lldb only native + /// modules are provided. The "managed" flag is for those later cases. /// - /// target instance + /// service container /// memory service to wrap - internal ImageMappingMemoryService(ITarget target, IMemoryService memoryService) + /// if true, map managed modules, else native + public ImageMappingMemoryService(ServiceContainer container, IMemoryService memoryService, bool managed) { + _serviceContainer = container; + container.AddService(memoryService); + _memoryService = memoryService; - _moduleService = target.Services.GetService(); + _moduleService = managed ? new ManagedImageMappingModuleService(container) : container.GetService(); _memoryCache = new MemoryCache(ReadMemoryFromModule); _recursionProtection = new HashSet(); - target.OnFlushEvent.Register(_memoryCache.FlushCache); - target.DisposeOnDestroy(target.Services.GetService()?.OnChangeEvent.Register(_memoryCache.FlushCache)); + + ITarget target = container.GetService(); + target.OnFlushEvent.Register(Flush); + + ISymbolService symbolService = container.GetService(); + _onChangeEvent = symbolService?.OnChangeEvent.Register(Flush); } + public void Dispose() + { + Flush(); + _onChangeEvent?.Dispose(); + _serviceContainer.RemoveService(typeof(IMemoryService)); + _serviceContainer.DisposeServices(); + if (_memoryService is IDisposable disposable) + { + disposable.Dispose(); + } + } + + protected void Flush() => _memoryCache.FlushCache(); + #region IMemoryService /// @@ -95,18 +119,22 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation private byte[] ReadMemoryFromModule(ulong address, int bytesRequested) { Debug.Assert((address & ~_memoryService.SignExtensionMask()) == 0); - IModule module = _moduleService.GetModuleFromAddress(address); - if (module != null) + + // Default is to cache errors + byte[] data = Array.Empty(); + + // Recursion can happen in the case where the PE, ELF or MachO headers (in the module.Services.GetService<>() calls) + // used to get the timestamp/filesize or build id are not in the dump. + if (!_recursionProtection.Contains(address)) { - // Recursion can happen in the case where the PE, ELF or MachO headers (in the module.Services.GetService<>() calls) - // used to get the timestamp/filesize or build id are not in the dump. - if (!_recursionProtection.Contains(address)) + _recursionProtection.Add(address); + try { - _recursionProtection.Add(address); - try + IModule module = _moduleService.GetModuleFromAddress(address); + if (module != null) { // We found a module that contains the memory requested. Now find or download the PE image. - PEReader reader = module.Services.GetService(); + PEReader reader = module.Services.GetService()?.GetPEReader(); if (reader is not null) { int rva = (int)(address - module.ImageBase); @@ -137,8 +165,6 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation try { - byte[] data = null; - // Read the memory from the PE image found/downloaded above PEMemoryBlock block = reader.GetEntireImage(); if (rva < block.Length) @@ -154,8 +180,6 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation Trace.TraceError($"ReadMemoryFromModule: FAILED address {address:X16} rva {rva:X8} {module.FileName}"); } } - - return data; } catch (Exception ex) when (ex is BadImageFormatException || ex is InvalidOperationException || ex is IOException) { @@ -165,11 +189,11 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation else { // Find or download the ELF image, if one. - Reader virtualAddressReader = module.Services.GetService()?.VirtualAddressReader; + Reader virtualAddressReader = module.Services.GetService()?.GetELFFile()?.VirtualAddressReader; if (virtualAddressReader is null) { // Find or download the MachO image, if one. - virtualAddressReader = module.Services.GetService()?.VirtualAddressReader; + virtualAddressReader = module.Services.GetService()?.GetMachOFile()?.VirtualAddressReader; } if (virtualAddressReader is not null) { @@ -181,14 +205,13 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation #if TRACE_VERBOSE Trace.TraceInformation($"ReadMemoryFromModule: address {address:X16} rva {rva:X16} size {bytesRequested:X8} in ELF or MachO file {module.FileName}"); #endif - byte[] data = new byte[bytesRequested]; + data = new byte[bytesRequested]; uint read = virtualAddressReader.Read(rva, data, 0, (uint)bytesRequested); if (read == 0) { Trace.TraceError($"ReadMemoryFromModule: FAILED address {address:X16} rva {rva:X16} {module.FileName}"); - data = null; + data = Array.Empty(); } - return data; } catch (Exception ex) when (ex is BadInputFormatException || ex is InvalidVirtualAddressException) { @@ -197,17 +220,17 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation } } } - finally - { - _recursionProtection.Remove(address); - } } - else + finally { - Trace.TraceError($"ReadMemoryFromModule: recursion: address {address:X16} size {bytesRequested:X8} {module.FileName}"); + _recursionProtection.Remove(address); } } - return null; + else + { + throw new InvalidOperationException($"ReadMemoryFromModule: recursion: address {address:X16} size {bytesRequested:X8}"); + } + return data; } enum BaseRelocationType @@ -235,7 +258,9 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation // Read IMAGE_BASE_RELOCATION struct int virtualAddress = blob.ReadInt32(); int sizeOfBlock = blob.ReadInt32(); - + if (sizeOfBlock <= 0) { + break; + } // Each relocation block covers 4K if (dataVA >= virtualAddress && dataVA < (virtualAddress + 4096)) { diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/MachOModule.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/MachOModule.cs index 467f52c8f..2d121cb88 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/MachOModule.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/MachOModule.cs @@ -2,50 +2,61 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.FileFormats; using Microsoft.FileFormats.MachO; using System; -using System.Diagnostics; -using System.IO; +using System.Runtime.InteropServices; namespace Microsoft.Diagnostics.DebugServices.Implementation { /// - /// Disposable MachOFile wrapper around the module file. + /// MachOModule service that provides downloaded module MachOFile wrapper. /// - public class MachOModule : MachOFile + public class MachOModule : IDisposable { + private readonly IModule _module; + private readonly ISymbolService _symbolService; + private readonly IDisposable _onChangeEvent; + private MachOFile _machOFile; + /// - /// Opens and returns an MachOFile instance from the local file path + /// Creates a MachOModule service instance of the downloaded or local (if exists) module file. /// - /// MachO file to open - /// MachOFile instance or null - public static MachOModule OpenFile(string filePath) + [ServiceExport(Scope = ServiceScope.Module)] + public static MachOModule CreateMachOModule(ISymbolService symbolService, IModule module) { - Stream stream = Utilities.TryOpenFile(filePath); - if (stream is not null) + if (module.Target.OperatingSystem == OSPlatform.OSX) { - try - { - var machoModule = new MachOModule(stream); - if (!machoModule.IsValid()) - { - Trace.TraceError($"OpenMachOFile: not a valid file"); - return null; - } - return machoModule; - } - catch (Exception ex) when (ex is InvalidVirtualAddressException || ex is BadInputFormatException || ex is IOException) + if (!module.BuildId.IsDefaultOrEmpty) { - Trace.TraceError($"OpenMachOFile: exception {ex.Message}"); + return new MachOModule(module, symbolService); } } return null; } - public MachOModule(Stream stream) : - base(new StreamAddressSpace(stream), position: 0, dataSourceIsVirtualAddressSpace: false) + private MachOModule(IModule module, ISymbolService symbolService) + { + _module = module; + _symbolService = symbolService; + _onChangeEvent = symbolService.OnChangeEvent.Register(() => { + _machOFile?.Dispose(); + _machOFile = null; + }); + } + + public MachOFile GetMachOFile() + { + if (_machOFile == null) + { + _machOFile = Utilities.OpenMachOFile(_symbolService.DownloadModuleFile(_module)); + } + return _machOFile; + } + + public void Dispose() { + _machOFile?.Dispose(); + _onChangeEvent.Dispose(); } } } diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/ManagedImageMappingModuleService.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/ManagedImageMappingModuleService.cs new file mode 100644 index 000000000..f4edd3887 --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/ManagedImageMappingModuleService.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Runtime; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; + +namespace Microsoft.Diagnostics.DebugServices.Implementation +{ + /// + /// Module service implementation for managed image mapping + /// + public class ManagedImageMappingModuleService : ModuleService + { + private readonly IRuntimeService _runtimeService; + + public ManagedImageMappingModuleService(IServiceProvider services) + : base(services) + { + _runtimeService = services.GetService(); + } + + /// + /// Get/create the modules dictionary. + /// + protected override Dictionary GetModulesInner() + { + var modules = new Dictionary(); + int moduleIndex = 0; + + IEnumerable runtimes = _runtimeService.EnumerateRuntimes(); + if (runtimes.Any()) + { + foreach (IRuntime runtime in runtimes) + { + ClrRuntime clrRuntime = runtime.Services.GetService(); + if (clrRuntime is not null) + { + foreach (ClrModule clrModule in clrRuntime.EnumerateModules()) + { + ModuleFromAddress module = new(this, moduleIndex, clrModule.ImageBase, clrModule.Size, clrModule.Name); + try + { + modules.Add(module.ImageBase, module); + moduleIndex++; + } + catch (ArgumentException) + { + Trace.TraceError($"GetModulesInner(): duplicate module base '{module}' dup '{modules[module.ImageBase]}'"); + } + } + } + } + } + + return modules; + } + } +} diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/MetadataMappingMemoryService.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/MetadataMappingMemoryService.cs index 19b382ee8..57aa6adfe 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/MetadataMappingMemoryService.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/MetadataMappingMemoryService.cs @@ -20,26 +20,47 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// that older (less than 5.0) createdumps generate so it needs this special /// metadata mapping memory service. /// - public class MetadataMappingMemoryService : IMemoryService + public class MetadataMappingMemoryService : IMemoryService, IDisposable { - private readonly ITarget _target; + private readonly ServiceContainer _serviceContainer; private readonly IMemoryService _memoryService; + private readonly IRuntimeService _runtimeService; + private readonly ISymbolService _symbolService; + private readonly IDisposable _onChangeEvent; private bool _regionInitialized; private ImmutableArray _regions; - private IRuntimeService _runtimeService; - private ISymbolService _symbolService; /// /// Memory service constructor /// - /// target instance + /// service container /// memory service to wrap - public MetadataMappingMemoryService(ITarget target, IMemoryService memoryService) + public MetadataMappingMemoryService(ServiceContainer container, IMemoryService memoryService) { - _target = target; + _serviceContainer = container; + container.AddService(memoryService); + _memoryService = memoryService; + _runtimeService = container.GetService(); + _symbolService = container.GetService(); + + ITarget target = container.GetService(); target.OnFlushEvent.Register(Flush); - target.DisposeOnDestroy(SymbolService?.OnChangeEvent.Register(Flush)); + + ISymbolService symbolService = container.GetService(); + _onChangeEvent = symbolService?.OnChangeEvent.Register(Flush); + } + + public void Dispose() + { + Flush(); + _onChangeEvent?.Dispose(); + _serviceContainer.RemoveService(typeof(IMemoryService)); + _serviceContainer.DisposeServices(); + if (_memoryService is IDisposable disposable) + { + disposable.Dispose(); + } } /// @@ -103,16 +124,16 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation // Need to set this before enumerating the runtimes to prevent reentrancy _regionInitialized = true; - var runtimes = RuntimeService.EnumerateRuntimes(); + var runtimes = _runtimeService.EnumerateRuntimes(); if (runtimes.Any()) { foreach (IRuntime runtime in runtimes) { Trace.TraceInformation($"FindRegion: initializing regions for runtime #{runtime.Id}"); ClrRuntime clrRuntime = runtime.Services.GetService(); - if (clrRuntime != null) + if (clrRuntime is not null) { - Trace.TraceInformation($"FindRegion: initializing regions for CLR runtime #{runtime.Id}"); + Trace.TraceInformation($"FindRegion: initializing regions for ClrRuntime #{runtime.Id}"); _regions = clrRuntime.EnumerateModules() .Where((module) => module.MetadataAddress != 0 && module.IsPEFile && !module.IsDynamic) .Select((module) => new MetadataRegion(this, module)) @@ -167,7 +188,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation PEFile peFile = new(new StreamAddressSpace(stream), isVirtual); if (peFile.IsValid()) { - metadata = SymbolService.GetMetadata(module.Name, peFile.Timestamp, peFile.SizeOfImage); + metadata = _symbolService.GetMetadata(module.Name, peFile.Timestamp, peFile.SizeOfImage); } else { @@ -181,10 +202,6 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation return metadata; } - private IRuntimeService RuntimeService => _runtimeService ??= _target.Services.GetService(); - - private ISymbolService SymbolService => _symbolService ??= _target.Services.GetService(); - class MetadataRegion : IComparable { private readonly MetadataMappingMemoryService _memoryService; diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/Module.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/Module.cs index 20ba0b92b..7dae04c6b 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/Module.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/Module.cs @@ -5,14 +5,12 @@ using Microsoft.Diagnostics.Runtime; using Microsoft.FileFormats; using Microsoft.FileFormats.ELF; -using Microsoft.FileFormats.MachO; using Microsoft.FileFormats.PE; using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Diagnostics; using System.IO; -using System.Reflection.PortableExecutable; using System.Runtime.InteropServices; namespace Microsoft.Diagnostics.DebugServices.Implementation @@ -30,93 +28,51 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation IsManaged = 0x02, IsFileLayout = 0x04, IsLoadedLayout = 0x08, - InitializePEInfo = 0x10, - InitializeVersion = 0x20, - InitializeProductVersion = 0x40, - InitializeSymbolFileName = 0x80 + InitializeVersion = 0x10, + InitializeProductVersion = 0x20, + InitializeSymbolFileName = 0x40 } - private readonly IDisposable _onChangeEvent; private Flags _flags; private IEnumerable _pdbFileInfos; - protected ImmutableArray _buildId; - private PEFile _peFile; private string _symbolFileName; - public readonly ServiceProvider ServiceProvider; + protected ImmutableArray _buildId; + protected readonly ServiceContainer _serviceContainer; - public Module(ITarget target) + public Module(IServiceProvider services) { - ServiceProvider = new ServiceProvider(); - ServiceProvider.AddServiceFactoryWithNoCaching(() => GetPEInfo()); - ServiceProvider.AddService(this); - - ServiceProvider.AddServiceFactory(() => { - if (!IndexTimeStamp.HasValue || !IndexFileSize.HasValue) { - return null; - } - return Utilities.OpenPEReader(ModuleService.SymbolService.DownloadModuleFile(this)); - }); - - if (target.OperatingSystem == OSPlatform.Linux) - { - ServiceProvider.AddServiceFactory(() => { - if (BuildId.IsDefaultOrEmpty) { - return null; - } - return ELFModule.OpenFile(ModuleService.SymbolService.DownloadModuleFile(this)); - }); - ServiceProvider.AddServiceFactory(() => { - Stream stream = ModuleService.MemoryService.CreateMemoryStream(); - var elfFile = new ELFFile(new StreamAddressSpace(stream), ImageBase, true); - return elfFile.IsValid() ? elfFile : null; - }); - } - - if (target.OperatingSystem == OSPlatform.OSX) - { - ServiceProvider.AddServiceFactory(() => { - if (BuildId.IsDefaultOrEmpty) { - return null; - } - return MachOModule.OpenFile(ModuleService.SymbolService.DownloadModuleFile(this)); - }); - ServiceProvider.AddServiceFactory(() => { - Stream stream = ModuleService.MemoryService.CreateMemoryStream(); - var machoFile = new MachOFile(new StreamAddressSpace(stream), ImageBase, true); - return machoFile.IsValid() ? machoFile : null; - }); - } - - _onChangeEvent = target.Services.GetService()?.OnChangeEvent.Register(() => { - ServiceProvider.RemoveService(typeof(MachOModule)); - ServiceProvider.RemoveService(typeof(ELFModule)); - ServiceProvider.RemoveService(typeof(PEReader)); - }); - } + ServiceContainerFactory containerFactory = services.GetService().CreateServiceContainerFactory(ServiceScope.Module, services); + containerFactory.AddServiceFactory((services) => ModuleService.GetPEInfo(ImageBase, ImageSize, out _pdbFileInfos, ref _flags)); + _serviceContainer = containerFactory.Build(); + _serviceContainer.AddService(this); + _serviceContainer.AddService(this); + } - public void Dispose() - { - _onChangeEvent?.Dispose(); + public virtual void Dispose() + { + _serviceContainer.RemoveService(typeof(IModule)); + _serviceContainer.RemoveService(typeof(IExportSymbols)); + _serviceContainer.DisposeServices(); } #region IModule public ITarget Target => ModuleService.Target; - public IServiceProvider Services => ServiceProvider; + public IServiceProvider Services => _serviceContainer; - public abstract int ModuleIndex { get; } + public virtual int ModuleIndex { get; protected set; } - public abstract string FileName { get; } + public virtual string FileName { get; protected set; } - public abstract ulong ImageBase { get; } + public virtual ulong ImageBase { get; protected set; } - public abstract ulong ImageSize { get; } + public virtual ulong ImageSize { get; protected set; } - public abstract uint? IndexFileSize { get; } + public virtual uint? IndexFileSize { get; protected set; } - public abstract uint? IndexTimeStamp { get; } + public virtual uint? IndexTimeStamp { get; protected set; } public bool IsPEImage { @@ -127,11 +83,8 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation { return true; } - else - { - GetPEInfo(); - return (_flags & Flags.IsPEImage) != 0; - } + Services.GetService(); + return (_flags & Flags.IsPEImage) != 0; } } @@ -139,7 +92,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation { get { - GetPEInfo(); + Services.GetService(); return (_flags & Flags.IsManaged) != 0; } } @@ -148,7 +101,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation { get { - GetPEInfo(); + Services.GetService(); if ((_flags & Flags.IsFileLayout) != 0) { return true; @@ -188,7 +141,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation public IEnumerable GetPdbFileInfos() { - GetPEInfo(); + Services.GetService(); Debug.Assert(_pdbFileInfos is not null); return _pdbFileInfos; } @@ -201,7 +154,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation { try { - Stream stream = ModuleService.RawMemoryService.CreateMemoryStream(); + Stream stream = ModuleService.MemoryService.CreateMemoryStream(); var elfFile = new ELFFile(new StreamAddressSpace(stream), ImageBase, true); if (elfFile.IsValid()) { @@ -256,7 +209,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation { if (ImageSize > 0) { - ModuleInfo module = ModuleInfo.TryCreate(Target.Services.GetService(), ImageBase, FileName); + ModuleInfo module = ModuleInfo.TryCreate(Services.GetService(), ImageBase, FileName); if (module is not null) { address = module.GetExportSymbolAddress(name); @@ -279,8 +232,8 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation { Version version = null; - PEFile peFile = GetPEInfo(); - if (peFile != null) + PEFile peFile = Services.GetService(); + if (peFile is not null) { try { @@ -329,15 +282,14 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation return version; } - protected PEFile GetPEInfo() + protected string GetVersionStringInner() { - if (InitializeValue(Flags.InitializePEInfo)) + if (ModuleService.Target.OperatingSystem != OSPlatform.Windows && !IsPEImage) { - _peFile = ModuleService.GetPEInfo(ImageBase, ImageSize, out _pdbFileInfos, ref _flags); + return ModuleService.GetVersionString(this); } - return _peFile; + return null; } - protected bool InitializeValue(Flags flag) { if ((_flags & flag) == 0) diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleFromAddress.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleFromAddress.cs new file mode 100644 index 000000000..d33ce7b1e --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleFromAddress.cs @@ -0,0 +1,76 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.FileFormats.PE; +using System; +using System.Runtime.InteropServices; + +namespace Microsoft.Diagnostics.DebugServices.Implementation +{ + /// + /// Create a IModule instance from a base address. + /// + public class ModuleFromAddress : Module + { + private Version _version; + private string _versionString; + + public ModuleFromAddress(ModuleService moduleService, int moduleIndex, ulong imageBase, ulong imageSize, string imageName) + : base(moduleService.Services) + { + ModuleService = moduleService; + ModuleIndex = moduleIndex; + ImageBase = imageBase; + ImageSize = imageSize; + FileName = imageName; + } + + #region IModule + + public override uint? IndexTimeStamp + { + get + { + PEFile peFile = Services.GetService(); + return peFile?.Timestamp; + } + } + + public override uint? IndexFileSize + { + get + { + PEFile peFile = Services.GetService(); + return peFile?.SizeOfImage; + } + } + + public override Version GetVersionData() + { + if (InitializeValue(Module.Flags.InitializeVersion)) + { + _version = GetVersionInner(); + } + return _version; + } + + public override string GetVersionString() + { + if (InitializeValue(Module.Flags.InitializeProductVersion)) + { + _versionString = GetVersionStringInner(); + } + return _versionString; + } + + public override string LoadSymbols() + { + return ModuleService.SymbolService.DownloadSymbolFile(this); + } + + #endregion + + protected override ModuleService ModuleService { get; } + } +} diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleService.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleService.cs index bd437e85e..2eb6f850a 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleService.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleService.cs @@ -19,7 +19,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// /// Module service base implementation /// - public abstract class ModuleService : IModuleService + public abstract class ModuleService : IModuleService, IDisposable { [Flags] internal enum ELFProgramHeaderAttributes : uint @@ -34,8 +34,6 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation // MachO writable segment attribute const uint VmProtWrite = 0x02; - internal protected readonly ITarget Target; - internal protected IMemoryService RawMemoryService; private IMemoryService _memoryService; private ISymbolService _symbolService; private ReadVirtualCache _versionCache; @@ -45,26 +43,33 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation private static readonly byte[] s_versionString = Encoding.ASCII.GetBytes("@(#)Version "); private static readonly int s_versionLength = s_versionString.Length; - public ModuleService(ITarget target, IMemoryService rawMemoryService) + internal protected readonly IServiceProvider Services; + internal protected readonly ITarget Target; + + public ModuleService(IServiceProvider services) { - Debug.Assert(target != null); - Target = target; - RawMemoryService = rawMemoryService; + Services = services; + Target = services.GetService(); + Target.OnFlushEvent.Register(Flush); + } + + public void Dispose() => Flush(); - target.OnFlushEvent.Register(() => { - _versionCache?.Clear(); - if (_modules != null) + private void Flush() + { + _versionCache?.Clear(); + if (_modules is not null) + { + foreach (IModule module in _modules.Values) { - foreach (IModule module in _modules.Values) - { - if (module is IDisposable disposable) { - disposable.Dispose(); - } + if (module is IDisposable disposable) { + disposable.Dispose(); } } + _modules.Clear(); _modules = null; - _sortedByBaseAddress = null; - }); + } + _sortedByBaseAddress = null; } #region IModuleService @@ -116,7 +121,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// module or null IModule IModuleService.GetModuleFromAddress(ulong address) { - Debug.Assert((address & ~RawMemoryService.SignExtensionMask()) == 0); + Debug.Assert((address & ~MemoryService.SignExtensionMask()) == 0); IModule[] modules = GetSortedModules(); int min = 0, max = modules.Length - 1; @@ -127,7 +132,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation IModule module = modules[mid]; ulong start = module.ImageBase; - Debug.Assert((start & ~RawMemoryService.SignExtensionMask()) == 0); + Debug.Assert((start & ~MemoryService.SignExtensionMask()) == 0); ulong end = start + module.ImageSize; if (address >= start && address < end) { @@ -170,7 +175,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// private Dictionary GetModules() { - if (_modules == null) + if (_modules is null) { _modules = GetModulesInner(); } @@ -183,7 +188,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// private IModule[] GetSortedModules() { - if (_sortedByBaseAddress == null) + if (_sortedByBaseAddress is null) { _sortedByBaseAddress = GetModules().OrderBy((pair) => pair.Key).Select((pair) => pair.Value).ToArray(); } @@ -260,7 +265,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation flags = 0; try { - Stream stream = RawMemoryService.CreateMemoryStream(address, size); + Stream stream = MemoryService.CreateMemoryStream(address, size); PEFile peFile = new(new StreamAddressSpace(stream), isVirtual); if (peFile.IsValid()) { @@ -288,7 +293,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation // This code is called by the image mapping memory service so it needs to use the // original or raw memory service to prevent recursion so it can't use the ELFFile // or MachOFile instance that is available from the IModule.Services provider. - Stream stream = RawMemoryService.CreateMemoryStream(); + Stream stream = MemoryService.CreateMemoryStream(); byte[] buildId = null; try { @@ -321,7 +326,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// /// module to get version string /// version string or null - protected string GetVersionString(IModule module) + internal string GetVersionString(IModule module) { try { @@ -389,9 +394,8 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation { byte[] buffer = new byte[s_versionString.Length]; - if (_versionCache == null) - { - // We use the possibly mapped memory service to find the version string in case it isn't in the dump. + // We use the mapped memory service to find the version string in case it isn't in the dump. + if (_versionCache is null) { _versionCache = new ReadVirtualCache(MemoryService); } _versionCache.Clear(); @@ -456,9 +460,9 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation } } - internal protected IMemoryService MemoryService => _memoryService ??= Target.Services.GetService(); + internal protected IMemoryService MemoryService => _memoryService ??= Services.GetService(); - internal protected ISymbolService SymbolService => _symbolService ??= Target.Services.GetService(); + internal protected ISymbolService SymbolService => _symbolService ??= Services.GetService(); /// /// Search memory helper class diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleServiceFromDataReader.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleServiceFromDataReader.cs index 0ad0019c4..4b3e2dba4 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleServiceFromDataReader.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/ModuleServiceFromDataReader.cs @@ -29,7 +29,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation private string _versionString; public ModuleFromDataReader(ModuleServiceFromDataReader moduleService, int moduleIndex, ModuleInfo moduleInfo, ulong imageSize) - : base(moduleService.Target) + : base(moduleService.Services) { _moduleService = moduleService; _moduleInfo = moduleInfo; @@ -39,8 +39,6 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation #region IModule - public override int ModuleIndex { get; } - public override string FileName => _moduleInfo.FileName; public override ulong ImageBase => _moduleInfo.ImageBase; @@ -75,10 +73,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation } else { - if (_moduleService.Target.OperatingSystem != OSPlatform.Windows) - { - _version = GetVersionInner(); - } + _version = GetVersionInner(); } } return _version; @@ -88,10 +83,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation { if (InitializeValue(Module.Flags.InitializeProductVersion)) { - if (_moduleService.Target.OperatingSystem != OSPlatform.Windows && !IsPEImage) - { - _versionString = _moduleService.GetVersionString(this); - } + _versionString = GetVersionStringInner(); } return _versionString; } @@ -114,8 +106,8 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation private readonly IDataReader _dataReader; - public ModuleServiceFromDataReader(ITarget target, IMemoryService rawMemoryService, IDataReader dataReader) - : base(target, rawMemoryService) + public ModuleServiceFromDataReader(IServiceProvider services, IDataReader dataReader) + : base(services) { _dataReader = dataReader; } @@ -159,14 +151,13 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation try { modules.Add(moduleInfo.ImageBase, module); + moduleIndex++; } catch (ArgumentException) { Trace.TraceError($"GetModules(): duplicate module base '{module}' dup '{modules[moduleInfo.ImageBase]}'"); } } - - moduleIndex++; } return modules; } diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/PEModule.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/PEModule.cs new file mode 100644 index 000000000..449f37ddc --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/PEModule.cs @@ -0,0 +1,58 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Reflection.PortableExecutable; + +namespace Microsoft.Diagnostics.DebugServices.Implementation +{ + /// + /// PEModule service that provides downloaded module PEReader wrapper. + /// + public class PEModule : IDisposable + { + private readonly IModule _module; + private readonly ISymbolService _symbolService; + private readonly IDisposable _onChangeEvent; + private PEReader _reader; + + /// + /// Creates a PEModule service instance of the downloaded or local (if exists) module file. + /// + [ServiceExport(Scope = ServiceScope.Module)] + public static PEModule CreatePEModule(IModule module, ISymbolService symbolService) + { + if (module.IndexTimeStamp.HasValue && module.IndexFileSize.HasValue) + { + return new PEModule(module, symbolService); + } + return null; + } + + public PEReader GetPEReader() + { + if (_reader == null) + { + _reader = Utilities.OpenPEReader(_symbolService.DownloadModuleFile(_module)); + } + return _reader; + } + + private PEModule(IModule module, ISymbolService symbolService) + { + _module = module; + _symbolService = symbolService; + _onChangeEvent = symbolService.OnChangeEvent.Register(() => { + _reader?.Dispose(); + _reader = null; + }); + } + + public void Dispose() + { + _reader?.Dispose(); + _onChangeEvent.Dispose(); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs index eb0d608f8..148d3c1ab 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/Runtime.cs @@ -16,23 +16,24 @@ using System.Text; namespace Microsoft.Diagnostics.DebugServices.Implementation { /// - /// IRuntime instance implementation + /// ClrMD runtime instance implementation /// - public class Runtime : IRuntime + public class Runtime : IRuntime, IDisposable { private readonly ClrInfo _clrInfo; - private ISymbolService _symbolService; - private ClrRuntime _clrRuntime; + private readonly IDisposable _onFlushEvent; + private readonly ISymbolService _symbolService; private string _dacFilePath; private string _dbiFilePath; - public readonly ServiceProvider ServiceProvider; + protected readonly ServiceContainer _serviceContainer; - public Runtime(ITarget target, int id, ClrInfo clrInfo) + public Runtime(IServiceProvider services, int id, ClrInfo clrInfo) { - Target = target ?? throw new ArgumentNullException(nameof(target)); + Target = services.GetService() ?? throw new ArgumentNullException(); Id = id; _clrInfo = clrInfo ?? throw new ArgumentNullException(nameof(clrInfo)); + _symbolService = services.GetService(); RuntimeType = RuntimeType.Unknown; if (clrInfo.Flavor == ClrFlavor.Core) { @@ -41,24 +42,41 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation else if (clrInfo.Flavor == ClrFlavor.Desktop) { RuntimeType = RuntimeType.Desktop; } - RuntimeModule = target.Services.GetService().GetModuleFromBaseAddress(clrInfo.ModuleInfo.ImageBase); + RuntimeModule = services.GetService().GetModuleFromBaseAddress(clrInfo.ModuleInfo.ImageBase); - ServiceProvider = new ServiceProvider(); - ServiceProvider.AddService(clrInfo); - ServiceProvider.AddServiceFactoryWithNoCaching(() => CreateRuntime()); + ServiceContainerFactory containerFactory = services.GetService().CreateServiceContainerFactory(ServiceScope.Runtime, services); + containerFactory .AddServiceFactory((services) => CreateRuntime()); + _serviceContainer = containerFactory.Build(); + _serviceContainer.AddService(this); + _serviceContainer.AddService(clrInfo); - target.OnFlushEvent.Register(() => _clrRuntime?.FlushCachedData()); + _onFlushEvent = Target.OnFlushEvent.Register(Flush); Trace.TraceInformation($"Created runtime #{id} {clrInfo.Flavor} {clrInfo}"); } + void IDisposable.Dispose() + { + _serviceContainer.RemoveService(typeof(IRuntime)); + _serviceContainer.DisposeServices(); + _onFlushEvent.Dispose(); + } + + private void Flush() + { + if (_serviceContainer.TryGetCachedService(typeof(ClrRuntime), out object service)) + { + ((ClrRuntime)service).FlushCachedData(); + } + } + #region IRuntime public int Id { get; } public ITarget Target { get; } - public IServiceProvider Services => ServiceProvider; + public IServiceProvider Services => _serviceContainer; public RuntimeType RuntimeType { get; } @@ -87,38 +105,35 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation #endregion /// - /// Create ClrRuntime helper + /// Create ClrRuntime instance /// private ClrRuntime CreateRuntime() { - if (_clrRuntime is null) + string dacFilePath = GetDacFilePath(); + if (dacFilePath is not null) { - string dacFilePath = GetDacFilePath(); - if (dacFilePath is not null) + Trace.TraceInformation($"Creating ClrRuntime #{Id} {dacFilePath}"); + try { - Trace.TraceInformation($"Creating ClrRuntime #{Id} {dacFilePath}"); - try - { - // Ignore the DAC version mismatch that can happen because the clrmd ELF dump reader - // returns 0.0.0.0 for the runtime module that the DAC is matched against. - _clrRuntime = _clrInfo.CreateRuntime(dacFilePath, ignoreMismatch: true); - } - catch (Exception ex) when - (ex is DllNotFoundException || - ex is FileNotFoundException || - ex is InvalidOperationException || - ex is InvalidDataException || - ex is ClrDiagnosticsException) - { - Trace.TraceError("CreateRuntime FAILED: {0}", ex.ToString()); - } + // Ignore the DAC version mismatch that can happen because the clrmd ELF dump reader + // returns 0.0.0.0 for the runtime module that the DAC is matched against. + return _clrInfo.CreateRuntime(dacFilePath, ignoreMismatch: true); } - else + catch (Exception ex) when + (ex is DllNotFoundException || + ex is FileNotFoundException || + ex is InvalidOperationException || + ex is InvalidDataException || + ex is ClrDiagnosticsException) { - Trace.TraceError($"Could not find or download matching DAC for this runtime: {RuntimeModule.FileName}"); + Trace.TraceError("CreateRuntime FAILED: {0}", ex.ToString()); } } - return _clrRuntime; + else + { + Trace.TraceError($"Could not find or download matching DAC for this runtime: {RuntimeModule.FileName}"); + } + return null; } private string GetLibraryPath(DebugLibraryKind kind) @@ -173,7 +188,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation OSPlatform platform = Target.OperatingSystem; string filePath = null; - if (SymbolService.IsSymbolStoreEnabled) + if (_symbolService.IsSymbolStoreEnabled) { SymbolStoreKey key = null; @@ -234,7 +249,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation if (key is not null) { // Now download the DAC module from the symbol server - filePath = SymbolService.DownloadFile(key); + filePath = _symbolService.DownloadFile(key); } } else @@ -244,8 +259,6 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation return filePath; } - private ISymbolService SymbolService => _symbolService ??= Target.Services.GetService(); - public override bool Equals(object obj) { IRuntime runtime = (IRuntime)obj; @@ -258,10 +271,11 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation } private static readonly string[] s_runtimeTypeNames = { + "Unknown", "Desktop .NET Framework", ".NET Core", ".NET Core (single-file)", - "Unknown" + "Other" }; public override string ToString() @@ -269,21 +283,24 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation var sb = new StringBuilder(); string config = s_runtimeTypeNames[(int)RuntimeType]; string index = _clrInfo.BuildId.IsDefaultOrEmpty ? $"{_clrInfo.IndexTimeStamp:X8} {_clrInfo.IndexFileSize:X8}" : _clrInfo.BuildId.ToHex(); - sb.AppendLine($"#{Id} {config} runtime at {RuntimeModule.ImageBase:X16} size {RuntimeModule.ImageSize:X8} index {index}"); + sb.AppendLine($"#{Id} {config} runtime {_clrInfo} at {RuntimeModule.ImageBase:X16} size {RuntimeModule.ImageSize:X8} index {index}"); if (_clrInfo.IsSingleFile) { - sb.AppendLine($" Single-file runtime module path: {RuntimeModule.FileName}"); + sb.Append($" Single-file runtime module path: {RuntimeModule.FileName}"); } else { - sb.AppendLine($" Runtime module path: {RuntimeModule.FileName}"); + sb.Append($" Runtime module path: {RuntimeModule.FileName}"); } if (RuntimeModuleDirectory is not null) { - sb.AppendLine($" Runtime module directory: {RuntimeModuleDirectory}"); + sb.AppendLine(); + sb.Append($" Runtime module directory: {RuntimeModuleDirectory}"); } if (_dacFilePath is not null) { - sb.AppendLine($" DAC: {_dacFilePath}"); + sb.AppendLine(); + sb.Append($" DAC: {_dacFilePath}"); } if (_dbiFilePath is not null) { - sb.AppendLine($" DBI: {_dbiFilePath}"); + sb.AppendLine(); + sb.Append($" DBI: {_dbiFilePath}"); } return sb.ToString(); } diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/RuntimeProvider.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/RuntimeProvider.cs new file mode 100644 index 000000000..1482f3f02 --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/RuntimeProvider.cs @@ -0,0 +1,43 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.Runtime; +using System; +using System.Collections.Generic; + +namespace Microsoft.Diagnostics.DebugServices.Implementation +{ + /// + /// ClrMD runtime provider implementation + /// + [ServiceExport(Type = typeof(IRuntimeProvider), Scope = ServiceScope.Provider)] + public class RuntimeProvider : IRuntimeProvider + { + private readonly IServiceProvider _services; + + public RuntimeProvider(IServiceProvider services) + { + _services = services; + } + + #region IRuntimeProvider + + /// + /// Returns the list of .NET runtimes in the target + /// + /// The starting runtime id for this provider + public IEnumerable EnumerateRuntimes(int startingRuntimeId) + { + DataTarget dataTarget = new(new CustomDataTarget(_services.GetService())) { + FileLocator = null + }; + for (int i = 0; i < dataTarget.ClrVersions.Length; i++) + { + yield return new Runtime(_services, startingRuntimeId + i, dataTarget.ClrVersions[i]); + } + } + + #endregion + } +} diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/RuntimeService.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/RuntimeService.cs index 74e93da88..43ae8e555 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/RuntimeService.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/RuntimeService.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using Microsoft.Diagnostics.Runtime; using System; using System.Collections.Generic; using System.Text; @@ -10,36 +9,37 @@ using System.Text; namespace Microsoft.Diagnostics.DebugServices.Implementation { /// - /// ClrMD runtime service implementation + /// Runtime service implementation /// - public class RuntimeService : IRuntimeService + [ServiceExport(Type = typeof(IRuntimeService), Scope = ServiceScope.Target)] + public class RuntimeService : IRuntimeService, IDisposable { - private readonly ITarget _target; - private readonly IDisposable _onFlushEvent; - private DataTarget _dataTarget; - private List _runtimes; - private IContextService _contextService; + private readonly IServiceProvider _services; + private readonly IServiceManager _serviceManager; + private List _runtimes; - public RuntimeService(ITarget target) + public RuntimeService(IServiceProvider services, ITarget target) { - _target = target; - _onFlushEvent = target.OnFlushEvent.Register(() => { - if (_runtimes is not null && _runtimes.Count == 0) + _services = services; + _serviceManager = services.GetService(); + target.OnFlushEvent.Register(Flush); + } + + void IDisposable.Dispose() => Flush(); + + private void Flush() + { + if (_runtimes is not null) + { + foreach (IRuntime runtime in _runtimes) { - // If there are no runtimes, try find them again when the target stops - _runtimes = null; - _dataTarget?.Dispose(); - _dataTarget = null; + if (runtime is IDisposable disposable) { + disposable.Dispose(); + } } - }); - // Can't make RuntimeService IDisposable directly because _dataTarget.Dispose() disposes the IDataReader - // passed which is this RuntimeService instance which would call _dataTarget.Dispose again and causing a - // stack overflow. - target.OnDestroyEvent.Register(() => { - _dataTarget?.Dispose(); - _dataTarget = null; - _onFlushEvent.Dispose(); - }); + _runtimes.Clear(); + _runtimes = null; + } } #region IRuntimeService @@ -51,38 +51,27 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation { if (_runtimes is null) { - _runtimes = new List(); - if (_dataTarget is null) - { - _dataTarget = new DataTarget(new CustomDataTarget(_target.Services.GetService())) { - FileLocator = null - }; - } - if (_dataTarget is not null) + _runtimes = new List(); + foreach (ServiceFactory factory in _serviceManager.EnumerateProviderFactories(typeof(IRuntimeProvider))) { - for (int i = 0; i < _dataTarget.ClrVersions.Length; i++) - { - _runtimes.Add(new Runtime(_target, i, _dataTarget.ClrVersions[i])); - } + IRuntimeProvider provider = (IRuntimeProvider)factory(_services); + _runtimes.AddRange(provider.EnumerateRuntimes(_runtimes.Count)); } } return _runtimes; } #endregion - - private IRuntime CurrentRuntime => ContextService.Services.GetService(); - - private IContextService ContextService => _contextService ??= _target.Services.GetService(); - + public override string ToString() { var sb = new StringBuilder(); if (_runtimes is not null) { + IRuntime currentRuntime = _services.GetService()?.GetCurrentRuntime(); foreach (IRuntime runtime in _runtimes) { - string current = _runtimes.Count > 1 ? runtime == CurrentRuntime ? "*" : " " : ""; + string current = _runtimes.Count > 1 ? runtime == currentRuntime ? "*" : " " : ""; sb.Append(current); sb.AppendLine(runtime.ToString()); } diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceEvent.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceEvent.cs index 79e9e88f2..b49b1fa19 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceEvent.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceEvent.cs @@ -41,7 +41,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation } } - private readonly LinkedListNode _events = new LinkedListNode(); + private readonly LinkedListNode _events = new(); public ServiceEvent() { @@ -67,4 +67,66 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation } } } + + /// + /// The service event with one parameter implementation + /// + public class ServiceEvent : IServiceEvent + { + private class EventNode : LinkedListNode, IDisposable + { + private readonly Action _callback; + + internal EventNode(bool oneshot, Action callback) + { + if (oneshot) + { + _callback = (T parameter) => { + callback(parameter); + Remove(); + }; + } + else + { + _callback = callback; + } + } + + internal void Fire(T parameter) + { + _callback(parameter); + } + + void IDisposable.Dispose() + { + Remove(); + } + } + + private readonly LinkedListNode _events = new(); + + public ServiceEvent() + { + } + + public IDisposable Register(Action callback) => Register(oneshot: false, callback); + + public IDisposable RegisterOneShot(Action callback) => Register(oneshot: true, callback); + + private IDisposable Register(bool oneshot, Action callback) + { + // Insert at the end of the list + var node = new EventNode(oneshot, callback); + _events.InsertBefore(node); + return node; + } + + public void Fire(T parameter) + { + foreach (EventNode node in _events.GetValues()) + { + node.Fire(parameter); + } + } + } } diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceManager.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceManager.cs new file mode 100644 index 000000000..2d3abf589 --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceManager.cs @@ -0,0 +1,234 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Reflection; +using System.IO; +using System.Diagnostics; + +namespace Microsoft.Diagnostics.DebugServices.Implementation +{ + /// + /// The service manager registers any ServiceExportAttribute on types and methods and sets properties, + /// fields and methods marked with the ServiceImportAttribute that match the provided services. Tracks + /// any unresolved service requests and injects them when the service is registered. + /// + public class ServiceManager : IServiceManager + { + private readonly Dictionary[] _factories; + private readonly Dictionary> _providerFactories; + private readonly ServiceEvent _notifyExtensionLoad; + private bool _finalized; + + /// + /// This event fires when an extension assembly is loaded + /// + public IServiceEvent NotifyExtensionLoad => _notifyExtensionLoad; + + /// + /// Create a service manager instance + /// + public ServiceManager() + { + _factories = new Dictionary[(int)ServiceScope.Max]; + _providerFactories = new Dictionary>(); + _notifyExtensionLoad = new ServiceEvent(); + for (int i = 0; i < (int)ServiceScope.Max; i++) + { + _factories[i] = new Dictionary(); + } + } + + /// + /// Creates a new service container factory with all the registered factories for the given scope. + /// + /// global, per-target, per-runtime, etc. service type + /// parent service provider to chain + /// + public ServiceContainerFactory CreateServiceContainerFactory(ServiceScope scope, IServiceProvider parent) + { + if (!_finalized) throw new InvalidOperationException(); + return new ServiceContainerFactory(parent, _factories[(int)scope]); + } + + /// + /// Get the provider factories for a type or interface. + /// + /// type or interface + /// the provider factories for the type + public IEnumerable EnumerateProviderFactories(Type providerType) + { + if (!_finalized) throw new InvalidOperationException(); + + if (_providerFactories.TryGetValue(providerType, out List factories)) + { + return factories; + } + return Array.Empty(); + } + + /// + /// Finds all the ServiceExport attributes in the assembly and registers. + /// + /// service implementation assembly + public void RegisterExportedServices(Assembly assembly) + { + foreach (Type serviceType in assembly.GetExportedTypes()) + { + if (serviceType.IsClass) + { + RegisterExportedServices(serviceType); + } + } + } + + /// + /// Finds all the ServiceExport attributes in the type and registers. + /// + /// service implementation type + public void RegisterExportedServices(Type serviceType) + { + if (_finalized) throw new InvalidOperationException(); + + for (Type currentType = serviceType; currentType is not null; currentType = currentType.BaseType) + { + if (currentType == typeof(object) || currentType == typeof(ValueType)) + { + break; + } + ServiceExportAttribute serviceAttribute = currentType.GetCustomAttribute(inherit: false); + if (serviceAttribute is not null) + { + ServiceFactory factory = (provider) => Utilities.CreateInstance(serviceType, provider); + AddServiceFactory(serviceAttribute.Scope, serviceAttribute.Type ?? serviceType, factory); + } + // The method or constructor must be static and public + foreach (MethodInfo methodInfo in currentType.GetMethods(BindingFlags.Static | BindingFlags.Public)) + { + serviceAttribute = methodInfo.GetCustomAttribute(inherit: false); + if (serviceAttribute is not null) + { + AddServiceFactory(serviceAttribute.Scope, serviceAttribute.Type ?? methodInfo.ReturnType, (provider) => Utilities.CreateInstance(methodInfo, provider)); + } + } + } + } + + /// + /// Add service containerFactory for the specific scope. + /// + /// service type + /// global, per-target, per-runtime, etc. service type + /// function to create service instance + public void AddServiceFactory(ServiceScope scope, ServiceFactory factory) => AddServiceFactory(scope, typeof(T), factory); + + /// + /// Add service containerFactory for the specific scope. + /// + /// global, per-target, per-runtime, etc. service type + /// service type or interface + /// function to create service instance + public void AddServiceFactory(ServiceScope scope, Type serviceType, ServiceFactory factory) + { + if (factory is null) throw new ArgumentNullException(nameof(factory)); + if (_finalized) throw new InvalidOperationException(); + + if (scope == ServiceScope.Provider) + { + if (!_providerFactories.TryGetValue(serviceType, out List factories)) + { + _providerFactories.Add(serviceType, factories = new List()); + } + factories.Add(factory); + } + else + { + _factories[(int)scope].Add(serviceType, factory); + } + } + + /// + /// Load any extra extensions in the search path + /// + public void LoadExtensions() + { + if (_finalized) throw new InvalidOperationException(); + + List extensionPaths = new(); + string diagnosticExtensions = Environment.GetEnvironmentVariable("DOTNET_DIAGNOSTIC_EXTENSIONS"); + if (!string.IsNullOrEmpty(diagnosticExtensions)) + { + string[] paths = diagnosticExtensions.Split(new char[] { Path.PathSeparator }, StringSplitOptions.RemoveEmptyEntries); + extensionPaths.AddRange(paths); + } + string assemblyPath = Assembly.GetExecutingAssembly().Location; + if (!string.IsNullOrEmpty(assemblyPath)) + { + string searchPath = Path.Combine(Path.GetDirectoryName(assemblyPath), "extensions"); + if (Directory.Exists(searchPath)) + { + try + { + string[] extensionFiles = Directory.GetFiles(searchPath, "*.dll"); + extensionPaths.AddRange(extensionFiles); + } + catch (Exception ex) when (ex is IOException || ex is ArgumentException || ex is UnauthorizedAccessException || ex is System.Security.SecurityException) + { + Trace.TraceError(ex.ToString()); + } + } + } + foreach (string extensionPath in extensionPaths) + { + LoadExtension(extensionPath); + } + } + + /// + /// Load extension from the path + /// + /// extension assembly path + public void LoadExtension(string extensionPath) + { + if (_finalized) throw new InvalidOperationException(); + Assembly assembly = null; + try + { + assembly = Assembly.LoadFrom(extensionPath); + } + catch (Exception ex) when (ex is IOException || ex is ArgumentException || ex is BadImageFormatException || ex is System.Security.SecurityException) + { + Trace.TraceError(ex.ToString()); + } + if (assembly is not null) + { + RegisterAssembly(assembly); + } + } + + /// + /// Register the exported services in the assembly and notify the assembly has loaded. + /// + /// extension assembly + public void RegisterAssembly(Assembly assembly) + { + if (_finalized) throw new InvalidOperationException(); + try + { + RegisterExportedServices(assembly); + _notifyExtensionLoad.Fire(assembly); + } + catch (Exception ex) when (ex is DiagnosticsException || ex is NotSupportedException || ex is FileNotFoundException) + { + Trace.TraceError(ex.ToString()); + } + } + + /// + /// Finalizes the service manager. Loading extensions or adding service factories are not allowed after this call. + /// + public void FinalizeServices() => _finalized = true; + } +} diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceProvider.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceProvider.cs deleted file mode 100644 index 24b98961a..000000000 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/ServiceProvider.cs +++ /dev/null @@ -1,118 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using System; -using System.Collections.Generic; - -namespace Microsoft.Diagnostics.DebugServices.Implementation -{ - public class ServiceProvider : IServiceProvider - { - private readonly Func[] _parents; - private readonly Dictionary> _factories; - private readonly Dictionary _services; - - /// - /// Create a service provider instance - /// - public ServiceProvider() - : this(Array.Empty>()) - { - } - - /// - /// Create a service provider with parent provider - /// - /// search this provider if service isn't found in this instance - public ServiceProvider(IServiceProvider parent) - : this(new Func[] { () => parent }) - { - } - - /// - /// Create a service provider with parent provider and service factories - /// - /// an array of functions to return the next provider to search if service isn't found in this instance - public ServiceProvider(Func[] parents) - { - _parents = parents; - _factories = new Dictionary>(); - _services = new Dictionary(); - } - - /// - /// Add service factory and cache result when requested. - /// - /// service type - /// function to create service instance - public void AddServiceFactory(Func factory) - { - _factories.Add(typeof(T), () => { - object service = factory(); - _services.Add(typeof(T), service); - return service; - }); - } - - /// - /// Add service factory. Lets the service decide on how the cache the result. - /// - /// service type - /// function to create service instance - public void AddServiceFactoryWithNoCaching(Func factory) => _factories.Add(typeof(T), factory); - - /// - /// Adds a service or context to inject into an command. - /// - /// type of service - /// service instance - public void AddService(T instance) => AddService(typeof(T), instance); - - /// - /// Add a service instance. - /// - /// service type - /// instance - public void AddService(Type type, object service) => _services.Add(type, service); - - /// - /// Flushes the cached service instance for the specified type. Does not remove the service factory registered for the type. - /// - /// service type - public void RemoveService(Type type) => _services.Remove(type); - - /// - /// Flushes all the cached instances of the services. Does not remove any of the service factories registered. - /// - public void FlushServices() => _services.Clear(); - - /// - /// Returns the instance of the service or returns null if service doesn't exist - /// - /// service type - /// service instance or null - public object GetService(Type type) - { - if (!_services.TryGetValue(type, out object service)) - { - if (_factories.TryGetValue(type, out Func factory)) - { - service = factory(); - } - } - if (service == null) - { - foreach (Func parent in _parents) - { - service = parent()?.GetService(type); - if (service != null) - { - break; - } - } - } - return service; - } - } -} diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/SymbolService.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/SymbolService.cs index 58b453f1b..e5b7afbb8 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/SymbolService.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/SymbolService.cs @@ -3,6 +3,8 @@ // See the LICENSE file in the project root for more information. using Microsoft.FileFormats; +using Microsoft.FileFormats.ELF; +using Microsoft.FileFormats.MachO; using Microsoft.FileFormats.PE; using Microsoft.SymbolStore; using Microsoft.SymbolStore.KeyGenerators; @@ -715,10 +717,10 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation string fileName = fileKey.FullPathName; if (File.Exists(fileName)) { - using ELFModule elfModule = ELFModule.OpenFile(fileName); - if (elfModule is not null) + using ELFFile elfFile = Utilities.OpenELFFile(fileName); + if (elfFile is not null) { - var generator = new ELFFileKeyGenerator(Tracer.Instance, elfModule, fileName); + var generator = new ELFFileKeyGenerator(Tracer.Instance, elfFile, fileName); foreach (SymbolStoreKey key in generator.GetKeys(flags)) { if (fileKey.Equals(key)) @@ -771,10 +773,10 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation string fileName = fileKey.FullPathName; if (File.Exists(fileName)) { - using MachOModule machOModule = MachOModule.OpenFile(fileName); - if (machOModule is not null) + using MachOFile machOFile = Utilities.OpenMachOFile(fileName); + if (machOFile is not null) { - var generator = new MachOFileKeyGenerator(Tracer.Instance, machOModule, fileName); + var generator = new MachOFileKeyGenerator(Tracer.Instance, machOFile, fileName); IEnumerable keys = generator.GetKeys(flags); foreach (SymbolStoreKey key in keys) { diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/Target.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/Target.cs index e4d66bcd1..e5f8116b6 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/Target.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/Target.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Runtime.InteropServices; @@ -15,12 +14,13 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// /// ITarget base implementation /// - public abstract class Target : ITarget, IDisposable + public abstract class Target : ITarget { private readonly string _dumpPath; private string _tempDirectory; + private ServiceContainer _serviceContainer; - public readonly ServiceProvider ServiceProvider; + protected readonly ServiceContainerFactory _serviceContainerFactory; public Target(IHost host, int id, string dumpPath) { @@ -32,13 +32,16 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation OnFlushEvent = new ServiceEvent(); OnDestroyEvent = new ServiceEvent(); - // Initialize the per-target services - ServiceProvider = new ServiceProvider(host.Services); + // Initialize the per-target services. + _serviceContainerFactory = host.Services.GetService().CreateServiceContainerFactory(ServiceScope.Target, host.Services); + _serviceContainerFactory.AddServiceFactory((_) => this); + } - // Add the per-target services - ServiceProvider.AddService(this); - ServiceProvider.AddServiceFactory(() => new DataReader(this)); - ServiceProvider.AddServiceFactory(() => new RuntimeService(this)); + protected void Finished() + { + // Now the that the target is completely initialized, finalize container and fire event + _serviceContainer = _serviceContainerFactory.Build(); + Host.OnTargetCreate.Fire(this); } #region ITarget @@ -93,7 +96,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// /// The per target services. /// - public IServiceProvider Services => ServiceProvider; + public IServiceProvider Services => _serviceContainer; /// /// Invoked when this target is flushed (via the Flush() call). @@ -110,22 +113,24 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation } /// - /// Invoked when the target is closed. + /// Invoked when the target is destroyed /// public IServiceEvent OnDestroyEvent { get; } - #endregion - /// - /// Releases the target and the target's resources. + /// Cleans up the target and releases target's resources. /// - public void Dispose() + public void Destroy() { - Trace.TraceInformation($"Disposing target #{Id}"); + Trace.TraceInformation($"Destroy target #{Id}"); OnDestroyEvent.Fire(); + _serviceContainer.RemoveService(typeof(ITarget)); + _serviceContainer.DisposeServices(); CleanupTempDirectory(); } + #endregion + private void CleanupTempDirectory() { if (_tempDirectory != null) @@ -166,7 +171,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation if (_dumpPath != null) { sb.AppendLine($"Dump path: {_dumpPath}"); } - var runtimeService = ServiceProvider.GetService(); + var runtimeService = Services.GetService(); if (runtimeService != null) { sb.AppendLine(runtimeService.ToString()); diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/TargetFromDataReader.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/TargetFromDataReader.cs index 07d901bb0..6933cf713 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/TargetFromDataReader.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/TargetFromDataReader.cs @@ -33,32 +33,43 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation IsDump = true; Architecture = dataReader.Architecture; - if (dataReader.ProcessId != -1) { + if (dataReader.ProcessId != -1) + { ProcessId = (uint)dataReader.ProcessId; } OnFlushEvent.Register(dataReader.FlushCachedData); // Add the thread, memory, and module services - IMemoryService rawMemoryService = new MemoryServiceFromDataReader(_dataReader); - ServiceProvider.AddServiceFactory(() => new ThreadServiceFromDataReader(this, _dataReader)); - ServiceProvider.AddServiceFactory(() => new ModuleServiceFromDataReader(this, rawMemoryService, _dataReader)); - ServiceProvider.AddServiceFactory(() => { - IMemoryService memoryService = rawMemoryService; + _serviceContainerFactory.AddServiceFactory((services) => new ThreadServiceFromDataReader(services, _dataReader)); + _serviceContainerFactory.AddServiceFactory((services) => new ModuleServiceFromDataReader(services, _dataReader)); + _serviceContainerFactory.AddServiceFactory((_) => { + IMemoryService memoryService = new MemoryServiceFromDataReader(_dataReader); if (IsDump) { - memoryService = new ImageMappingMemoryService(this, memoryService); - // Any dump created for a MacOS target does not have managed assemblies in the module service so - // we need to use the metadata mapping memory service to make sure the metadata is available and - // 7.0 Linux builds have an extra System.Private.CoreLib module mapping that causes the image - // mapper not to be able to map in the metadata. + // The target container factory needs to be cloned for the memory services so the original IMemoryService + // factory can be removed so it doesn't inadvertently get called. The image mapping service is going to + // replace it with the memory service instance passed. The clone.Build() creates a new separate service + // container instance for the image mapping service. + ServiceContainerFactory clone = _serviceContainerFactory.Clone(); + clone.RemoveServiceFactory(); + + // The underlying host (dotnet-dump usually) doesn't map native modules into the address space + memoryService = new ImageMappingMemoryService(clone.Build(), memoryService, managed: false); + + // Any dump created for a MacOS target does not have managed assemblies in the native module service so + // we need to use this managed mapping memory service to make sure the metadata is available and 7.0 Linux + // builds have an extra System.Private.CoreLib module mapping that causes the native image mapper not to + // be able to map in the metadata. if (targetOS == OSPlatform.OSX || targetOS == OSPlatform.Linux) { - memoryService = new MetadataMappingMemoryService(this, memoryService); + memoryService = new ImageMappingMemoryService(clone.Build(), memoryService, managed: true); } } return memoryService; }); + + Finished(); } } } diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/Thread.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/Thread.cs index 577e1905a..99a972c1b 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/Thread.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/Thread.cs @@ -8,20 +8,27 @@ using System.Runtime.InteropServices; namespace Microsoft.Diagnostics.DebugServices.Implementation { - public class Thread : IThread + public class Thread : IThread, IDisposable { private readonly ThreadService _threadService; private byte[] _threadContext; private ulong? _teb; - public readonly ServiceProvider ServiceProvider; + protected readonly ServiceContainer _serviceContainer; public Thread(ThreadService threadService, int index, uint id) { _threadService = threadService; ThreadIndex = index; ThreadId = id; - ServiceProvider = new ServiceProvider(); + _serviceContainer = threadService.Services.GetService().CreateServiceContainer(ServiceScope.Thread, threadService.Services); + _serviceContainer.AddService(this); + } + + void IDisposable.Dispose() + { + _serviceContainer.RemoveService(typeof(IThread)); + _serviceContainer.DisposeServices(); } #region IThread @@ -32,7 +39,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation public ITarget Target => _threadService.Target; - public IServiceProvider Services => ServiceProvider; + public IServiceProvider Services => _serviceContainer; public bool TryGetRegisterValue(int index, out ulong value) { diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/ThreadService.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/ThreadService.cs index e201665a5..89cd9cee4 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/ThreadService.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/ThreadService.cs @@ -15,30 +15,29 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// /// Provides thread and register info and values for the clrmd IDataReader /// - public abstract class ThreadService : IThreadService + public abstract class ThreadService : IThreadService, IDisposable { - internal protected readonly ITarget Target; private readonly int _contextSize; private readonly uint _contextFlags; private readonly Dictionary _lookupByName; private readonly Dictionary _lookupByIndex; private Dictionary _threads; - public ThreadService(ITarget target) - { - Target = target; + internal protected readonly IServiceProvider Services; + internal protected readonly ITarget Target; - target.OnFlushEvent.Register(() => { - _threads?.Clear(); - _threads = null; - }); + public ThreadService(IServiceProvider services) + { + Services = services; + Target = services.GetService(); + Target.OnFlushEvent.Register(Flush); Type contextType; - switch (target.Architecture) + switch (Target.Architecture) { case Architecture.X64: // Dumps generated with newer dbgeng have bigger context buffers and clrmd requires the context size to at least be that size. - _contextSize = target.OperatingSystem == OSPlatform.Windows ? 0x700 : AMD64Context.Size; + _contextSize = Target.OperatingSystem == OSPlatform.Windows ? 0x700 : AMD64Context.Size; _contextFlags = AMD64Context.ContextControl | AMD64Context.ContextInteger | AMD64Context.ContextSegments | AMD64Context.ContextFloatingPoint; contextType = typeof(AMD64Context); break; @@ -62,7 +61,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation break; default: - throw new PlatformNotSupportedException($"Unsupported architecture: {target.Architecture}"); + throw new PlatformNotSupportedException($"Unsupported architecture: {Target.Architecture}"); } var registers = new List(); @@ -71,7 +70,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation FieldInfo[] fields = contextType.GetFields(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic); foreach (FieldInfo field in fields) { RegisterAttribute registerAttribute = field.GetCustomAttributes(inherit: false).SingleOrDefault(); - if (registerAttribute == null) { + if (registerAttribute is null) { continue; } RegisterType registerType = registerAttribute.RegisterType & RegisterType.TypeMask; @@ -104,6 +103,23 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation Registers = registers; } + void IDisposable.Dispose() => Flush(); + + private void Flush() + { + if (_threads is not null) + { + foreach (IThread thread in _threads.Values) + { + if (thread is IDisposable disposable) { + disposable.Dispose(); + } + } + _threads.Clear(); + _threads = null; + } + } + #region IThreadService /// @@ -226,7 +242,7 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// private Dictionary GetThreads() { - if (_threads == null) { + if (_threads is null) { _threads = GetThreadsInner().OrderBy((thread) => thread.ThreadId).ToDictionary((thread) => thread.ThreadId); } return _threads; diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/ThreadServiceFromDataReader.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/ThreadServiceFromDataReader.cs index 68e57af5b..fb5c7f35e 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/ThreadServiceFromDataReader.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/ThreadServiceFromDataReader.cs @@ -6,7 +6,6 @@ using Microsoft.Diagnostics.Runtime; using Microsoft.Diagnostics.Runtime.DataReaders.Implementation; using System; using System.Collections.Generic; -using System.Collections.Immutable; using System.Diagnostics; using System.Linq; @@ -20,8 +19,8 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation private readonly IDataReader _dataReader; private readonly IThreadReader _threadReader; - public ThreadServiceFromDataReader(ITarget target, IDataReader dataReader) - : base(target) + public ThreadServiceFromDataReader(IServiceProvider services, IDataReader dataReader) + : base(services) { _dataReader = dataReader; _threadReader = (IThreadReader)dataReader; diff --git a/src/Microsoft.Diagnostics.DebugServices.Implementation/Utilities.cs b/src/Microsoft.Diagnostics.DebugServices.Implementation/Utilities.cs index 6eba2ce40..3bbf9fea5 100644 --- a/src/Microsoft.Diagnostics.DebugServices.Implementation/Utilities.cs +++ b/src/Microsoft.Diagnostics.DebugServices.Implementation/Utilities.cs @@ -2,6 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Microsoft.FileFormats; +using Microsoft.FileFormats.ELF; +using Microsoft.FileFormats.MachO; using Microsoft.FileFormats.PE; using System; using System.Collections.Immutable; @@ -10,6 +13,7 @@ using System.IO; using System.Linq; using System.Reflection; using System.Reflection.PortableExecutable; +using System.Runtime.InteropServices; namespace Microsoft.Diagnostics.DebugServices.Implementation { @@ -87,6 +91,98 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation return null; } + /// + /// Opens and returns an ELFFile instance from the local file path + /// + /// ELF file to open + /// ELFFile instance or null + public static ELFFile OpenELFFile(string filePath) + { + Stream stream = TryOpenFile(filePath); + if (stream is not null) + { + try + { + ELFFile elfFile = new(new StreamAddressSpace(stream), position: 0, isDataSourceVirtualAddressSpace: false); + if (!elfFile.IsValid()) + { + Trace.TraceError($"OpenFile: not a valid file {filePath}"); + return null; + } + return elfFile; + } + catch (Exception ex) when (ex is InvalidVirtualAddressException || ex is BadInputFormatException || ex is IOException) + { + Trace.TraceError($"OpenFile: {filePath} exception {ex.Message}"); + } + } + return null; + } + + /// + /// Opens and returns an MachOFile instance from the local file path + /// + /// MachO file to open + /// MachOFile instance or null + public static MachOFile OpenMachOFile(string filePath) + { + Stream stream = TryOpenFile(filePath); + if (stream is not null) + { + try + { + MachOFile machoModule = new(new StreamAddressSpace(stream), position: 0, dataSourceIsVirtualAddressSpace: false); + if (!machoModule.IsValid()) + { + Trace.TraceError($"OpenMachOFile: not a valid file {filePath}"); + return null; + } + return machoModule; + } + catch (Exception ex) when (ex is InvalidVirtualAddressException || ex is BadInputFormatException || ex is IOException) + { + Trace.TraceError($"OpenMachOFile: {filePath} exception {ex.Message}"); + } + } + return null; + } + + /// + /// Creates a ELFFile service instance of the module in memory. + /// + [ServiceExport(Scope = ServiceScope.Module)] + public static ELFFile CreateELFFile(IMemoryService memoryService, IModule module) + { + if (module.Target.OperatingSystem == OSPlatform.Linux) + { + Stream stream = memoryService.CreateMemoryStream(); + var elfFile = new ELFFile(new StreamAddressSpace(stream), module.ImageBase, true); + if (elfFile.IsValid()) + { + return elfFile; + } + } + return null; + } + + /// + /// Creates a MachOFile service instance of the module in memory. + /// + [ServiceExport(Scope = ServiceScope.Module)] + public static MachOFile CreateMachOFile(IMemoryService memoryService, IModule module) + { + if (module.Target.OperatingSystem == OSPlatform.OSX) + { + Stream stream = memoryService.CreateMemoryStream(); + var elfFile = new MachOFile(new StreamAddressSpace(stream), module.ImageBase, true); + if (elfFile.IsValid()) + { + return elfFile; + } + } + return null; + } + /// /// Attempt to open a file stream. /// @@ -109,18 +205,119 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation return null; } + /// + /// Returns the .NET user directory + /// + public static string GetDotNetHomeDirectory() + { + string dotnetHome; + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { + dotnetHome = Path.Combine(Environment.GetEnvironmentVariable("USERPROFILE") ?? throw new ArgumentNullException("USERPROFILE environment variable not found"), ".dotnet"); + } + else { + dotnetHome = Path.Combine(Environment.GetEnvironmentVariable("HOME") ?? throw new ArgumentNullException("HOME environment variable not found"), ".dotnet"); + } + return dotnetHome; + } + + /// + /// Create the type instance and fill in any service imports + /// + /// type to create + /// service provider + /// new instance + public static object CreateInstance(Type type, IServiceProvider provider) + { + object instance = InvokeConstructor(type, provider); + if (instance is not null) + { + ImportServices(instance, provider); + } + return instance; + } + + /// + /// Call the static method (constructor) to create the instance and fill in any service imports + /// + /// static method (constructor) to use to create instance + /// service provider + /// new instance + public static object CreateInstance(MethodBase method, IServiceProvider provider) + { + object instance = Invoke(method, null, provider); + if (instance is not null) + { + ImportServices(instance, provider); + } + return instance; + } + + /// + /// Set any fields, property or method marked with the ServiceImportAttribute to the service requested. + /// + /// object instance to process + /// service provider + public static void ImportServices(object instance, IServiceProvider provider) + { + if (instance == null) throw new ArgumentNullException(nameof(instance)); + + for (Type currentType = instance.GetType(); currentType is not null; currentType = currentType.BaseType) + { + if (currentType == typeof(object) || currentType == typeof(ValueType)) + { + break; + } + FieldInfo[] fields = currentType.GetFields(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic); + foreach (FieldInfo field in fields) + { + ServiceImportAttribute attribute = field.GetCustomAttribute(inherit: false); + if (attribute is not null) + { + object serviceInstance = provider.GetService(field.FieldType); + if (serviceInstance is null && !attribute.Optional) + { + throw new DiagnosticsException($"The {field.FieldType.Name} service is required by the {field.Name} field"); + } + field.SetValue(instance, serviceInstance); + } + } + PropertyInfo[] properties = currentType.GetProperties(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic); + foreach (PropertyInfo property in properties) + { + ServiceImportAttribute attribute = property.GetCustomAttribute(inherit: false); + if (attribute is not null) + { + object serviceInstance = provider.GetService(property.PropertyType); + if (serviceInstance is null && !attribute.Optional) + { + throw new DiagnosticsException($"The {property.PropertyType.Name} service is required by the {property.Name} property"); + } + property.SetValue(instance, serviceInstance); + } + } + MethodInfo[] methods = currentType.GetMethods(BindingFlags.Instance | BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.NonPublic); + foreach (MethodInfo method in methods) + { + ServiceImportAttribute attribute = method.GetCustomAttribute(inherit: false); + if (attribute is not null) + { + Utilities.Invoke(method, instance, provider); + } + } + } + } + /// /// Call the constructor of the type and return the instance binding any /// services in the constructor parameters. /// /// type to create /// services - /// if true, the service is not required /// type instance - public static object InvokeConstructor(Type type, IServiceProvider provider, bool optional) + public static object InvokeConstructor(Type type, IServiceProvider provider) { ConstructorInfo constructor = type.GetConstructors().Single(); - object[] arguments = BuildArguments(constructor, provider, optional); + object[] arguments = BuildArguments(constructor, provider); try { return constructor.Invoke(arguments); @@ -138,11 +335,10 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation /// method to invoke /// class instance or null if static /// services - /// if true, the service is not required /// method return value - public static object Invoke(MethodBase method, object instance, IServiceProvider provider, bool optional) + public static object Invoke(MethodBase method, object instance, IServiceProvider provider) { - object[] arguments = BuildArguments(method, provider, optional); + object[] arguments = BuildArguments(method, provider); try { return method.Invoke(instance, arguments); @@ -154,14 +350,20 @@ namespace Microsoft.Diagnostics.DebugServices.Implementation } } - private static object[] BuildArguments(MethodBase methodBase, IServiceProvider services, bool optional) + private static object[] BuildArguments(MethodBase methodBase, IServiceProvider services) { ParameterInfo[] parameters = methodBase.GetParameters(); object[] arguments = new object[parameters.Length]; for (int i = 0; i < parameters.Length; i++) { - // The parameter will passed as null to allow for "optional" services. The invoked - // method needs to check for possible null parameters. + // The service import attribute isn't necessary on parameters unless Optional property needs to be changed from the default of false. + bool optional = false; + ServiceImportAttribute attribute = parameters[i].GetCustomAttribute(inherit: false); + if (attribute is not null) + { + optional = attribute.Optional; + } + // The parameter will passed as null to allow for "optional" services. The invoked method needs to check for possible null parameters. arguments[i] = services.GetService(parameters[i].ParameterType); if (arguments[i] is null && !optional) { diff --git a/src/Microsoft.Diagnostics.DebugServices/CommandBase.cs b/src/Microsoft.Diagnostics.DebugServices/CommandBase.cs index 2898669a4..70f2ebec1 100644 --- a/src/Microsoft.Diagnostics.DebugServices/CommandBase.cs +++ b/src/Microsoft.Diagnostics.DebugServices/CommandBase.cs @@ -14,6 +14,7 @@ namespace Microsoft.Diagnostics.DebugServices /// /// Console service /// + [ServiceImport] public IConsoleService Console { get; set; } /// @@ -31,6 +32,15 @@ namespace Microsoft.Diagnostics.DebugServices Console.Write(message); } + /// + /// Display a blank line + /// + protected void WriteLine() + { + Console.WriteLine(); + Console.CancellationToken.ThrowIfCancellationRequested(); + } + /// /// Display line /// diff --git a/src/Microsoft.Diagnostics.DebugServices/CommandServiceExtensions.cs b/src/Microsoft.Diagnostics.DebugServices/CommandServiceExtensions.cs index 3a1d79e54..116bffa47 100644 --- a/src/Microsoft.Diagnostics.DebugServices/CommandServiceExtensions.cs +++ b/src/Microsoft.Diagnostics.DebugServices/CommandServiceExtensions.cs @@ -38,19 +38,10 @@ namespace Microsoft.Diagnostics.DebugServices /// list of types to search public static void AddCommands(this ICommandService commandService, IEnumerable types) { - foreach (Type type in types) { + foreach (Type type in types) + { commandService.AddCommands(type); } } - - /// - /// Add the commands and aliases attributes found in the type. - /// - /// command service instance - /// Command type to search - public static void AddCommands(this ICommandService commandService, Type type) - { - commandService.AddCommands(type, factory: null); - } } } diff --git a/src/Microsoft.Diagnostics.DebugServices/ConsoleServiceExtensions.cs b/src/Microsoft.Diagnostics.DebugServices/ConsoleServiceExtensions.cs index 808dc1dab..89eded2e6 100644 --- a/src/Microsoft.Diagnostics.DebugServices/ConsoleServiceExtensions.cs +++ b/src/Microsoft.Diagnostics.DebugServices/ConsoleServiceExtensions.cs @@ -8,6 +8,15 @@ namespace Microsoft.Diagnostics.DebugServices { public static class ConsoleServiceExtensions { + /// + /// Display a blank line + /// + /// + public static void WriteLine(this IConsoleService console) + { + console.Write(Environment.NewLine); + } + /// /// Display text /// diff --git a/src/Microsoft.Diagnostics.DebugServices/ContextServiceExtensions.cs b/src/Microsoft.Diagnostics.DebugServices/ContextServiceExtensions.cs index c8e1736de..351099887 100644 --- a/src/Microsoft.Diagnostics.DebugServices/ContextServiceExtensions.cs +++ b/src/Microsoft.Diagnostics.DebugServices/ContextServiceExtensions.cs @@ -11,25 +11,16 @@ namespace Microsoft.Diagnostics.DebugServices /// /// Returns the current target /// - public static ITarget GetCurrentTarget(this IContextService contextService) - { - return contextService.Services.GetService(); - } + public static ITarget GetCurrentTarget(this IContextService contextService) => contextService.Services.GetService(); /// /// Returns the current thread /// - public static IThread GetCurrentThread(this IContextService contextService) - { - return contextService.Services.GetService(); - } + public static IThread GetCurrentThread(this IContextService contextService) => contextService.Services.GetService(); /// /// Returns the current runtime /// - public static IRuntime GetCurrentRuntime(this IContextService contextService) - { - return contextService.Services.GetService(); - } + public static IRuntime GetCurrentRuntime(this IContextService contextService) => contextService.Services.GetService(); } } diff --git a/src/Microsoft.Diagnostics.DebugServices/DiagnosticsException.cs b/src/Microsoft.Diagnostics.DebugServices/DiagnosticsException.cs index 41f01735c..eabc327d6 100644 --- a/src/Microsoft.Diagnostics.DebugServices/DiagnosticsException.cs +++ b/src/Microsoft.Diagnostics.DebugServices/DiagnosticsException.cs @@ -26,4 +26,25 @@ namespace Microsoft.Diagnostics.DebugServices { } } + + /// + /// Thrown if a command is not supported on the configuration, platform or runtime + /// + public class CommandNotSupportedException : DiagnosticsException + { + public CommandNotSupportedException() + : base() + { + } + + public CommandNotSupportedException(string message) + : base(message) + { + } + + public CommandNotSupportedException(string message, Exception innerException) + : base(message, innerException) + { + } + } } diff --git a/src/Microsoft.Diagnostics.DebugServices/ICommandService.cs b/src/Microsoft.Diagnostics.DebugServices/ICommandService.cs index 7b4643842..b29484ae1 100644 --- a/src/Microsoft.Diagnostics.DebugServices/ICommandService.cs +++ b/src/Microsoft.Diagnostics.DebugServices/ICommandService.cs @@ -21,8 +21,7 @@ namespace Microsoft.Diagnostics.DebugServices /// Add the commands and aliases attributes found in the type. /// /// Command type to search - /// function to create command instance - void AddCommands(Type type, Func factory); + void AddCommands(Type type); /// /// Displays the help for a command diff --git a/src/Microsoft.Diagnostics.DebugServices/IContextService.cs b/src/Microsoft.Diagnostics.DebugServices/IContextService.cs index 1ecf28447..764c3aced 100644 --- a/src/Microsoft.Diagnostics.DebugServices/IContextService.cs +++ b/src/Microsoft.Diagnostics.DebugServices/IContextService.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Threading; namespace Microsoft.Diagnostics.DebugServices { diff --git a/src/Microsoft.Diagnostics.DebugServices/IField.cs b/src/Microsoft.Diagnostics.DebugServices/IField.cs new file mode 100644 index 000000000..a8668e575 --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices/IField.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Microsoft.Diagnostics.DebugServices +{ + /// + /// Describes a field in a type (IType) + /// + public interface IField + { + /// + /// The type this field belongs + /// + IType Type { get; } + + /// + /// The name of the field + /// + string Name { get; } + + /// + /// The offset from the beginning of the instance + /// + uint Offset { get; } + } +} \ No newline at end of file diff --git a/src/Microsoft.Diagnostics.DebugServices/IHost.cs b/src/Microsoft.Diagnostics.DebugServices/IHost.cs index 4c60e6d15..c9593a3e4 100644 --- a/src/Microsoft.Diagnostics.DebugServices/IHost.cs +++ b/src/Microsoft.Diagnostics.DebugServices/IHost.cs @@ -25,10 +25,15 @@ namespace Microsoft.Diagnostics.DebugServices public interface IHost { /// - /// Invoked on hosting debugger or dotnet-dump shutdown + /// Fires on hosting debugger or dotnet-dump shutdown /// IServiceEvent OnShutdownEvent { get; } + /// + /// Fires when an new target is created. + /// + IServiceEvent OnTargetCreate { get; } + /// /// Returns the hosting debugger type /// @@ -43,11 +48,5 @@ namespace Microsoft.Diagnostics.DebugServices /// Enumerates all the targets /// IEnumerable EnumerateTargets(); - - /// - /// Destroys/closes the specified target instance - /// - /// target instance - void DestroyTarget(ITarget target); } } diff --git a/src/Microsoft.Diagnostics.DebugServices/IModuleSymbols.cs b/src/Microsoft.Diagnostics.DebugServices/IModuleSymbols.cs index 0fe8c48b4..1cde6e6a5 100644 --- a/src/Microsoft.Diagnostics.DebugServices/IModuleSymbols.cs +++ b/src/Microsoft.Diagnostics.DebugServices/IModuleSymbols.cs @@ -2,7 +2,6 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; namespace Microsoft.Diagnostics.DebugServices { @@ -27,5 +26,13 @@ namespace Microsoft.Diagnostics.DebugServices /// address of symbol /// true if found bool TryGetSymbolAddress(string name, out ulong address); + + /// + /// Searches for a type by name + /// + /// type name to find + /// returned type if found + /// true if type found + bool TryGetType(string typeName, out IType type); } } diff --git a/src/Microsoft.Diagnostics.DebugServices/IRuntime.cs b/src/Microsoft.Diagnostics.DebugServices/IRuntime.cs index 56bd7723e..3c146a6c9 100644 --- a/src/Microsoft.Diagnostics.DebugServices/IRuntime.cs +++ b/src/Microsoft.Diagnostics.DebugServices/IRuntime.cs @@ -11,10 +11,11 @@ namespace Microsoft.Diagnostics.DebugServices /// public enum RuntimeType { - Desktop = 0, - NetCore = 1, - SingleFile = 2, - Unknown = 3 + Unknown = 0, + Desktop = 1, + NetCore = 2, + SingleFile = 3, + Other = 4 } /// diff --git a/src/Microsoft.Diagnostics.DebugServices/IRuntimeProvider.cs b/src/Microsoft.Diagnostics.DebugServices/IRuntimeProvider.cs new file mode 100644 index 000000000..8632ea4b8 --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices/IRuntimeProvider.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Diagnostics.DebugServices +{ + /// + /// Provides the runtime information to the runtime service + /// + public interface IRuntimeProvider + { + /// + /// Returns the list of runtimes in the target + /// + /// The starting runtime id for this provider + IEnumerable EnumerateRuntimes(int startingRuntimeId); + } +} diff --git a/src/Microsoft.Diagnostics.DebugServices/IServiceEvent.cs b/src/Microsoft.Diagnostics.DebugServices/IServiceEvent.cs index bcbaed50a..7c171b6c7 100644 --- a/src/Microsoft.Diagnostics.DebugServices/IServiceEvent.cs +++ b/src/Microsoft.Diagnostics.DebugServices/IServiceEvent.cs @@ -31,4 +31,30 @@ namespace Microsoft.Diagnostics.DebugServices /// void Fire(); } + + /// + /// An event interface with one parameter. + /// + public interface IServiceEvent + { + /// + /// Register for the event callback. Puts the new callback at the end of the list. + /// + /// callback delegate + /// An opaque IDisposable that will unregister the callback when disposed + IDisposable Register(Action callback); + + /// + /// Register for the event callback. Puts the new callback at the end of the list. Automatically + /// removed from the event list when fired. + /// + /// callback delegate + /// An opaque IDisposable that will unregister the callback when disposed + IDisposable RegisterOneShot(Action callback); + + /// + /// Fires the event + /// + void Fire(T parameter); + } } diff --git a/src/Microsoft.Diagnostics.DebugServices/IServiceManager.cs b/src/Microsoft.Diagnostics.DebugServices/IServiceManager.cs new file mode 100644 index 000000000..59e98f2de --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices/IServiceManager.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; + +namespace Microsoft.Diagnostics.DebugServices +{ + public interface IServiceManager + { + /// + /// Creates a new service container factory with all the registered factories for the given scope. + /// + /// global, per-target, per-runtime, etc. service type + /// parent service provider to chain to + /// IServiceContainerFactory instance + ServiceContainerFactory CreateServiceContainerFactory(ServiceScope scope, IServiceProvider parent); + + /// + /// Get the provider factories for a type or interface. + /// + /// type or interface + /// the provider factories for the type + IEnumerable EnumerateProviderFactories(Type providerType); + } +} diff --git a/src/Microsoft.Diagnostics.DebugServices/ITarget.cs b/src/Microsoft.Diagnostics.DebugServices/ITarget.cs index 4043cb99c..03e78754e 100644 --- a/src/Microsoft.Diagnostics.DebugServices/ITarget.cs +++ b/src/Microsoft.Diagnostics.DebugServices/ITarget.cs @@ -68,5 +68,10 @@ namespace Microsoft.Diagnostics.DebugServices /// Invoked when the target is destroyed. /// IServiceEvent OnDestroyEvent { get; } + + /// + /// Cleans up the target and releases target's resources. + /// + void Destroy(); } } diff --git a/src/Microsoft.Diagnostics.DebugServices/IType.cs b/src/Microsoft.Diagnostics.DebugServices/IType.cs new file mode 100644 index 000000000..90c8cf031 --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices/IType.cs @@ -0,0 +1,37 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; + +namespace Microsoft.Diagnostics.DebugServices +{ + /// + /// Describes a native type in a module + /// + public interface IType + { + /// + /// Name of the type + /// + string Name { get; } + + /// + /// The module of the type + /// + IModule Module { get; } + + /// + /// A list of all the fields in the type + /// + List Fields { get; } + + /// + /// Get a field by name + /// + /// name of the field to find + /// the returned field if found + /// true if found + bool TryGetField(string fieldName, out IField field); + } +} diff --git a/src/Microsoft.Diagnostics.DebugServices/ServiceContainer.cs b/src/Microsoft.Diagnostics.DebugServices/ServiceContainer.cs new file mode 100644 index 000000000..d11319b00 --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices/ServiceContainer.cs @@ -0,0 +1,116 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.Diagnostics.DebugServices +{ + /// + /// This service provider and container implementation caches the service instance. Calls + /// the service factory for the type if not already instantiated and cached and if no + /// factory, chains to the parent service container. + /// + /// This implementations allows multiple instances of the same service type to be + /// registered. They are queried by getting the IEnumerable of the service type. If + /// the non-enumerable service type is queried and there are multiple instances, an + /// exception is thrown. The IRuntimeService implementation uses this feature to + /// enumerate all the IRuntimeProvider instances registered in the system. + /// + public class ServiceContainer : IServiceProvider + { + private readonly IServiceProvider _parent; + private readonly Dictionary _instances; + private readonly Dictionary _factories; + + /// + /// Build a service provider with parent provider and service factories + /// + /// search this provider if service isn't found in this instance or null + /// service factories to initialize provider or null + public ServiceContainer(IServiceProvider parent, Dictionary factories) + { + Debug.Assert(factories != null); + _parent = parent; + _factories = factories; + _instances = new Dictionary(); + } + + /// + /// Add a service instance. Multiple instances for the same type are not allowed. + /// + /// service type + /// service instance (must derives from type) + public void AddService(Type type, object service) => _instances.Add(type, service); + + /// + /// Add a service instance. Multiple instances for the same type are not allowed. + /// + /// type of service + /// service instance (must derive from T) + public void AddService(T instance) => AddService(typeof(T), instance); + + /// + /// Flushes the cached service instance for the specified type. Does not remove the service factory registered for the type. + /// + /// service type + public void RemoveService(Type type) => _instances.Remove(type); + + /// + /// Dispose of the instantiated services. + /// + public void DisposeServices() + { + foreach (object service in _instances.Values) + { + if (service is IDisposable disposable) + { + disposable.Dispose(); + } + } + _instances.Clear(); + } + + /// + /// Get the cached/instantiated service instance if one exists. Don't call the factory or parent to create. + /// + /// service type + /// service instance (can be null) + /// if true, found service + public bool TryGetCachedService(Type type, out object service) + { + Debug.Assert(type != null); + if (type == typeof(IServiceProvider)) + { + service = this; + return true; + } + return _instances.TryGetValue(type, out service); + } + + /// + /// Returns the instance of the service or returns null if service doesn't exist + /// + /// service type + /// service instance or null + public object GetService(Type type) + { + if (TryGetCachedService(type, out object service)) + { + return service; + } + if (_factories.TryGetValue(type, out ServiceFactory factory)) + { + service = factory(this); + _instances.Add(type, service); + } + else + { + service = _parent?.GetService(type); + } + return service; + } + } +} diff --git a/src/Microsoft.Diagnostics.DebugServices/ServiceContainerFactory.cs b/src/Microsoft.Diagnostics.DebugServices/ServiceContainerFactory.cs new file mode 100644 index 000000000..a96a3f081 --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices/ServiceContainerFactory.cs @@ -0,0 +1,101 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.Diagnostics.DebugServices +{ + /// + /// The method used to create a service instance + /// + /// service provider instance that this factory is registered to. + /// service instance + public delegate object ServiceFactory(IServiceProvider provider); + + /// + /// + public class ServiceContainerFactory + { + private readonly Dictionary _factories; + private readonly IServiceProvider _parent; + private bool _finalized; + + /// + /// Build a service container factory with parent provider and service factories + /// + /// search this provider if service isn't found in this instance or null + /// service factories to initialize provider or null + public ServiceContainerFactory(IServiceProvider parent, IDictionary factories) + { + Debug.Assert(factories != null); + _parent = parent; + _factories = new Dictionary(factories); + } + + /// + /// Add a service factory. + /// + /// service type or interface + /// function to create service instance + /// thrown if type or factory is null + /// thrown if factory has been finalized + public void AddServiceFactory(Type type, ServiceFactory factory) + { + if (type is null) throw new ArgumentNullException(nameof(type)); + if (factory is null) throw new ArgumentNullException(nameof(factory)); + if (_finalized) throw new InvalidOperationException(); + _factories.Add(type, factory); + } + + /// + /// Add a service factory. + /// + /// service type + /// function to create service instance + public void AddServiceFactory(ServiceFactory factory) => AddServiceFactory(typeof(T), factory); + + /// + /// Removes the factory for the specified type. + /// + /// service type + /// thrown if type is null + /// thrown if factory has been finalized + public void RemoveServiceFactory(Type type) + { + if (type is null) throw new ArgumentNullException(nameof(type)); + if (_finalized) throw new InvalidOperationException(); + _factories.Remove(type); + } + + /// + /// Remove a service factory. + /// + /// service type + public void RemoveServiceFactory() => RemoveServiceFactory(typeof(T)); + + /// + /// Creates a new service container/provider instance and marks this factory as finalized. No more factories can be added or removed. + /// + /// thrown if factory has not been finalized + /// service container/provider instance + public ServiceContainer Build() + { + _finalized = true; + return new ServiceContainer(_parent, _factories); + } + + /// + /// Creates a copy of the container factory with the service factories and the parent service provider. + /// + /// thrown if factory has not been finalized + /// clone + public ServiceContainerFactory Clone() + { + if (!_finalized) throw new InvalidOperationException(); + return new ServiceContainerFactory(_parent, _factories); + } + } +} diff --git a/src/Microsoft.Diagnostics.DebugServices/ServiceExportAttribute.cs b/src/Microsoft.Diagnostics.DebugServices/ServiceExportAttribute.cs new file mode 100644 index 000000000..c2df9ece3 --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices/ServiceExportAttribute.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Diagnostics.DebugServices +{ + public enum ServiceScope + { + Global, + Context, + Provider, + Target, + Module, + Thread, + Runtime, + Max + } + + /// + /// Marks classes or methods (service factories) as services + /// + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)] + public class ServiceExportAttribute : Attribute + { + /// + /// The interface or type to register the service. If null, the service type registered will be any + /// interfaces on the the class, the class itself if no interfaces or the return type of the method. + /// + public Type Type { get; set; } + + /// + /// The scope of the service (global, per-target, per-context, per-runtime, etc). + /// + public ServiceScope Scope { get; set; } + + /// + /// Default constructor. + /// + public ServiceExportAttribute() + { + } + } +} diff --git a/src/Microsoft.Diagnostics.DebugServices/ServiceImportAttribute.cs b/src/Microsoft.Diagnostics.DebugServices/ServiceImportAttribute.cs new file mode 100644 index 000000000..0c5e0e491 --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices/ServiceImportAttribute.cs @@ -0,0 +1,27 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Diagnostics.DebugServices +{ + /// + /// Marks properties or methods to import another service when a service is instantiated. + /// + [AttributeUsage(AttributeTargets.Parameter | AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method)] + public class ServiceImportAttribute : Attribute + { + /// + /// If true, the service is optional and can even up null. + /// + public bool Optional { get; set; } + + /// + /// Default constructor. + /// + public ServiceImportAttribute() + { + } + } +} diff --git a/src/Microsoft.Diagnostics.DebugServices/ServiceManagerExtensions.cs b/src/Microsoft.Diagnostics.DebugServices/ServiceManagerExtensions.cs new file mode 100644 index 000000000..79ebc830a --- /dev/null +++ b/src/Microsoft.Diagnostics.DebugServices/ServiceManagerExtensions.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; + +namespace Microsoft.Diagnostics.DebugServices +{ + public static class ServiceManagerExtensions + { + /// + /// Creates a new service container with all the registered factories for the given scope. + /// + /// service manager instance + /// global, per-target, per-runtime, etc. service type + /// parent service provider to chain to + /// IServiceContainer instance + public static ServiceContainer CreateServiceContainer(this IServiceManager serviceManager, ServiceScope scope, IServiceProvider parent) + { + ServiceContainerFactory containerFactory = serviceManager.CreateServiceContainerFactory(scope, parent); + return containerFactory.Build(); + } + } +} diff --git a/src/Microsoft.Diagnostics.DebugServices/ServiceProviderExtensions.cs b/src/Microsoft.Diagnostics.DebugServices/ServiceProviderExtensions.cs index b994d4d13..c3cfb7d95 100644 --- a/src/Microsoft.Diagnostics.DebugServices/ServiceProviderExtensions.cs +++ b/src/Microsoft.Diagnostics.DebugServices/ServiceProviderExtensions.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using System; -using System.Collections.Generic; namespace Microsoft.Diagnostics.DebugServices { @@ -14,9 +13,6 @@ namespace Microsoft.Diagnostics.DebugServices /// /// service type /// service instance or null - public static T GetService(this IServiceProvider serviceProvider) - { - return (T)serviceProvider.GetService(typeof(T)); - } + public static T GetService(this IServiceProvider serviceProvider) => (T)serviceProvider.GetService(typeof(T)); } } diff --git a/src/Microsoft.Diagnostics.DebugServices/TargetExtensions.cs b/src/Microsoft.Diagnostics.DebugServices/TargetExtensions.cs index a8beb0692..4824217a2 100644 --- a/src/Microsoft.Diagnostics.DebugServices/TargetExtensions.cs +++ b/src/Microsoft.Diagnostics.DebugServices/TargetExtensions.cs @@ -32,16 +32,5 @@ namespace Microsoft.Diagnostics.DebugServices } throw new PlatformNotSupportedException(target.OperatingSystem.ToString()); } - - /// - /// Registers an object to be disposed when target is destroyed. - /// - /// target instance - /// object to be disposed or null - /// IDisposable to unregister this event or null - public static IDisposable DisposeOnDestroy(this ITarget target, IDisposable disposable) - { - return disposable != null ? target.OnDestroyEvent.Register(() => disposable.Dispose()) : null; - } } } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/ClrMDHelper.cs b/src/Microsoft.Diagnostics.ExtensionCommands/ClrMDHelper.cs index 40b9662a5..74dcadbc7 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/ClrMDHelper.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/ClrMDHelper.cs @@ -16,7 +16,13 @@ namespace Microsoft.Diagnostics.ExtensionCommands private readonly ClrRuntime _clr; private readonly ClrHeap _heap; - public ClrMDHelper(ClrRuntime clr) + [ServiceExport(Scope = ServiceScope.Runtime)] + public static ClrMDHelper Create([ServiceImport(Optional = true)] ClrRuntime clrRuntime) + { + return clrRuntime != null ? new ClrMDHelper(clrRuntime) : null; + } + + private ClrMDHelper(ClrRuntime clr) { Debug.Assert(clr != null); _clr = clr; diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/ClrModulesCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/ClrModulesCommand.cs index 4c253cdfb..29e4e6414 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/ClrModulesCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/ClrModulesCommand.cs @@ -13,8 +13,10 @@ namespace Microsoft.Diagnostics.ExtensionCommands [Command(Name = "clrmodules", Help = "Lists the managed modules in the process.")] public class ClrModulesCommand : CommandBase { + [ServiceImport(Optional = true)] public ClrRuntime Runtime { get; set; } + [ServiceImport] public IModuleService ModuleService { get; set; } [Option(Name = "--name", Aliases = new string[] { "-n" }, Help = "RegEx filter on module name (path not included).")] diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/DumpAsyncCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/DumpAsyncCommand.cs index d09bf213f..d01136c92 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/DumpAsyncCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/DumpAsyncCommand.cs @@ -49,6 +49,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands $"Show each stack that includes an object at a specific address, and include fields: !{CommandName} --address 0x000001264adce778 --fields"; /// Gets the runtime for the process. Set by the command framework. + [ServiceImport(Optional = true)] public ClrRuntime? Runtime { get; set; } /// Gets whether to only show stacks that include the object with the specified address. diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/DumpConcurrentDictionaryCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/DumpConcurrentDictionaryCommand.cs index fc83c22c9..e23856b3e 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/DumpConcurrentDictionaryCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/DumpConcurrentDictionaryCommand.cs @@ -14,6 +14,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands [Argument(Help = "The address of a ConcurrentDictionary object.")] public string Address { get; set; } + [ServiceImport] public ClrRuntime Runtime { get; set; } public override void ExtensionInvoke() diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/DumpConcurrentQueueCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/DumpConcurrentQueueCommand.cs index ea7b0a70d..0df3ea480 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/DumpConcurrentQueueCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/DumpConcurrentQueueCommand.cs @@ -14,6 +14,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands [Argument(Help = "The address of a ConcurrentQueue object.")] public string Address { get; set; } + [ServiceImport] public ClrRuntime Runtime { get; set; } public override void ExtensionInvoke() diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/ExtensionCommandBase.cs b/src/Microsoft.Diagnostics.ExtensionCommands/ExtensionCommandBase.cs index c2f4c099d..49b07faab 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/ExtensionCommandBase.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/ExtensionCommandBase.cs @@ -11,6 +11,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands /// /// Helper bound to the current ClrRuntime that provides high level services on top of ClrMD. /// + [ServiceImport(Optional = true)] public ClrMDHelper Helper { get; set; } public override void Invoke() diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Host/ConsoleLoggingCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/Host/ConsoleLoggingCommand.cs index 221f469a1..b8764508b 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/Host/ConsoleLoggingCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/Host/ConsoleLoggingCommand.cs @@ -10,6 +10,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands [Command(Name = "logclose", DefaultOptions = "--disable", Help = "Disables console file logging.", Flags = CommandFlags.Global)] public class ConsoleLoggingCommand : CommandBase { + [ServiceImport(Optional = true)] public IConsoleFileLoggingService FileLoggingService { get; set; } [Argument(Name = "path", Help = "Log file path.")] diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Host/HelpCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/Host/HelpCommand.cs new file mode 100644 index 000000000..1445b3450 --- /dev/null +++ b/src/Microsoft.Diagnostics.ExtensionCommands/Host/HelpCommand.cs @@ -0,0 +1,30 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Microsoft.Diagnostics.DebugServices; +using System; + +namespace Microsoft.Diagnostics.ExtensionCommands +{ + [Command(Name = "help", Help = "Displays help for a command.", Flags = CommandFlags.Global)] + public class HelpCommand : CommandBase + { + [Argument(Help = "Command to find help.")] + public string Command { get; set; } + + [ServiceImport] + public ICommandService CommandService { get; set; } + + [ServiceImport] + public IServiceProvider Services { get; set; } + + public override void Invoke() + { + if (!CommandService.DisplayHelp(Command, Services)) + { + throw new NotSupportedException($"Help for {Command} not found"); + } + } + } +} diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Host/LoggingCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/Host/LoggingCommand.cs index 4732ab1bb..9b63dbe0c 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/Host/LoggingCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/Host/LoggingCommand.cs @@ -13,6 +13,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands [Command(Name = "logging", Help = "Enables/disables internal diagnostic logging.", Flags = CommandFlags.Global)] public class LoggingCommand : CommandBase { + [ServiceImport(Optional = true)] public IDiagnosticLoggingService DiagnosticLoggingService { get; set; } [Argument(Name = "path", Help = "Log file path.")] diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Host/ModulesCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/Host/ModulesCommand.cs index 74fd7a9ca..5836f55b2 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/Host/ModulesCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/Host/ModulesCommand.cs @@ -31,6 +31,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands [Option(Name = "--address", Aliases = new string[] { "-a" }, Help = "Lookup address in module list.")] public ulong? Address { get; set; } + [ServiceImport] public IModuleService ModuleService { get; set; } public override void Invoke() @@ -97,8 +98,10 @@ namespace Microsoft.Diagnostics.ExtensionCommands } } + [ServiceImport] public ITarget Target { get; set; } + [ServiceImport] public IMemoryService MemoryService { get; set; } void DisplaySegments(IModule module) diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Host/RegistersCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/Host/RegistersCommand.cs index d8d3bc023..135522aed 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/Host/RegistersCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/Host/RegistersCommand.cs @@ -10,8 +10,10 @@ namespace Microsoft.Diagnostics.ExtensionCommands [Command(Name = "registers", Aliases = new string[] { "r" }, Help = "Displays the thread's registers.")] public class RegistersCommand : CommandBase { + [ServiceImport] public IThreadService ThreadService { get; set; } + [ServiceImport] public IThread CurrentThread { get; set; } [Option(Name = "--verbose", Aliases = new string[] { "-v" }, Help = "Displays more details.")] diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Host/RuntimesCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/Host/RuntimesCommand.cs index b88dd18ef..bd5f4ab2b 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/Host/RuntimesCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/Host/RuntimesCommand.cs @@ -12,12 +12,18 @@ namespace Microsoft.Diagnostics.ExtensionCommands [Command(Name = "runtimes", Help = "Lists the runtimes in the target or changes the default runtime.")] public class RuntimesCommand : CommandBase { + [ServiceImport] public IRuntimeService RuntimeService { get; set; } + [ServiceImport] public IContextService ContextService { get; set; } + [ServiceImport] public ITarget Target { get; set; } + [Argument(Help = "Switch to the runtime by id.")] + public int? Id { get; set; } + [Option(Name = "--netfx", Aliases = new string[] { "-netfx", "-f" }, Help = "Switches to the desktop .NET Framework if exists.")] public bool NetFx { get; set; } @@ -39,22 +45,28 @@ namespace Microsoft.Diagnostics.ExtensionCommands NetCore && runtime.RuntimeType == RuntimeType.NetCore) { ContextService.SetCurrentRuntime(runtime.Id); - WriteLine("Switched to {0} runtime successfully", name); + WriteLine($"Switched to {name} runtime successfully"); return; } } - WriteLineError("The {0} runtime is not loaded", name); + WriteLineError($"The {name} runtime is not loaded"); + } + else if (Id.HasValue) + { + ContextService.SetCurrentRuntime(Id.Value); + WriteLine($"Switched to runtime #{Id.Value} successfully"); } else { - // Display the current runtime star ("*") only if there is more than one runtime + // Display the current runtime star ("*") only if there is more than one runtime and it is the current one bool displayStar = RuntimeService.EnumerateRuntimes().Count() > 1; + IRuntime currentRuntime = ContextService.GetCurrentRuntime(); foreach (IRuntime runtime in RuntimeService.EnumerateRuntimes()) { - string current = displayStar ? (runtime == ContextService.GetCurrentRuntime() ? "*" : " ") : ""; + string current = displayStar ? (runtime == currentRuntime ? "*" : " ") : ""; Write(current); - Write(runtime.ToString()); + WriteLine(runtime.ToString()); ClrInfo clrInfo = runtime.Services.GetService(); if (clrInfo is not null) { @@ -70,7 +82,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands } } - public static class Utilities + public static class CommandUtilities { public static string ToHex(this ImmutableArray array) => string.Concat(array.Select((b) => b.ToString("x2"))); } diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Host/SetClrPathCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/Host/SetClrPathCommand.cs index 1191419d5..66d1cac5d 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/Host/SetClrPathCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/Host/SetClrPathCommand.cs @@ -10,6 +10,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands [Command(Name = "setclrpath", Help = "Sets the path to load coreclr DAC/DBI files.")] public class SetClrPath: CommandBase { + [ServiceImport(Optional = true)] public IRuntime Runtime { get; set; } [Argument(Name = "path", Help = "Runtime directory path.")] diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Host/SetSymbolServerCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/Host/SetSymbolServerCommand.cs index 46484519d..28cadedcf 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/Host/SetSymbolServerCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/Host/SetSymbolServerCommand.cs @@ -18,8 +18,10 @@ namespace Microsoft.Diagnostics.ExtensionCommands Flags = CommandFlags.Global)] public class SetSymbolServerCommand : CommandBase { + [ServiceImport] public ISymbolService SymbolService { get; set; } + [ServiceImport(Optional = true)] public IModuleService ModuleService { get; set; } [Option(Name = "--ms", Aliases = new string[] { "-ms" }, Help = "Use the public Microsoft symbol server.")] @@ -89,7 +91,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands { SymbolService.AddDirectoryPath(Directory); } - if (LoadSymbols) + if (LoadSymbols && ModuleService is not null) { foreach (IModule module in ModuleService.EnumerateModules()) { diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Host/StatusCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/Host/StatusCommand.cs index cbc7414b6..eaef61634 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/Host/StatusCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/Host/StatusCommand.cs @@ -10,8 +10,10 @@ namespace Microsoft.Diagnostics.ExtensionCommands [Command(Name = "sosstatus", Help = "Displays internal status or resets the internal cached state.")] public class StatusCommand : CommandBase { + [ServiceImport] public ITarget Target { get; set; } + [ServiceImport] public ISymbolService SymbolService { get; set; } [Option(Name = "-reset", Help = "Reset all the cached internal state.")] diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/Host/ThreadsCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/Host/ThreadsCommand.cs index 9f700c292..21ab894e5 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/Host/ThreadsCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/Host/ThreadsCommand.cs @@ -18,10 +18,13 @@ namespace Microsoft.Diagnostics.ExtensionCommands [Option(Name = "--verbose", Aliases = new string[] { "-v" }, Help = "Displays more details.")] public bool Verbose { get; set; } + [ServiceImport(Optional = true)] public IThread CurrentThread { get; set; } + [ServiceImport] public IThreadService ThreadService { get; set; } + [ServiceImport] public IContextService ContextService { get; set; } public override void Invoke() diff --git a/src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacksCommand.cs b/src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacksCommand.cs index 43db291e1..16b12c6b6 100644 --- a/src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacksCommand.cs +++ b/src/Microsoft.Diagnostics.ExtensionCommands/ParallelStacksCommand.cs @@ -12,6 +12,7 @@ namespace Microsoft.Diagnostics.ExtensionCommands [Command(Name = "parallelstacks", Aliases = new string[] { "pstacks" }, Help = "Displays the merged threads stack similarly to the Visual Studio 'Parallel Stacks' panel.")] public class ParallelStacksCommand : ExtensionCommandBase { + [ServiceImport] public ClrRuntime Runtime { get; set; } [Option(Name = "--allthreads", Aliases = new string[] { "-a" }, Help = "Displays all threads per group instead of at most 4 by default.")] diff --git a/src/Microsoft.Diagnostics.Repl/HelpCommand.cs b/src/Microsoft.Diagnostics.Repl/HelpCommand.cs deleted file mode 100644 index 30f1b63cc..000000000 --- a/src/Microsoft.Diagnostics.Repl/HelpCommand.cs +++ /dev/null @@ -1,33 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using Microsoft.Diagnostics.DebugServices; -using System; - -namespace Microsoft.Diagnostics.Repl -{ - [Command(Name = "help", Help = "Displays help for a command.", Flags = CommandFlags.Global | CommandFlags.Manual)] - public class HelpCommand : CommandBase - { - [Argument(Help = "Command to find help.")] - public string Command { get; set; } - - private readonly ICommandService _commandService; - private readonly IServiceProvider _services; - - public HelpCommand(ICommandService commandService, IServiceProvider services) - { - _commandService = commandService; - _services = services; - } - - public override void Invoke() - { - if (!_commandService.DisplayHelp(Command, _services)) - { - throw new NotSupportedException($"Help for {Command} not found"); - } - } - } -} diff --git a/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDataWriter.cs b/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDataWriter.cs index 537275ce3..99a194bad 100644 --- a/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDataWriter.cs +++ b/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDataWriter.cs @@ -1,6 +1,7 @@ using Microsoft.Diagnostics.DebugServices; using System; using System.Collections.Immutable; +using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; @@ -20,19 +21,21 @@ namespace Microsoft.Diagnostics.TestHelpers public TestDataWriter() { Root = new XElement("TestData"); - Root.Add(new XElement("Version", "1.0.1")); + Root.Add(new XElement("Version", "1.0.2")); Target = new XElement("Target"); Root.Add(Target); } - public void Build(ITarget target) + public void Build(IServiceProvider services) { + ITarget target = services.GetService(); + Debug.Assert(target is not null); AddMembers(Target, typeof(ITarget), target, nameof(ITarget.Id), nameof(ITarget.GetTempDirectory)); var modulesElement = new XElement("Modules"); Target.Add(modulesElement); - var moduleService = target.Services.GetService(); + var moduleService = services.GetService(); string runtimeModuleName = target.GetPlatformModuleName("coreclr"); foreach (IModule module in moduleService.EnumerateModules()) { @@ -44,7 +47,7 @@ namespace Microsoft.Diagnostics.TestHelpers var threadsElement = new XElement("Threads"); Target.Add(threadsElement); - var threadService = target.Services.GetService(); + var threadService = services.GetService(); var registerIndexes = new int[] { threadService.InstructionPointerIndex, threadService.StackPointerIndex, threadService.FramePointerIndex }; foreach (IThread thread in threadService.EnumerateThreads()) { @@ -73,7 +76,7 @@ namespace Microsoft.Diagnostics.TestHelpers var runtimesElement = new XElement("Runtimes"); Target.Add(runtimesElement); - var runtimeService = target.Services.GetService(); + var runtimeService = services.GetService(); foreach (IRuntime runtime in runtimeService.EnumerateRuntimes()) { var runtimeElement = new XElement("Runtime"); @@ -93,7 +96,12 @@ namespace Microsoft.Diagnostics.TestHelpers private void AddModuleMembers(XElement element, IModule module, string symbolModuleName) { - AddMembers(element, typeof(IModule), module, nameof(IModule.ModuleIndex), nameof(IModule.GetPdbFileInfos), nameof(IModule.GetVersionString), nameof(IModule.GetSymbolFileName)); + AddMembers(element, typeof(IModule), module, + nameof(IModule.ModuleIndex), + nameof(IModule.GetPdbFileInfos), + nameof(IModule.GetVersionString), + nameof(IModule.GetSymbolFileName), + nameof(IModule.LoadSymbols)); if (symbolModuleName != null && IsModuleEqual(module, symbolModuleName)) { diff --git a/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDump.cs b/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDump.cs index 2df8072ee..49d93217c 100644 --- a/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDump.cs +++ b/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestDump.cs @@ -10,8 +10,8 @@ namespace Microsoft.Diagnostics.TestHelpers { public class TestDump : TestHost, IHost { - private readonly ServiceProvider _serviceProvider; - private readonly ContextService _contextService; + private readonly ServiceManager _serviceManager; + private readonly ServiceContainer _serviceContainer; private readonly SymbolService _symbolService; private DataTarget _dataTarget; private int _targetIdFactory; @@ -19,11 +19,23 @@ namespace Microsoft.Diagnostics.TestHelpers public TestDump(TestConfiguration config) : base(config) { - _serviceProvider = new ServiceProvider(); - _contextService = new ContextService(this); + _serviceManager = new ServiceManager(); + + // Register all the services and commands in the Microsoft.Diagnostics.DebugServices.Implementation assembly + _serviceManager.RegisterAssembly(typeof(Target).Assembly); + + // Loading extensions or adding service factories not allowed after this point. + _serviceManager.FinalizeServices(); + + _serviceContainer = _serviceManager.CreateServiceContainer(ServiceScope.Global, parent: null); + _serviceContainer.AddService(_serviceManager); + _serviceContainer.AddService(this); + + var contextService = new ContextService(this); + _serviceContainer.AddService(contextService); + _symbolService = new SymbolService(this); - _serviceProvider.AddService(_contextService); - _serviceProvider.AddService(_symbolService); + _serviceContainer.AddService(_symbolService); // Automatically enable symbol server support _symbolService.AddSymbolServer(msdl: true, symweb: false, timeoutInMinutes: 6, retryCount: 5); @@ -46,25 +58,13 @@ namespace Microsoft.Diagnostics.TestHelpers public IServiceEvent OnShutdownEvent { get; } = new ServiceEvent(); - HostType IHost.HostType => HostType.DotnetDump; + public IServiceEvent OnTargetCreate { get; } = new ServiceEvent(); - IServiceProvider IHost.Services => _serviceProvider; + public HostType HostType => HostType.DotnetDump; - IEnumerable IHost.EnumerateTargets() => Target != null ? new ITarget[] { Target } : Array.Empty(); + public IServiceProvider Services => _serviceContainer; - void IHost.DestroyTarget(ITarget target) - { - if (target == null) { - throw new ArgumentNullException(nameof(target)); - } - if (target == Target) - { - _contextService.ClearCurrentTarget(); - if (target is IDisposable disposable) { - disposable.Dispose(); - } - } - } + public IEnumerable EnumerateTargets() => Target != null ? new ITarget[] { Target } : Array.Empty(); #endregion } diff --git a/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestHost.cs b/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestHost.cs index 92ba06039..9bd22981a 100644 --- a/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestHost.cs +++ b/src/Microsoft.Diagnostics.TestHelpers/TestHost/TestHost.cs @@ -1,4 +1,6 @@ -using Microsoft.Diagnostics.DebugServices; +using Microsoft.Diagnostics.DebugServices; +using System; +using System.IO; namespace Microsoft.Diagnostics.TestHelpers { diff --git a/src/SOS/SOS.Extensions/ContextServiceFromDebuggerServices.cs b/src/SOS/SOS.Extensions/ContextServiceFromDebuggerServices.cs index 849d47294..c6ad8a6b7 100644 --- a/src/SOS/SOS.Extensions/ContextServiceFromDebuggerServices.cs +++ b/src/SOS/SOS.Extensions/ContextServiceFromDebuggerServices.cs @@ -23,7 +23,7 @@ namespace SOS.Extensions _debuggerServices = debuggerServices; } - public override IThread GetCurrentThread() + protected override IThread GetCurrentThread() { HResult hr = _debuggerServices.GetCurrentThreadId(out uint threadId); if (hr != HResult.S_OK) @@ -31,7 +31,10 @@ namespace SOS.Extensions Trace.TraceError("GetCurrentThreadId() FAILED {0:X8}", hr); return null; } - return ThreadService?.GetThreadFromId(threadId); + IThread currentThread = ThreadService?.GetThreadFromId(threadId); + // This call fires the context change event if the thread obtain by the host debugger differs from the current thread + base.SetCurrentThread(currentThread); + return currentThread; } public override void SetCurrentThread(IThread thread) diff --git a/src/SOS/SOS.Extensions/DebuggerServices.cs b/src/SOS/SOS.Extensions/DebuggerServices.cs index a6ebabb0c..4cc700bf3 100644 --- a/src/SOS/SOS.Extensions/DebuggerServices.cs +++ b/src/SOS/SOS.Extensions/DebuggerServices.cs @@ -325,6 +325,30 @@ namespace SOS return VTable.GetOffsetBySymbol(Self, moduleIndex, symbolPtr, out address); } } + + public HResult GetTypeId(int moduleIndex, string typeName, out ulong typeId) + { + if (string.IsNullOrEmpty(typeName)) throw new ArgumentException(nameof(typeName)); + + byte[] typeNameBytes = Encoding.ASCII.GetBytes(typeName + "\0"); + fixed (byte* typeNamePtr = typeNameBytes) + { + return VTable.GetTypeId(Self, moduleIndex, typeNamePtr, out typeId); + } + } + + public HResult GetFieldOffset(int moduleIndex, ulong typeId, string typeName, string fieldName, out uint offset) + { + if (string.IsNullOrEmpty(fieldName)) throw new ArgumentException(nameof(fieldName)); + + byte[] typeNameBytes = Encoding.ASCII.GetBytes(typeName + "\0"); + byte[] fieldNameBytes = Encoding.ASCII.GetBytes(fieldName + "\0"); + fixed (byte* typeNamePtr = typeNameBytes) + fixed (byte *fieldNamePtr = fieldNameBytes) + { + return VTable.GetFieldOffset(Self, moduleIndex, typeNamePtr, typeId, fieldNamePtr, out offset); + } + } public int GetOutputWidth() => (int)VTable.GetOutputWidth(Self); @@ -387,6 +411,8 @@ namespace SOS public readonly delegate* unmanaged[Stdcall] GetSymbolPath; public readonly delegate* unmanaged[Stdcall] GetSymbolByOffset; public readonly delegate* unmanaged[Stdcall] GetOffsetBySymbol; + public readonly delegate* unmanaged[Stdcall] GetTypeId; + public readonly delegate* unmanaged[Stdcall] GetFieldOffset; public readonly delegate* unmanaged[Stdcall] GetOutputWidth; public readonly delegate* unmanaged[Stdcall] SupportsDml; public readonly delegate* unmanaged[Stdcall] OutputDmlString; diff --git a/src/SOS/SOS.Extensions/HostServices.cs b/src/SOS/SOS.Extensions/HostServices.cs index 744d54c59..dd1fc44d8 100644 --- a/src/SOS/SOS.Extensions/HostServices.cs +++ b/src/SOS/SOS.Extensions/HostServices.cs @@ -37,14 +37,14 @@ namespace SOS.Extensions internal DebuggerServices DebuggerServices { get; private set; } - private readonly ServiceProvider _serviceProvider; + private readonly ServiceManager _serviceManager; private readonly CommandService _commandService; private readonly SymbolService _symbolService; private readonly HostWrapper _hostWrapper; + private ServiceContainer _serviceContainer; private ContextServiceFromDebuggerServices _contextService; private int _targetIdFactory; private ITarget _target; - private TargetWrapper _targetWrapper; /// /// Enable the assembly resolver to get the right versions in the same directory as this assembly. @@ -106,20 +106,17 @@ namespace SOS.Extensions private HostServices() { - _serviceProvider = new ServiceProvider(); - _serviceProvider.AddService(DiagnosticLoggingService.Instance); - _symbolService = new SymbolService(this); - _symbolService.DefaultTimeout = DefaultTimeout; - _symbolService.DefaultRetryCount = DefaultRetryCount; + _serviceManager = new ServiceManager(); _commandService = new CommandService(RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ">!ext" : null); - _commandService.AddCommands(new Assembly[] { typeof(HostServices).Assembly }); - _commandService.AddCommands(new Assembly[] { typeof(ClrMDHelper).Assembly }); + _serviceManager.NotifyExtensionLoad.Register(_commandService.AddCommands); - _serviceProvider.AddService(this); - _serviceProvider.AddService(_commandService); - _serviceProvider.AddService(_symbolService); + _symbolService = new SymbolService(this) + { + DefaultTimeout = DefaultTimeout, + DefaultRetryCount = DefaultRetryCount + }; - _hostWrapper = new HostWrapper(this, () => _targetWrapper); + _hostWrapper = new HostWrapper(this); _hostWrapper.ServiceWrapper.AddServiceWrapper(IID_IHostServices, this); VTableBuilder builder = AddInterface(IID_IHostServices, validate: false); @@ -141,39 +138,20 @@ namespace SOS.Extensions { Trace.TraceInformation("HostServices.Destroy"); _hostWrapper.ServiceWrapper.RemoveServiceWrapper(IID_IHostServices); - _hostWrapper.Release(); + _hostWrapper.ReleaseWithCheck(); } #region IHost public IServiceEvent OnShutdownEvent { get; } = new ServiceEvent(); + public IServiceEvent OnTargetCreate { get; } = new ServiceEvent(); + public HostType HostType => RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? HostType.DbgEng : HostType.Lldb; - IServiceProvider IHost.Services => _serviceProvider; + public IServiceProvider Services => _serviceContainer; - IEnumerable IHost.EnumerateTargets() => _target != null ? new ITarget[] { _target } : Array.Empty(); - - public void DestroyTarget(ITarget target) - { - if (target == null) { - throw new ArgumentNullException(nameof(target)); - } - Trace.TraceInformation("IHost.DestroyTarget #{0}", target.Id); - if (target == _target) - { - _target = null; - if (_targetWrapper != null) - { - _targetWrapper.Release(); - _targetWrapper = null; - } - _contextService.ClearCurrentTarget(); - if (target is IDisposable disposable) { - disposable.Dispose(); - } - } - } + public IEnumerable EnumerateTargets() => _target != null ? new ITarget[] { _target } : Array.Empty(); #endregion @@ -206,31 +184,50 @@ namespace SOS.Extensions Trace.TraceError(ex.Message); return HResult.E_NOINTERFACE; } - try - { - var remoteMemoryService = new RemoteMemoryService(iunk); - _serviceProvider.AddService(remoteMemoryService); - } - catch (InvalidCastException) - { - } HResult hr; try { var consoleService = new ConsoleServiceFromDebuggerServices(DebuggerServices); var fileLoggingConsoleService = new FileLoggingConsoleService(consoleService); DiagnosticLoggingService.Instance.SetConsole(consoleService, fileLoggingConsoleService); - _serviceProvider.AddService(fileLoggingConsoleService); - _serviceProvider.AddService(fileLoggingConsoleService); + + // Don't register everything in the SOSHost assembly; just the wrappers + _serviceManager.RegisterExportedServices(typeof(TargetWrapper)); + _serviceManager.RegisterExportedServices(typeof(RuntimeWrapper)); + + // Register all the services and commands in the Microsoft.Diagnostics.DebugServices.Implementation assembly + _serviceManager.RegisterAssembly(typeof(Target).Assembly); + + // Register all the services and commands in the SOS.Extensions (this) assembly + _serviceManager.RegisterAssembly(typeof(HostServices).Assembly); + + // Register all the services and commands in the Microsoft.Diagnostics.ExtensionCommands assembly + _serviceManager.RegisterAssembly(typeof(ClrMDHelper).Assembly); + + // Display any extension assembly loads on console + _serviceManager.NotifyExtensionLoad.Register((Assembly assembly) => fileLoggingConsoleService.WriteLine($"Loading extension {assembly.Location}")); + + // Load any extra extensions in the search path + _serviceManager.LoadExtensions(); + + // Loading extensions or adding service factories not allowed after this point. + _serviceManager.FinalizeServices(); + + // Add all the global services to the global service container + _serviceContainer = _serviceManager.CreateServiceContainer(ServiceScope.Global, parent: null); + _serviceContainer.AddService(_serviceManager); + _serviceContainer.AddService(this); + _serviceContainer.AddService(_commandService); + _serviceContainer.AddService(_symbolService); + _serviceContainer.AddService(fileLoggingConsoleService); + _serviceContainer.AddService(fileLoggingConsoleService); + _serviceContainer.AddService(DiagnosticLoggingService.Instance); _contextService = new ContextServiceFromDebuggerServices(this, DebuggerServices); - _serviceProvider.AddService(_contextService); - _serviceProvider.AddServiceFactory(() => new ThreadUnwindServiceFromDebuggerServices(DebuggerServices)); + _serviceContainer.AddService(_contextService); - _contextService.ServiceProvider.AddServiceFactory(() => { - ClrRuntime clrRuntime = _contextService.Services.GetService(); - return clrRuntime != null ? new ClrMDHelper(clrRuntime) : null; - }); + var threadUnwindService = new ThreadUnwindServiceFromDebuggerServices(DebuggerServices); + _serviceContainer.AddService(threadUnwindService); // Add each extension command to the native debugger foreach ((string name, string help, IEnumerable aliases) in _commandService.Commands) @@ -247,6 +244,17 @@ namespace SOS.Extensions Trace.TraceError(ex.ToString()); return HResult.E_FAIL; } + try + { + var remoteMemoryService = new RemoteMemoryService(iunk); + // This service needs another reference since it is implemented as part of IDebuggerServices and gets + // disposed in Uninitialize() below by the DisposeServices call. + remoteMemoryService.AddRef(); + _serviceContainer.AddService(remoteMemoryService); + } + catch (InvalidCastException) + { + } hr = DebuggerServices.GetSymbolPath(out string symbolPath); if (hr == HResult.S_OK) { @@ -271,10 +279,9 @@ namespace SOS.Extensions } try { - _target = new TargetFromDebuggerServices(DebuggerServices, this, _targetIdFactory++); - _contextService.SetCurrentTarget(_target); - _targetWrapper = new TargetWrapper(_contextService.Services); - _targetWrapper.ServiceWrapper.AddServiceWrapper(SymbolServiceWrapper.IID_ISymbolService, () => new SymbolServiceWrapper(_symbolService, _target.Services.GetService())); + var target = new TargetFromDebuggerServices(DebuggerServices, this, _targetIdFactory++); + _contextService.SetCurrentTarget(target); + _target = target; } catch (Exception ex) { @@ -317,10 +324,8 @@ namespace SOS.Extensions Trace.TraceInformation("HostServices.DestroyTarget #{0}", _target != null ? _target.Id : ""); try { - if (_target != null) - { - DestroyTarget(_target); - } + _target?.Destroy(); + _target = null; } catch (Exception ex) { @@ -355,6 +360,10 @@ namespace SOS.Extensions return HResult.S_OK; } } + catch (CommandNotSupportedException) + { + return HResult.E_NOTIMPL; + } catch (Exception ex) { Trace.TraceError(ex.ToString()); @@ -373,6 +382,10 @@ namespace SOS.Extensions return HResult.E_INVALIDARG; } } + catch (CommandNotSupportedException) + { + return HResult.E_NOTIMPL; + } catch (Exception ex) { Trace.TraceError(ex.ToString()); @@ -389,22 +402,24 @@ namespace SOS.Extensions { DestroyTarget(self); - if (DebuggerServices != null) - { - // This turns off any logging to console now that debugger services will be released and the console service will no longer work. - DiagnosticLoggingService.Instance.SetConsole(consoleService: null, fileLoggingService: null); - DebuggerServices.Release(); - DebuggerServices = null; - } - // Send shutdown event on exit OnShutdownEvent.Fire(); - // Release the host services wrapper - Release(); + // Dispose of the global services which RemoteMemoryService but not host services (this) + _serviceContainer.DisposeServices(); + + // This turns off any logging to console now that debugger services will be released and the console service will no longer work. + DiagnosticLoggingService.Instance.SetConsole(consoleService: null, fileLoggingService: null); + + // Release the debugger services instance + DebuggerServices?.ReleaseWithCheck(); + DebuggerServices = null; // Clear HostService instance Instance = null; + + // Release the host services wrapper + this.ReleaseWithCheck(); } catch (Exception ex) { diff --git a/src/SOS/SOS.Extensions/MemoryServiceFromDebuggerServices.cs b/src/SOS/SOS.Extensions/MemoryServiceFromDebuggerServices.cs index 299749e59..4fb30b73e 100644 --- a/src/SOS/SOS.Extensions/MemoryServiceFromDebuggerServices.cs +++ b/src/SOS/SOS.Extensions/MemoryServiceFromDebuggerServices.cs @@ -15,7 +15,6 @@ namespace SOS.Extensions /// internal class MemoryServiceFromDebuggerServices : IMemoryService { - private readonly ITarget _target; private readonly DebuggerServices _debuggerServices; /// @@ -27,7 +26,6 @@ namespace SOS.Extensions { Debug.Assert(target != null); Debug.Assert(debuggerServices != null); - _target = target; _debuggerServices = debuggerServices; switch (target.Architecture) diff --git a/src/SOS/SOS.Extensions/ModuleServiceFromDebuggerServices.cs b/src/SOS/SOS.Extensions/ModuleServiceFromDebuggerServices.cs index fc1e61b15..04e9f807c 100644 --- a/src/SOS/SOS.Extensions/ModuleServiceFromDebuggerServices.cs +++ b/src/SOS/SOS.Extensions/ModuleServiceFromDebuggerServices.cs @@ -18,6 +18,53 @@ namespace SOS.Extensions /// internal class ModuleServiceFromDebuggerServices : ModuleService { + class FieldFromDebuggerServices : IField + { + public FieldFromDebuggerServices(IType type, string fieldName, uint offset) + { + Type = type; + Name = fieldName; + Offset = offset; + } + public IType Type { get; } + + public string Name { get; } + + public uint Offset { get; } + } + + class TypeFromDebuggerServices : IType + { + private ModuleServiceFromDebuggerServices _moduleService; + private ulong _typeId; + + public TypeFromDebuggerServices(ModuleServiceFromDebuggerServices moduleService, IModule module, ulong typeId, string typeName) + { + _moduleService = moduleService; + _typeId = typeId; + Module = module; + Name = typeName; + } + + public IModule Module { get; } + + public string Name { get; } + + public List Fields => throw new NotImplementedException(); + + public bool TryGetField(string fieldName, out IField field) + { + HResult hr = _moduleService._debuggerServices.GetFieldOffset(Module.ModuleIndex, _typeId, Name, fieldName, out uint offset); + if (hr != HResult.S_OK) + { + field = null; + return false; + } + field = new FieldFromDebuggerServices(this, fieldName, offset); + return true; + } + } + class ModuleFromDebuggerServices : Module, IModuleSymbols { // This is what dbgeng/IDebuggerServices returns for non-PE modules that don't have a timestamp @@ -34,8 +81,8 @@ namespace SOS.Extensions ulong imageBase, ulong imageSize, uint indexFileSize, - uint indexTimeStamp) - : base(moduleService.Target) + uint indexTimeStamp) + : base(moduleService.Services) { _moduleService = moduleService; ModuleIndex = moduleIndex; @@ -45,22 +92,16 @@ namespace SOS.Extensions IndexFileSize = indexTimeStamp == InvalidTimeStamp ? null : indexFileSize; IndexTimeStamp = indexTimeStamp == InvalidTimeStamp ? null : indexTimeStamp; - ServiceProvider.AddService(this); + _serviceContainer.AddService(this); } - #region IModule - - public override int ModuleIndex { get; } - - public override string FileName { get; } - - public override ulong ImageBase { get; } - - public override ulong ImageSize { get; } - - public override uint? IndexFileSize { get; } + public override void Dispose() + { + _serviceContainer.RemoveService(typeof(IModuleSymbols)); + base.Dispose(); + } - public override uint? IndexTimeStamp { get; } + #region IModule public override Version GetVersionData() { @@ -77,10 +118,7 @@ namespace SOS.Extensions } else { - if (_moduleService.Target.OperatingSystem != OSPlatform.Windows) - { - _version = GetVersionInner(); - } + _version = GetVersionInner(); } } return _version; @@ -93,10 +131,7 @@ namespace SOS.Extensions HResult hr = _moduleService._debuggerServices.GetModuleVersionString(ModuleIndex, out _versionString); if (!hr.IsOK) { - if (_moduleService.Target.OperatingSystem != OSPlatform.Windows && !IsPEImage) - { - _versionString = _moduleService.GetVersionString(this); - } + _versionString = GetVersionStringInner(); } } return _versionString; @@ -126,6 +161,18 @@ namespace SOS.Extensions return _moduleService._debuggerServices.GetOffsetBySymbol(ModuleIndex, name, out address).IsOK; } + bool IModuleSymbols.TryGetType(string typeName, out IType type) + { + HResult hr = _moduleService._debuggerServices.GetTypeId(ModuleIndex, typeName, out ulong typeId); + if (hr != HResult.S_OK) + { + type = null; + return false; + } + type = new TypeFromDebuggerServices(_moduleService, this, typeId, typeName); + return true; + } + #endregion protected override bool TryGetSymbolAddressInner(string name, out ulong address) @@ -138,8 +185,8 @@ namespace SOS.Extensions private readonly DebuggerServices _debuggerServices; - internal ModuleServiceFromDebuggerServices(ITarget target, IMemoryService rawMemoryService, DebuggerServices debuggerServices) - : base(target, rawMemoryService) + internal ModuleServiceFromDebuggerServices(IServiceProvider services, DebuggerServices debuggerServices) + : base(services) { Debug.Assert(debuggerServices != null); _debuggerServices = debuggerServices; diff --git a/src/SOS/SOS.Extensions/TargetFromFromDebuggerServices.cs b/src/SOS/SOS.Extensions/TargetFromFromDebuggerServices.cs index b57f491fd..069b81c9f 100644 --- a/src/SOS/SOS.Extensions/TargetFromFromDebuggerServices.cs +++ b/src/SOS/SOS.Extensions/TargetFromFromDebuggerServices.cs @@ -19,7 +19,7 @@ namespace SOS.Extensions internal class TargetFromDebuggerServices : Target { /// - /// Create a target instance from IDataReader + /// Build a target instance from IDataReader /// internal TargetFromDebuggerServices(DebuggerServices debuggerServices, IHost host, int id) : base(host, id, dumpPath: null) @@ -72,22 +72,29 @@ namespace SOS.Extensions } // Add the thread, memory, and module services - IMemoryService rawMemoryService = new MemoryServiceFromDebuggerServices(this, debuggerServices); - ServiceProvider.AddServiceFactory(() => new ModuleServiceFromDebuggerServices(this, rawMemoryService, debuggerServices)); - ServiceProvider.AddServiceFactory(() => new ThreadServiceFromDebuggerServices(this, debuggerServices)); - ServiceProvider.AddServiceFactory(() => { + _serviceContainerFactory.AddServiceFactory((services) => new ModuleServiceFromDebuggerServices(services, debuggerServices)); + _serviceContainerFactory.AddServiceFactory((services) => new ThreadServiceFromDebuggerServices(services, debuggerServices)); + _serviceContainerFactory.AddServiceFactory((_) => { Debug.Assert(Host.HostType != HostType.DotnetDump); - IMemoryService memoryService = rawMemoryService; + IMemoryService memoryService = new MemoryServiceFromDebuggerServices(this, debuggerServices); if (IsDump && Host.HostType == HostType.Lldb) { + ServiceContainerFactory clone = _serviceContainerFactory.Clone(); + clone.RemoveServiceFactory(); + + // lldb doesn't map managed modules into the address space + memoryService = new ImageMappingMemoryService(clone.Build(), memoryService, managed: true); + // This is a special memory service that maps the managed assemblies' metadata into the address // space. The lldb debugger returns zero's (instead of failing the memory read) for missing pages // in core dumps that older (< 5.0) createdumps generate so it needs this special metadata mapping // memory service. dotnet-dump needs this logic for clrstack -i (uses ICorDebug data targets). - return new MetadataMappingMemoryService(this, memoryService); + memoryService = new MetadataMappingMemoryService(clone.Build(), memoryService); } return memoryService; }); + + Finished(); } } } diff --git a/src/SOS/SOS.Extensions/ThreadServiceFromDebuggerServices.cs b/src/SOS/SOS.Extensions/ThreadServiceFromDebuggerServices.cs index 63dc87364..d26ee2a46 100644 --- a/src/SOS/SOS.Extensions/ThreadServiceFromDebuggerServices.cs +++ b/src/SOS/SOS.Extensions/ThreadServiceFromDebuggerServices.cs @@ -5,6 +5,7 @@ using Microsoft.Diagnostics.DebugServices; using Microsoft.Diagnostics.DebugServices.Implementation; using Microsoft.Diagnostics.Runtime.Utilities; +using System; using System.Collections.Generic; using System.Diagnostics; @@ -17,8 +18,8 @@ namespace SOS.Extensions { private readonly DebuggerServices _debuggerServices; - internal ThreadServiceFromDebuggerServices(ITarget target, DebuggerServices debuggerServices) - : base(target) + internal ThreadServiceFromDebuggerServices(IServiceProvider services, DebuggerServices debuggerServices) + : base(services) { Debug.Assert(debuggerServices != null); _debuggerServices = debuggerServices; diff --git a/src/SOS/SOS.Hosting/Commands/SOSCommand.cs b/src/SOS/SOS.Hosting/Commands/SOSCommand.cs index b196c5cdd..1fbb30dd5 100644 --- a/src/SOS/SOS.Hosting/Commands/SOSCommand.cs +++ b/src/SOS/SOS.Hosting/Commands/SOSCommand.cs @@ -74,6 +74,7 @@ namespace SOS.Hosting [Argument(Name = "arguments", Help = "Arguments to SOS command.")] public string[] Arguments { get; set; } + [ServiceImport] public SOSHost SOSHost { get; set; } public override void Invoke() diff --git a/src/SOS/SOS.Hosting/CorDebugDataTargetWrapper.cs b/src/SOS/SOS.Hosting/CorDebugDataTargetWrapper.cs index 058cdb4df..8153ece03 100644 --- a/src/SOS/SOS.Hosting/CorDebugDataTargetWrapper.cs +++ b/src/SOS/SOS.Hosting/CorDebugDataTargetWrapper.cs @@ -26,10 +26,11 @@ namespace SOS.Hosting public IntPtr ICorDebugDataTarget { get; } - public CorDebugDataTargetWrapper(IServiceProvider services) + public CorDebugDataTargetWrapper(IServiceProvider services, IRuntime runtime) { Debug.Assert(services != null); - _target = services.GetService(); + Debug.Assert(runtime != null); + _target = runtime.Target; _symbolService = services.GetService(); _memoryService = services.GetService(); _threadService = services.GetService(); diff --git a/src/SOS/SOS.Hosting/DataTargetWrapper.cs b/src/SOS/SOS.Hosting/DataTargetWrapper.cs index a5a9d4992..86cbafb5d 100644 --- a/src/SOS/SOS.Hosting/DataTargetWrapper.cs +++ b/src/SOS/SOS.Hosting/DataTargetWrapper.cs @@ -23,8 +23,8 @@ namespace SOS.Hosting // For ClrMD's magic hand shake private const ulong MagicCallbackConstant = 0x43; - private readonly IServiceProvider _services; - private readonly ulong _runtimeBaseAddress; + private readonly IRuntime _runtime; + private readonly IContextService _contextService; private readonly ISymbolService _symbolService; private readonly IMemoryService _memoryService; private readonly IThreadService _threadService; @@ -39,8 +39,8 @@ namespace SOS.Hosting { Debug.Assert(services != null); Debug.Assert(runtime != null); - _services = services; - _runtimeBaseAddress = runtime.RuntimeModule.ImageBase; + _runtime = runtime; + _contextService = services.GetService(); _symbolService = services.GetService(); _memoryService = services.GetService(); _threadService = services.GetService(); @@ -105,11 +105,8 @@ namespace SOS.Hosting IntPtr self, out IMAGE_FILE_MACHINE machineType) { - ITarget target = _services.GetService(); - if (target == null) { - machineType = IMAGE_FILE_MACHINE.UNKNOWN; - return HResult.E_FAIL; - } + ITarget target = _runtime.Target; + Debug.Assert(target != null); machineType = target.Architecture switch { Architecture.X64 => IMAGE_FILE_MACHINE.AMD64, @@ -206,7 +203,7 @@ namespace SOS.Hosting IntPtr self, out uint threadId) { - uint? id = _services.GetService()?.ThreadId; + uint? id = _contextService.GetCurrentThread()?.ThreadId; if (id.HasValue) { threadId = id.Value; @@ -352,7 +349,7 @@ namespace SOS.Hosting IntPtr self, out ulong address) { - address = _runtimeBaseAddress; + address = _runtime.RuntimeModule.ImageBase; return HResult.S_OK; } diff --git a/src/SOS/SOS.Hosting/HostWrapper.cs b/src/SOS/SOS.Hosting/HostWrapper.cs index 5b9a2c17d..3058a01b6 100644 --- a/src/SOS/SOS.Hosting/HostWrapper.cs +++ b/src/SOS/SOS.Hosting/HostWrapper.cs @@ -5,6 +5,7 @@ using Microsoft.Diagnostics.DebugServices; using Microsoft.Diagnostics.Runtime.Utilities; using System; +using System.Diagnostics; using System.Runtime.InteropServices; namespace SOS.Hosting @@ -14,16 +15,14 @@ namespace SOS.Hosting private static readonly Guid IID_IHost = new Guid("E0CD8534-A88B-40D7-91BA-1B4C925761E9"); private readonly IHost _host; - private readonly Func _getTarget; public ServiceWrapper ServiceWrapper { get; } = new ServiceWrapper(); public IntPtr IHost { get; } - public HostWrapper(IHost host, Func getTarget) + public HostWrapper(IHost host) { _host = host; - _getTarget = getTarget; VTableBuilder builder = AddInterface(IID_IHost, validate: false); builder.AddMethod(new GetHostTypeDelegate(GetHostType)); @@ -34,7 +33,11 @@ namespace SOS.Hosting AddRef(); } - protected override void Destroy() => ServiceWrapper.Dispose(); + protected override void Destroy() + { + Trace.TraceInformation("HostWrapper.Destroy"); + ServiceWrapper.Dispose(); + } #region IHost @@ -50,7 +53,9 @@ namespace SOS.Hosting /// S_OK private int GetCurrentTarget(IntPtr self, out IntPtr targetWrapper) { - TargetWrapper wrapper = _getTarget(); + IContextService contextService = _host.Services.GetService(); + ITarget target = contextService.GetCurrentTarget(); + TargetWrapper wrapper = target?.Services.GetService(); if (wrapper == null) { targetWrapper = IntPtr.Zero; diff --git a/src/SOS/SOS.Hosting/LLDBServices.cs b/src/SOS/SOS.Hosting/LLDBServices.cs index d10050fc5..9a57b0395 100644 --- a/src/SOS/SOS.Hosting/LLDBServices.cs +++ b/src/SOS/SOS.Hosting/LLDBServices.cs @@ -97,7 +97,7 @@ namespace SOS.Hosting string GetCoreClrDirectory( IntPtr self) { - IRuntime currentRuntime = _soshost.Services.GetService(); + IRuntime currentRuntime = _soshost.ContextService.GetCurrentRuntime(); if (currentRuntime is not null) { return Path.GetDirectoryName(currentRuntime.RuntimeModule.FileName); } diff --git a/src/SOS/SOS.Hosting/RuntimeWrapper.cs b/src/SOS/SOS.Hosting/RuntimeWrapper.cs index 4306f83d4..8e28b3c39 100644 --- a/src/SOS/SOS.Hosting/RuntimeWrapper.cs +++ b/src/SOS/SOS.Hosting/RuntimeWrapper.cs @@ -14,7 +14,8 @@ using System.Text; namespace SOS.Hosting { - public sealed unsafe class RuntimeWrapper : COMCallableIUnknown + [ServiceExport(Scope = ServiceScope.Runtime)] + public sealed unsafe class RuntimeWrapper : COMCallableIUnknown, IDisposable { /// /// The runtime OS and type. Must match IRuntime::RuntimeConfiguration in runtime.h. @@ -115,6 +116,12 @@ namespace SOS.Hosting AddRef(); } + void IDisposable.Dispose() + { + Trace.TraceInformation("RuntimeWrapper.Dispose"); + this.ReleaseWithCheck(); + } + protected override void Destroy() { Trace.TraceInformation("RuntimeWrapper.Destroy"); @@ -137,13 +144,13 @@ namespace SOS.Hosting // TODO: there is a better way to flush _corDebugProcess with ICorDebugProcess4::ProcessStateChanged(FLUSH_ALL) if (_corDebugProcess != IntPtr.Zero) { - COMHelper.Release(_corDebugProcess); + ComWrapper.ReleaseWithCheck(_corDebugProcess); _corDebugProcess = IntPtr.Zero; } // TODO: there is a better way to flush _clrDataProcess with ICLRDataProcess::Flush() if (_clrDataProcess != IntPtr.Zero) { - COMHelper.Release(_clrDataProcess); + ComWrapper.ReleaseWithCheck(_clrDataProcess); _clrDataProcess = IntPtr.Zero; } } @@ -209,8 +216,16 @@ namespace SOS.Hosting if (ppClrDataProcess == null) { return HResult.E_INVALIDARG; } - if (_clrDataProcess == IntPtr.Zero) { - _clrDataProcess = CreateClrDataProcess(); + if (_clrDataProcess == IntPtr.Zero) + { + try + { + _clrDataProcess = CreateClrDataProcess(); + } + catch (Exception) + { + return HResult.E_NOINTERFACE; + } } *ppClrDataProcess = _clrDataProcess; if (*ppClrDataProcess == IntPtr.Zero) { @@ -317,7 +332,7 @@ namespace SOS.Hosting } finally { - dataTarget.Release(); + dataTarget.ReleaseWithCheck(); } } @@ -350,7 +365,7 @@ namespace SOS.Hosting Build = 0, Revision = 0, }; - var dataTarget = new CorDebugDataTargetWrapper(_services); + var dataTarget = new CorDebugDataTargetWrapper(_services, _runtime); ulong clrInstanceId = _runtime.RuntimeModule.ImageBase; int hresult = 0; try @@ -446,7 +461,7 @@ namespace SOS.Hosting } finally { - dataTarget.Release(); + dataTarget.ReleaseWithCheck(); } } diff --git a/src/SOS/SOS.Hosting/SOSHost.cs b/src/SOS/SOS.Hosting/SOSHost.cs index 18cc0ae38..b290afdb8 100644 --- a/src/SOS/SOS.Hosting/SOSHost.cs +++ b/src/SOS/SOS.Hosting/SOSHost.cs @@ -19,42 +19,45 @@ namespace SOS.Hosting /// /// Helper code to hosting the native SOS code /// + [ServiceExport(Scope = ServiceScope.Target)] public sealed class SOSHost : IDisposable { // This is what dbgeng/IDebuggerServices returns for non-PE modules that don't have a timestamp internal const uint InvalidTimeStamp = 0xFFFFFFFE; internal const uint InvalidChecksum = 0xFFFFFFFF; - internal readonly IServiceProvider Services; internal readonly ITarget Target; - internal readonly TargetWrapper TargetWrapper; + internal readonly IMemoryService MemoryService; + +#pragma warning disable CS0649 + [ServiceImport] internal readonly IConsoleService ConsoleService; + + [ServiceImport] + internal readonly IContextService ContextService; + + [ServiceImport] internal readonly IModuleService ModuleService; + + [ServiceImport] internal readonly IThreadService ThreadService; - internal readonly IMemoryService MemoryService; + + [ServiceImport] private readonly SOSLibrary _sosLibrary; +#pragma warning restore + private readonly IntPtr _interface; private readonly ulong _ignoreAddressBitsMask; private bool _disposed; /// - /// Create an instance of the hosting class. Has the lifetime of the target. Depends on the - /// context service for the current thread and runtime. + /// Create an instance of the hosting class. Has the lifetime of the target. /// - /// service provider - public SOSHost(IServiceProvider services) - { - Services = services; - Target = services.GetService() ?? throw new DiagnosticsException("No target"); - TargetWrapper = new TargetWrapper(services); - Target.DisposeOnDestroy(this); - ConsoleService = services.GetService(); - ModuleService = services.GetService(); - ThreadService = services.GetService(); - MemoryService = services.GetService(); - TargetWrapper.ServiceWrapper.AddServiceWrapper(SymbolServiceWrapper.IID_ISymbolService, () => new SymbolServiceWrapper(services.GetService(), MemoryService)); - _ignoreAddressBitsMask = MemoryService.SignExtensionMask(); - _sosLibrary = services.GetService(); + public SOSHost(ITarget target, IMemoryService memoryService) + { + Target = target ?? throw new DiagnosticsException("No target"); + MemoryService = memoryService; + _ignoreAddressBitsMask = memoryService.SignExtensionMask(); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { @@ -74,8 +77,7 @@ namespace SOS.Hosting if (!_disposed) { _disposed = true; - TargetWrapper.Release(); - COMHelper.Release(_interface); + ComWrapper.ReleaseWithCheck(_interface); } } @@ -577,7 +579,7 @@ namespace SOS.Hosting IntPtr context, uint contextSize) { - IThread thread = Services.GetService(); + IThread thread = ContextService.GetCurrentThread(); if (thread is not null) { return GetThreadContextBySystemId(self, thread.ThreadId, 0, contextSize, context); @@ -654,7 +656,7 @@ namespace SOS.Hosting IntPtr self, out uint id) { - IThread thread = Services.GetService(); + IThread thread = ContextService.GetCurrentThread(); if (thread is not null) { return GetThreadIdBySystemId(self, thread.ThreadId, out id); } @@ -668,11 +670,7 @@ namespace SOS.Hosting { try { - var contextService = Services.GetService(); - if (contextService is null) { - return HResult.E_FAIL; - } - contextService.SetCurrentThread(ThreadService.GetThreadFromIndex(unchecked((int)id)).ThreadId); + ContextService.SetCurrentThread(ThreadService.GetThreadFromIndex(unchecked((int)id)).ThreadId); } catch (DiagnosticsException) { @@ -685,7 +683,7 @@ namespace SOS.Hosting IntPtr self, out uint sysId) { - IThread thread = Services.GetService(); + IThread thread = ContextService.GetCurrentThread(); if (thread is not null) { sysId = thread.ThreadId; @@ -749,7 +747,7 @@ namespace SOS.Hosting IntPtr self, ulong* offset) { - IThread thread = Services.GetService(); + IThread thread = ContextService.GetCurrentThread(); if (thread is not null) { try @@ -848,7 +846,7 @@ namespace SOS.Hosting int index, out ulong value) { - IThread thread = Services.GetService(); + IThread thread = ContextService.GetCurrentThread(); if (thread is not null) { if (thread.TryGetRegisterValue(index, out value)) @@ -895,4 +893,37 @@ namespace SOS.Hosting } } } + + public static class ComWrapper + { + /// + /// Asserts the the reference count doesn't go below 0. + /// + /// wrapper instance + public static void ReleaseWithCheck(this COMCallableIUnknown comCallable) + { + int count = comCallable.Release(); + Debug.Assert(count >= 0); + } + + /// + /// Asserts the the reference count doesn't go below 0. + /// + /// wrapper instance + public static void ReleaseWithCheck(this CallableCOMWrapper callableCOM) + { + int count = callableCOM.Release(); + Debug.Assert(count >= 0); + } + + /// + /// Asserts the the reference count doesn't go below 0. + /// + /// wrapper instance + public static void ReleaseWithCheck(IntPtr punk) + { + int count = COMHelper.Release(punk); + Debug.Assert(count >= 0); + } + } } diff --git a/src/SOS/SOS.Hosting/SOSLibrary.cs b/src/SOS/SOS.Hosting/SOSLibrary.cs index a43683f7c..d96bde6a5 100644 --- a/src/SOS/SOS.Hosting/SOSLibrary.cs +++ b/src/SOS/SOS.Hosting/SOSLibrary.cs @@ -15,7 +15,7 @@ namespace SOS.Hosting /// /// Helper code to load and initialize SOS /// - public sealed class SOSLibrary + public sealed class SOSLibrary : IDisposable { [UnmanagedFunctionPointer(CallingConvention.Winapi)] private delegate int SOSCommandDelegate( @@ -33,7 +33,6 @@ namespace SOS.Hosting private const string SOSInitialize = "SOSInitializeByHost"; private const string SOSUninitialize = "SOSUninitializeByHost"; - private readonly IContextService _contextService; private readonly HostWrapper _hostWrapper; private IntPtr _sosLibrary = IntPtr.Zero; @@ -42,6 +41,7 @@ namespace SOS.Hosting /// public string SOSPath { get; set; } + [ServiceExport(Scope = ServiceScope.Global)] public static SOSLibrary Create(IHost host) { SOSLibrary sosLibrary = null; @@ -53,13 +53,8 @@ namespace SOS.Hosting catch { sosLibrary.Uninitialize(); - sosLibrary = null; throw; } - host.OnShutdownEvent.Register(() => { - sosLibrary.Uninitialize(); - sosLibrary = null; - }); return sosLibrary; } @@ -69,14 +64,13 @@ namespace SOS.Hosting /// target instance private SOSLibrary(IHost host) { - _contextService = host.Services.GetService(); - string rid = InstallHelper.GetRid(); SOSPath = Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), rid); - - _hostWrapper = new HostWrapper(host, () => GetSOSHost()?.TargetWrapper); + _hostWrapper = new HostWrapper(host); } + void IDisposable.Dispose() => Uninitialize(); + /// /// Loads and initializes the SOS module. /// @@ -143,7 +137,7 @@ namespace SOS.Hosting Microsoft.Diagnostics.Runtime.DataTarget.PlatformFunctions.FreeLibrary(_sosLibrary); _sosLibrary = IntPtr.Zero; } - _hostWrapper.Release(); + _hostWrapper.ReleaseWithCheck(); } /// @@ -162,12 +156,14 @@ namespace SOS.Hosting throw new DiagnosticsException($"SOS command not found: {command}"); } int result = commandFunc(client, arguments ?? ""); + if (result == HResult.E_NOTIMPL) + { + throw new CommandNotSupportedException($"SOS command not found: {command}"); + } if (result != HResult.S_OK) { Trace.TraceError($"SOS command FAILED 0x{result:X8}"); } } - - private SOSHost GetSOSHost() => _contextService.Services.GetService(); } } diff --git a/src/SOS/SOS.Hosting/ServiceWrapper.cs b/src/SOS/SOS.Hosting/ServiceWrapper.cs index 43ac85e9a..517459b6c 100644 --- a/src/SOS/SOS.Hosting/ServiceWrapper.cs +++ b/src/SOS/SOS.Hosting/ServiceWrapper.cs @@ -23,7 +23,7 @@ namespace SOS.Hosting Trace.TraceInformation("ServiceWrapper.Dispose"); foreach (var wrapper in _wrappers.Values) { - wrapper.Release(); + wrapper.ReleaseWithCheck(); } _wrappers.Clear(); } @@ -95,7 +95,6 @@ namespace SOS.Hosting if (wrapper == null) { return HResult.E_NOINTERFACE; } - wrapper.AddRef(); return COMHelper.QueryInterface(wrapper.IUnknownObject, guid, out ptr); } } diff --git a/src/SOS/SOS.Hosting/SymbolServiceWrapper.cs b/src/SOS/SOS.Hosting/SymbolServiceWrapper.cs index ca1292ae1..86899321b 100644 --- a/src/SOS/SOS.Hosting/SymbolServiceWrapper.cs +++ b/src/SOS/SOS.Hosting/SymbolServiceWrapper.cs @@ -62,7 +62,6 @@ namespace SOS.Hosting _symbolService = symbolService; _memoryService = memoryService; _ignoreAddressBitsMask = memoryService.SignExtensionMask(); - Debug.Assert(_symbolService != null); VTableBuilder builder = AddInterface(IID_ISymbolService, validate: false); builder.AddMethod(new ParseSymbolPathDelegate(ParseSymbolPath)); diff --git a/src/SOS/SOS.Hosting/TargetWrapper.cs b/src/SOS/SOS.Hosting/TargetWrapper.cs index 6a5531814..f96663b0a 100644 --- a/src/SOS/SOS.Hosting/TargetWrapper.cs +++ b/src/SOS/SOS.Hosting/TargetWrapper.cs @@ -5,13 +5,13 @@ using Microsoft.Diagnostics.DebugServices; using Microsoft.Diagnostics.Runtime.Utilities; using System; -using System.Collections.Generic; using System.Diagnostics; using System.Runtime.InteropServices; namespace SOS.Hosting { - public sealed unsafe class TargetWrapper : COMCallableIUnknown + [ServiceExport(Scope = ServiceScope.Target)] + public sealed unsafe class TargetWrapper : COMCallableIUnknown, IDisposable { // Must be the same as ITarget::OperatingSystem enum OperatingSystem @@ -28,14 +28,18 @@ namespace SOS.Hosting public IntPtr ITarget { get; } - private readonly IServiceProvider _services; private readonly ITarget _target; - private readonly Dictionary _wrappers = new Dictionary(); + private readonly IContextService _contextService; - public TargetWrapper(IServiceProvider services) + public TargetWrapper(ITarget target, IContextService contextService, ISymbolService symbolService, IMemoryService memoryService) { - _services = services; - _target = services.GetService() ?? throw new DiagnosticsException("No target"); + Debug.Assert(target != null); + Debug.Assert(contextService != null); + Debug.Assert(symbolService != null); + _target = target; + _contextService = contextService; + + ServiceWrapper.AddServiceWrapper(SymbolServiceWrapper.IID_ISymbolService, () => new SymbolServiceWrapper(symbolService, memoryService)); VTableBuilder builder = AddInterface(IID_ITarget, validate: false); @@ -50,15 +54,17 @@ namespace SOS.Hosting AddRef(); } + void IDisposable.Dispose() + { + Trace.TraceInformation("TargetWrapper.Dispose"); + this.ReleaseWithCheck(); + } + protected override void Destroy() { Trace.TraceInformation("TargetWrapper.Destroy"); + ServiceWrapper.RemoveServiceWrapper(SymbolServiceWrapper.IID_ISymbolService); ServiceWrapper.Dispose(); - foreach (RuntimeWrapper wrapper in _wrappers.Values) - { - wrapper.Release(); - } - _wrappers.Clear(); } private OperatingSystem GetOperatingSystem( @@ -89,14 +95,13 @@ namespace SOS.Hosting if (ppRuntime == null) { return HResult.E_INVALIDARG; } - IRuntime runtime = _services.GetService(); - if (runtime == null) { + IRuntime runtime = _contextService.GetCurrentRuntime(); + if (runtime is null) { return HResult.E_NOINTERFACE; } - if (!_wrappers.TryGetValue(runtime, out RuntimeWrapper wrapper)) - { - wrapper = new RuntimeWrapper(_services, runtime); - _wrappers.Add(runtime, wrapper); + RuntimeWrapper wrapper = runtime.Services.GetService(); + if (wrapper is null) { + return HResult.E_NOINTERFACE; } *ppRuntime = wrapper.IRuntime; return HResult.S_OK; diff --git a/src/SOS/SOS.UnitTests/Scripts/DualRuntimes.script b/src/SOS/SOS.UnitTests/Scripts/DualRuntimes.script index 90d7ca3fb..7bc2e2c49 100644 --- a/src/SOS/SOS.UnitTests/Scripts/DualRuntimes.script +++ b/src/SOS/SOS.UnitTests/Scripts/DualRuntimes.script @@ -62,7 +62,7 @@ VERIFY:\s*Total\s+\s+objects\s+ # SOSCOMMAND:SOSStatus -VERIFY:.*\.NET Core .*runtime at.*\s+ +VERIFY:.*\.NET Core .*runtime.*at.*\s+ IFDEF:TRIAGE_DUMP SOSCOMMAND:setclrpath %DESKTOP_RUNTIME_PATH% diff --git a/src/SOS/SOS.UnitTests/Scripts/StackAndOtherTests.script b/src/SOS/SOS.UnitTests/Scripts/StackAndOtherTests.script index a05ce1cfe..d0ea28430 100644 --- a/src/SOS/SOS.UnitTests/Scripts/StackAndOtherTests.script +++ b/src/SOS/SOS.UnitTests/Scripts/StackAndOtherTests.script @@ -74,10 +74,10 @@ VERIFY:\s+\s+\s+SymbolTestApp\.Program\.Main\(.*\)\s+\[(?i:.*[\\ SOSCOMMAND:runtimes !IFDEF:DESKTOP -VERIFY:.*\.NET Core .*runtime at.*\s+ +VERIFY:.*\.NET Core .*runtime.*at.*\s+ ENDIF:DESKTOP IFDEF:DESKTOP -VERIFY:.*\Desktop .NET Framework.*runtime at.*\s+ +VERIFY:.*\Desktop .NET Framework.*runtime.*at.*\s+ ENDIF:DESKTOP SOSCOMMAND:SOSStatus diff --git a/src/SOS/Strike/dbgengservices.cpp b/src/SOS/Strike/dbgengservices.cpp index f87400013..540e1cc9f 100644 --- a/src/SOS/Strike/dbgengservices.cpp +++ b/src/SOS/Strike/dbgengservices.cpp @@ -439,6 +439,44 @@ DbgEngServices::GetOffsetBySymbol( return m_symbols->GetOffsetByName(symbolName.c_str(), offset); } +HRESULT +DbgEngServices::GetTypeId( + ULONG moduleIndex, + PCSTR typeName, + PULONG64 typeId) +{ + ULONG64 moduleBase; + HRESULT hr = m_symbols->GetModuleByIndex(moduleIndex, &moduleBase); + if (FAILED(hr)) { + return hr; + } + + ULONG typeIdTemp = 0; + hr = m_symbols->GetTypeId(moduleBase, typeName, &typeIdTemp); + // typeId is 64 bit for compatibility across platforms, can't pass it in to GetTypeId + // that expects a 32 bit ULONG. + *typeId = typeIdTemp; + + return hr; +} + +HRESULT +DbgEngServices::GetFieldOffset( + ULONG moduleIndex, + PCSTR typeName, // Unused on windbg + ULONG64 typeId, + PCSTR fieldName, + PULONG offset) +{ + ULONG64 moduleBase; + HRESULT hr = m_symbols->GetModuleByIndex(moduleIndex, &moduleBase); + if (FAILED(hr)) { + return hr; + } + + return m_symbols->GetFieldOffset(moduleBase, (ULONG)typeId, fieldName, offset); +} + ULONG DbgEngServices::GetOutputWidth() { diff --git a/src/SOS/Strike/dbgengservices.h b/src/SOS/Strike/dbgengservices.h index 377025d49..e51a3604a 100644 --- a/src/SOS/Strike/dbgengservices.h +++ b/src/SOS/Strike/dbgengservices.h @@ -182,6 +182,18 @@ public: ULONG moduleIndex, PCSTR name, PULONG64 offset); + + HRESULT STDMETHODCALLTYPE GetTypeId( + ULONG moduleIndex, + PCSTR typeName, + PULONG64 typeId); + + HRESULT STDMETHODCALLTYPE GetFieldOffset( + ULONG moduleIndex, + PCSTR typeName, + ULONG64 typeId, + PCSTR fieldName, + PULONG offset); ULONG STDMETHODCALLTYPE GetOutputWidth(); diff --git a/src/SOS/Strike/symbols.cpp b/src/SOS/Strike/symbols.cpp index e155bbb4b..a01ebcdf1 100644 --- a/src/SOS/Strike/symbols.cpp +++ b/src/SOS/Strike/symbols.cpp @@ -473,9 +473,7 @@ HRESULT SymbolReader::LoadSymbolsForPortablePDB(__in_z WCHAR* pModuleName, ___in { return E_NOINTERFACE; } - m_symbolReaderHandle = GetSymbolService()->LoadSymbolsForModule( - pModuleName, isFileLayout, peAddress, (int)peSize, inMemoryPdbAddress, (int)inMemoryPdbSize); - + m_symbolReaderHandle = symbolService->LoadSymbolsForModule(pModuleName, isFileLayout, peAddress, (int)peSize, inMemoryPdbAddress, (int)inMemoryPdbSize); if (m_symbolReaderHandle == 0) { return E_FAIL; diff --git a/src/SOS/extensions/extensions.cpp b/src/SOS/extensions/extensions.cpp index e478ab092..1b4abe193 100644 --- a/src/SOS/extensions/extensions.cpp +++ b/src/SOS/extensions/extensions.cpp @@ -133,7 +133,7 @@ ISymbolService* Extensions::GetSymbolService() { if (m_pSymbolService == nullptr) { - ITarget* target = GetTarget(); + ITarget* target = GetTarget(); if (target != nullptr) { target->GetService(__uuidof(ISymbolService), (void**)&m_pSymbolService); diff --git a/src/SOS/inc/debuggerservices.h b/src/SOS/inc/debuggerservices.h index 66f2aab5c..74e37fab6 100644 --- a/src/SOS/inc/debuggerservices.h +++ b/src/SOS/inc/debuggerservices.h @@ -144,6 +144,18 @@ public: PCSTR name, PULONG64 offset) = 0; + virtual HRESULT STDMETHODCALLTYPE GetTypeId( + ULONG moduleIndex, + PCSTR typeName, + PULONG64 typeId) = 0; + + virtual HRESULT STDMETHODCALLTYPE GetFieldOffset( + ULONG moduleIndex, + PCSTR typeName, + ULONG64 typeId, + PCSTR fieldName, + PULONG offset) = 0; + virtual ULONG STDMETHODCALLTYPE GetOutputWidth() = 0; virtual HRESULT STDMETHODCALLTYPE SupportsDml(PULONG supported) = 0; diff --git a/src/SOS/lldbplugin/services.cpp b/src/SOS/lldbplugin/services.cpp index 61f797e5b..31b4a4ab2 100644 --- a/src/SOS/lldbplugin/services.cpp +++ b/src/SOS/lldbplugin/services.cpp @@ -950,16 +950,16 @@ LLDBServices::GetNameByOffset( goto exit; } - address = target.ResolveLoadAddress(offset); - if (!address.IsValid()) - { - hr = E_INVALIDARG; - goto exit; - } - // If module index is invalid, add module name to symbol if (moduleIndex == DEBUG_ANY_ID) { + address = target.ResolveLoadAddress(offset); + if (!address.IsValid()) + { + hr = E_INVALIDARG; + goto exit; + } + module = address.GetModule(); if (!module.IsValid()) { @@ -982,6 +982,13 @@ LLDBServices::GetNameByOffset( goto exit; } + address = module.ResolveFileAddress(offset); + if (!address.IsValid()) + { + hr = E_INVALIDARG; + goto exit; + } + if (module != address.GetModule()) { hr = E_INVALIDARG; @@ -2242,6 +2249,7 @@ LLDBServices::OutputString( file = m_debugger.GetOutputFileHandle(); } fputs(str, file); + fflush(file); } HRESULT @@ -2390,6 +2398,131 @@ exit: return hr; } +HRESULT +LLDBServices::GetTypeId( + ULONG moduleIndex, + PCSTR typeName, + PULONG64 typeId) +{ + HRESULT hr = S_OK; + + lldb::SBTarget target; + lldb::SBModule module; + lldb::SBTypeList typeList; + lldb::SBType type; + + if (typeId == nullptr) + { + hr = E_INVALIDARG; + goto exit; + } + + *typeId = -1; + + target = m_debugger.GetSelectedTarget(); + if (!target.IsValid()) + { + hr = E_FAIL; + goto exit; + } + + module = target.GetModuleAtIndex(moduleIndex); + if (!module.IsValid()) + { + hr = E_INVALIDARG; + goto exit; + } + + type = module.FindFirstType(typeName); + if (!type.IsValid()) + { + hr = E_INVALIDARG; + goto exit; + } + +exit: + return hr; +} + +HRESULT +LLDBServices::GetFieldOffset( + ULONG moduleIndex, + PCSTR typeName, + ULONG64 typeId, // unused on lldb + PCSTR fieldName, + PULONG offset) +{ + HRESULT hr = S_OK; + + lldb::SBTarget target; + lldb::SBModule module; + lldb::SBTypeList typeList; + lldb::SBType type; + lldb::SBTypeMember field; + lldb::SBTypeMember baseClassTypeMember; + lldb::SBType baseClass; + std::vector baseClassTypes; + + if (offset == nullptr) + { + hr = E_INVALIDARG; + goto exit; + } + + *offset = -1; + + target = m_debugger.GetSelectedTarget(); + if (!target.IsValid()) + { + hr = E_FAIL; + goto exit; + } + + module = target.GetModuleAtIndex(moduleIndex); + if (!module.IsValid()) + { + hr = E_INVALIDARG; + goto exit; + } + + type = module.FindFirstType(typeName); + if (!type.IsValid()) + { + hr = E_INVALIDARG; + goto exit; + } + + // lldb only returns the information about the specific class you requested, not any base + // classes. So we have to do a DFS to find the field we care about. + baseClassTypes.push_back(type); + while (baseClassTypes.size() > 0) + { + type = baseClassTypes.back(); + baseClassTypes.pop_back(); + + for (int fieldIndex = 0; fieldIndex < type.GetNumberOfFields(); ++fieldIndex) + { + field = type.GetFieldAtIndex(fieldIndex); + if (strcmp(fieldName, field.GetName()) == 0) + { + *offset = field.GetOffsetInBytes(); + goto exit; + } + } + + for (int baseClassIndex = 0; baseClassIndex < type.GetNumberOfDirectBaseClasses(); ++baseClassIndex) + { + baseClass = type.GetDirectBaseClassAtIndex(baseClassIndex).GetType(); + baseClassTypes.push_back(baseClass); + } + } + + hr = E_INVALIDARG; + +exit: + return hr; +} + ULONG LLDBServices::GetOutputWidth() { diff --git a/src/SOS/lldbplugin/services.h b/src/SOS/lldbplugin/services.h index d741ed51e..18d580d7d 100644 --- a/src/SOS/lldbplugin/services.h +++ b/src/SOS/lldbplugin/services.h @@ -395,6 +395,18 @@ public: ULONG moduleIndex, PCSTR name, PULONG64 offset); + + HRESULT STDMETHODCALLTYPE GetTypeId( + ULONG moduleIndex, + PCSTR typeName, + PULONG64 typeId); + + HRESULT STDMETHODCALLTYPE GetFieldOffset( + ULONG moduleIndex, + PCSTR typeName, + ULONG64 typeId, + PCSTR fieldName, + PULONG offset); ULONG STDMETHODCALLTYPE GetOutputWidth(); diff --git a/src/Tools/dotnet-dump/Analyzer.cs b/src/Tools/dotnet-dump/Analyzer.cs index 6071b5ac2..d155aff69 100644 --- a/src/Tools/dotnet-dump/Analyzer.cs +++ b/src/Tools/dotnet-dump/Analyzer.cs @@ -22,47 +22,33 @@ namespace Microsoft.Diagnostics.Tools.Dump { public class Analyzer : IHost { - private readonly ServiceProvider _serviceProvider; + private readonly ServiceManager _serviceManager; private readonly ConsoleService _consoleService; private readonly FileLoggingConsoleService _fileLoggingConsoleService; private readonly CommandService _commandService; - private readonly SymbolService _symbolService; - private readonly ContextService _contextService; + private readonly List _targets = new(); + private ServiceContainer _serviceContainer; private int _targetIdFactory; - private Target _target; public Analyzer() { DiagnosticLoggingService.Initialize(); - _serviceProvider = new ServiceProvider(); + _serviceManager = new ServiceManager(); _consoleService = new ConsoleService(); - _fileLoggingConsoleService = new FileLoggingConsoleService(_consoleService); - _commandService = new CommandService(); - _symbolService = new SymbolService(this); - _contextService = new ContextService(this); + _fileLoggingConsoleService = new FileLoggingConsoleService(_consoleService); DiagnosticLoggingService.Instance.SetConsole(_fileLoggingConsoleService, _fileLoggingConsoleService); - _serviceProvider.AddService(this); - _serviceProvider.AddService(_fileLoggingConsoleService); - _serviceProvider.AddService(_fileLoggingConsoleService); - _serviceProvider.AddService(DiagnosticLoggingService.Instance); - _serviceProvider.AddService(_commandService); - _serviceProvider.AddService(_symbolService); - _serviceProvider.AddService(_contextService); - _serviceProvider.AddServiceFactory(() => SOSLibrary.Create(this)); - - _contextService.ServiceProvider.AddServiceFactory(() => { - ClrRuntime clrRuntime = _contextService.Services.GetService(); - return clrRuntime != null ? new ClrMDHelper(clrRuntime) : null; + _commandService = new CommandService(); + _serviceManager.NotifyExtensionLoad.Register(_commandService.AddCommands); + + // Add and remove targets from the host + OnTargetCreate.Register((target) => { + _targets.Add(target); + target.OnDestroyEvent.Register(() => { + _targets.Remove(target); + }); }); - - _commandService.AddCommands(new Assembly[] { typeof(Analyzer).Assembly }); - _commandService.AddCommands(new Assembly[] { typeof(ClrMDHelper).Assembly }); - _commandService.AddCommands(new Assembly[] { typeof(SOSHost).Assembly }); - _commandService.AddCommands(typeof(HelpCommand), (services) => new HelpCommand(_commandService, services)); - _commandService.AddCommands(typeof(ExitCommand), (services) => new ExitCommand(_consoleService.Stop)); - _commandService.AddCommands(typeof(SOSCommand), (services) => new SOSCommand(_commandService, services)); } public Task Analyze(FileInfo dump_path, string[] command) @@ -70,49 +56,89 @@ namespace Microsoft.Diagnostics.Tools.Dump _fileLoggingConsoleService.WriteLine($"Loading core dump: {dump_path} ..."); // Attempt to load the persisted command history - string dotnetHome = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".dotnet"); - string historyFileName = Path.Combine(dotnetHome, "dotnet-dump.history"); + string historyFileName = null; try { + historyFileName = Path.Combine(Utilities.GetDotNetHomeDirectory(), "dotnet-dump.history"); string[] history = File.ReadAllLines(historyFileName); _consoleService.AddCommandHistory(history); } catch (Exception ex) when (ex is IOException || + ex is ArgumentNullException || ex is UnauthorizedAccessException || ex is NotSupportedException || ex is SecurityException) { } + // Register all the services and commands in the Microsoft.Diagnostics.DebugServices.Implementation assembly + _serviceManager.RegisterAssembly(typeof(Target).Assembly); + + // Register all the services and commands in the dotnet-dump (this) assembly + _serviceManager.RegisterAssembly(typeof(Analyzer).Assembly); + + // Register all the services and commands in the SOS.Hosting assembly + _serviceManager.RegisterAssembly(typeof(SOSHost).Assembly); + + // Register all the services and commands in the Microsoft.Diagnostics.ExtensionCommands assembly + _serviceManager.RegisterAssembly(typeof(ClrMDHelper).Assembly); + + // Add the specially handled exit command + _commandService.AddCommands(typeof(ExitCommand), (services) => new ExitCommand(_consoleService.Stop)); + + // Add "sos" command manually + _commandService.AddCommands(typeof(SOSCommand), (services) => new SOSCommand(_commandService, services)); + + // Display any extension assembly loads on console + _serviceManager.NotifyExtensionLoad.Register((Assembly assembly) => _fileLoggingConsoleService.WriteLine($"Loading extension {assembly.Location}")); + // Load any extra extensions - LoadExtensions(); + _serviceManager.LoadExtensions(); + + // Loading extensions or adding service factories not allowed after this point. + _serviceManager.FinalizeServices(); + + // Add all the global services to the global service container + _serviceContainer = _serviceManager.CreateServiceContainer(ServiceScope.Global, parent: null); + _serviceContainer.AddService(_serviceManager); + _serviceContainer.AddService(this); + _serviceContainer.AddService(_fileLoggingConsoleService); + _serviceContainer.AddService(_fileLoggingConsoleService); + _serviceContainer.AddService(DiagnosticLoggingService.Instance); + _serviceContainer.AddService(_commandService); + + var symbolService = new SymbolService(this); + _serviceContainer.AddService(symbolService); + + var contextService = new ContextService(this); + _serviceContainer.AddService(contextService); try { using DataTarget dataTarget = DataTarget.LoadDump(dump_path.FullName); OSPlatform targetPlatform = dataTarget.DataReader.TargetPlatform; - if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || dataTarget.DataReader.EnumerateModules().Any((module) => Path.GetExtension(module.FileName) == ".dylib")) + if (targetPlatform != OSPlatform.OSX && + (RuntimeInformation.IsOSPlatform(OSPlatform.OSX) || + dataTarget.DataReader.EnumerateModules().Any((module) => Path.GetExtension(module.FileName) == ".dylib"))) { targetPlatform = OSPlatform.OSX; } - _target = new TargetFromDataReader(dataTarget.DataReader, targetPlatform, this, _targetIdFactory++, dump_path.FullName); - _contextService.SetCurrentTarget(_target); - - _target.ServiceProvider.AddServiceFactory(() => new SOSHost(_contextService.Services)); + var target = new TargetFromDataReader(dataTarget.DataReader, targetPlatform, this, _targetIdFactory++, dump_path.FullName); + contextService.SetCurrentTarget(target); // Automatically enable symbol server support, default cache and search for symbols in the dump directory - _symbolService.AddSymbolServer(msdl: true, symweb: false, retryCount: 3); - _symbolService.AddCachePath(_symbolService.DefaultSymbolCache); - _symbolService.AddDirectoryPath(Path.GetDirectoryName(dump_path.FullName)); + symbolService.AddSymbolServer(msdl: true, symweb: false, retryCount: 3); + symbolService.AddCachePath(symbolService.DefaultSymbolCache); + symbolService.AddDirectoryPath(Path.GetDirectoryName(dump_path.FullName)); // Run the commands from the dotnet-dump command line if (command != null) { foreach (string cmd in command) { - _commandService.Execute(cmd, _contextService.Services); + _commandService.Execute(cmd, contextService.Services); if (_consoleService.Shutdown) { break; @@ -128,7 +154,7 @@ namespace Microsoft.Diagnostics.Tools.Dump _consoleService.Start((string prompt, string commandLine, CancellationToken cancellation) => { _fileLoggingConsoleService.WriteLine("{0}{1}", prompt, commandLine); - _commandService.Execute(commandLine, _contextService.Services); + _commandService.Execute(commandLine, contextService.Services); }); } } @@ -147,24 +173,33 @@ namespace Microsoft.Diagnostics.Tools.Dump } finally { - if (_target != null) + foreach (ITarget target in _targets.ToArray()) { - DestroyTarget(_target); + target.Destroy(); } + _targets.Clear(); + // Persist the current command history - try - { - File.WriteAllLines(historyFileName, _consoleService.GetCommandHistory()); - } - catch (Exception ex) when - (ex is IOException || - ex is UnauthorizedAccessException || - ex is NotSupportedException || - ex is SecurityException) + if (historyFileName != null) { + try + { + File.WriteAllLines(historyFileName, _consoleService.GetCommandHistory()); + } + catch (Exception ex) when + (ex is IOException || + ex is UnauthorizedAccessException || + ex is NotSupportedException || + ex is SecurityException) + { + } } + // Send shutdown event on exit OnShutdownEvent.Fire(); + + // Dispose of the global services + _serviceContainer.DisposeServices(); } return Task.FromResult(0); } @@ -173,69 +208,14 @@ namespace Microsoft.Diagnostics.Tools.Dump public IServiceEvent OnShutdownEvent { get; } = new ServiceEvent(); - HostType IHost.HostType => HostType.DotnetDump; + public IServiceEvent OnTargetCreate { get; } = new ServiceEvent(); - IServiceProvider IHost.Services => _serviceProvider; + public HostType HostType => HostType.DotnetDump; - IEnumerable IHost.EnumerateTargets() => _target != null ? new ITarget[] { _target } : Array.Empty(); + public IServiceProvider Services => _serviceContainer; - public void DestroyTarget(ITarget target) - { - if (target == null) - { - throw new ArgumentNullException(nameof(target)); - } - if (target == _target) - { - _target = null; - _contextService.ClearCurrentTarget(); - if (target is IDisposable disposable) - { - disposable.Dispose(); - } - } - } + public IEnumerable EnumerateTargets() => _targets.ToArray(); #endregion - - /// - /// Load any extra extensions in the search path - /// - /// Used to add the commands - private void LoadExtensions() - { - string diagnosticExtensions = Environment.GetEnvironmentVariable("DOTNET_DIAGNOSTIC_EXTENSIONS"); - if (!string.IsNullOrEmpty(diagnosticExtensions)) - { - string[] paths = diagnosticExtensions.Split(new char[] { ';' }, StringSplitOptions.RemoveEmptyEntries); - foreach (string extensionPath in paths) - { - LoadExtension(extensionPath); - } - } - } - - /// - /// Load any extra extensions in the search path - /// - /// Used to add the commands - /// Extension assembly path - private void LoadExtension(string extensionPath) - { - Assembly assembly = null; - try - { - assembly = Assembly.LoadFrom(extensionPath); - } - catch (Exception ex) when (ex is IOException || ex is ArgumentException || ex is BadImageFormatException || ex is System.Security.SecurityException) - { - _fileLoggingConsoleService.WriteLineError($"Extension load {extensionPath} FAILED {ex.Message}"); - } - if (assembly is not null) - { - _commandService.AddCommands(assembly); - _fileLoggingConsoleService.WriteLine($"Extension loaded {extensionPath}"); - } - } } } diff --git a/src/Tools/dotnet-dump/Commands/ReadMemoryCommand.cs b/src/Tools/dotnet-dump/Commands/ReadMemoryCommand.cs index 016dbfe0d..11b0a7cc1 100644 --- a/src/Tools/dotnet-dump/Commands/ReadMemoryCommand.cs +++ b/src/Tools/dotnet-dump/Commands/ReadMemoryCommand.cs @@ -22,21 +22,17 @@ namespace Microsoft.Diagnostics.Tools.Dump [Argument(Name = "address", Help = "Address to dump.")] public string AddressValue { - get => Address?.ToString(); - set => Address = ParseAddress(value); + get => _address?.ToString(); + set => _address = ParseAddress(value); } - public ulong? Address { get; set; } - [Option(Name = "--end", Aliases = new string[] { "-e" }, Help = "Ending address to dump.")] public string EndAddressValue { - get => EndAddress?.ToString(); - set => EndAddress = ParseAddress(value); + get => _endAddress?.ToString(); + set => _endAddress = ParseAddress(value); } - public ulong? EndAddress { get; set; } - // **************************************************************************************** // The following arguments are static so they are remembered for the "d" command. This // allows a command like "db
" to be followed by "d" and the next block is dumped @@ -71,16 +67,21 @@ namespace Microsoft.Diagnostics.Tools.Dump [Option(Name = "--show-address", Help = "Display the addresses of data found.")] public bool ShowAddress { get; set; } = true; + [ServiceImport] public IMemoryService MemoryService { get; set; } + [ServiceImport] public ITarget Target { get; set; } + private ulong? _address; + private ulong? _endAddress; + private static ulong _lastAddress; public override void Invoke() { - if (Address.HasValue) { - _lastAddress = Address.Value; + if (_address.HasValue) { + _lastAddress = _address.Value; } int length = Length; @@ -100,11 +101,11 @@ namespace Microsoft.Diagnostics.Tools.Dump } Length = length; - if (EndAddress.HasValue) { - if (EndAddress.Value <= _lastAddress) { + if (_endAddress.HasValue) { + if (_endAddress.Value <= _lastAddress) { throw new ArgumentException("Cannot dump a negative range"); } - int range = (int)(EndAddress.Value - _lastAddress); + int range = (int)(_endAddress.Value - _lastAddress); Count = range / length; } diff --git a/src/Tools/dotnet-dump/Commands/SOSCommand.cs b/src/Tools/dotnet-dump/Commands/SOSCommand.cs index 3edaf7125..030ef11bb 100644 --- a/src/Tools/dotnet-dump/Commands/SOSCommand.cs +++ b/src/Tools/dotnet-dump/Commands/SOSCommand.cs @@ -41,20 +41,24 @@ namespace Microsoft.Diagnostics.Tools.Dump } if (_commandService.IsCommand(commandName)) { - _commandService.Execute(commandLine, _services); + try + { + _commandService.Execute(commandLine, _services); + return; + } + catch (CommandNotSupportedException) + { + } } - else + if (_sosHost is null) { + _sosHost = _services.GetService(); if (_sosHost is null) { - _sosHost = _services.GetService(); - if (_sosHost is null) - { - throw new DiagnosticsException($"'{commandName}' command not found"); - } + throw new DiagnosticsException($"'{commandName}' command not found"); } - _sosHost.ExecuteCommand(commandLine); } + _sosHost.ExecuteCommand(commandLine); } } } diff --git a/src/tests/DbgShim.UnitTests/DbgShim.UnitTests.csproj b/src/tests/DbgShim.UnitTests/DbgShim.UnitTests.csproj index 9db24ce0f..e8fe6766f 100644 --- a/src/tests/DbgShim.UnitTests/DbgShim.UnitTests.csproj +++ b/src/tests/DbgShim.UnitTests/DbgShim.UnitTests.csproj @@ -4,7 +4,7 @@ net6.0 true $(OutputPath)$(TargetFramework)\Debugger.Tests.Common.txt - 1.0.257801 + 1.0.351101 diff --git a/src/tests/DbgShim.UnitTests/DbgShimTests.cs b/src/tests/DbgShim.UnitTests/DbgShimTests.cs index 925403a31..b1d21c86c 100644 --- a/src/tests/DbgShim.UnitTests/DbgShimTests.cs +++ b/src/tests/DbgShim.UnitTests/DbgShimTests.cs @@ -274,7 +274,7 @@ namespace Microsoft.Diagnostics IRuntimeService runtimeService = target.Services.GetService(); IRuntime runtime = runtimeService.EnumerateRuntimes().Single(); - CorDebugDataTargetWrapper dataTarget = new(target.Services); + CorDebugDataTargetWrapper dataTarget = new(target.Services, runtime); LibraryProviderWrapper libraryProvider = new(target.OperatingSystem, runtime.RuntimeModule.BuildId, runtime.GetDbiFilePath(), runtime.GetDacFilePath()); ClrDebuggingVersion maxDebuggerSupportedVersion = new() { diff --git a/src/tests/DbgShim.UnitTests/LibraryProviderWrapper.cs b/src/tests/DbgShim.UnitTests/LibraryProviderWrapper.cs index 4d92f048b..e85221afc 100644 --- a/src/tests/DbgShim.UnitTests/LibraryProviderWrapper.cs +++ b/src/tests/DbgShim.UnitTests/LibraryProviderWrapper.cs @@ -7,6 +7,8 @@ using Microsoft.Diagnostics.DebugServices.Implementation; using Microsoft.Diagnostics.Runtime; using Microsoft.Diagnostics.Runtime.Utilities; using Microsoft.FileFormats; +using Microsoft.FileFormats.ELF; +using Microsoft.FileFormats.MachO; using Microsoft.FileFormats.PE; using Microsoft.SymbolStore; using Microsoft.SymbolStore.KeyGenerators; @@ -396,19 +398,19 @@ namespace SOS.Hosting { if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) { - using ELFModule elfModule = ELFModule.OpenFile(filePath); - if (elfModule is not null) + using ELFFile elfFile = Utilities.OpenELFFile(filePath); + if (elfFile is not null) { - return elfModule.BuildID.ToImmutableArray(); + return elfFile.BuildID.ToImmutableArray(); } throw new ArgumentException($"TestBuildId {filePath} not valid ELF file"); } else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) { - using MachOModule machOModule = MachOModule.OpenFile(filePath); - if (machOModule is not null) + using MachOFile machOFile = Utilities.OpenMachOFile(filePath); + if (machOFile is not null) { - return machOModule.Uuid.ToImmutableArray(); + return machOFile.Uuid.ToImmutableArray(); } throw new ArgumentException($"TestBuildId {filePath} not valid MachO file"); } @@ -456,14 +458,14 @@ namespace SOS.Hosting IServiceEvent IHost.OnShutdownEvent => throw new NotImplementedException(); + IServiceEvent IHost.OnTargetCreate => throw new NotImplementedException(); + HostType IHost.HostType => HostType.DotnetDump; IServiceProvider IHost.Services => throw new NotImplementedException(); IEnumerable IHost.EnumerateTargets() => throw new NotImplementedException(); - void IHost.DestroyTarget(ITarget target) => throw new NotImplementedException(); - #endregion #region ICLRDebuggingLibraryProvider* delegates diff --git a/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/Microsoft.Diagnostics.DebugServices.UnitTests.csproj b/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/Microsoft.Diagnostics.DebugServices.UnitTests.csproj index d825e0296..98e50dbba 100644 --- a/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/Microsoft.Diagnostics.DebugServices.UnitTests.csproj +++ b/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/Microsoft.Diagnostics.DebugServices.UnitTests.csproj @@ -3,7 +3,7 @@ net6.0 $(OutputPath)$(TargetFramework)\Debugger.Tests.Common.txt - 1.0.257801 + 1.0.351101 true diff --git a/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/RunTests.cs b/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/RunTests.cs index 8749b1cea..8ec4976a5 100644 --- a/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/RunTests.cs +++ b/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/RunTests.cs @@ -16,6 +16,7 @@ namespace Microsoft.Diagnostics.DebugServices.UnitTests [Command(Name = "runtests", Help = "Runs the debug services xunit tests.")] public class RunTestsCommand : CommandBase, ITestOutputHelper { + [ServiceImport] public ITarget Target { get; set; } [Argument(Help = "Test data xml file path.")] diff --git a/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/SymbolServiceTests.cs b/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/SymbolServiceTests.cs index 21e0f0040..bbbefd47e 100644 --- a/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/SymbolServiceTests.cs +++ b/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/SymbolServiceTests.cs @@ -97,13 +97,13 @@ namespace Microsoft.Diagnostics.DebugServices.UnitTests public IServiceEvent OnShutdownEvent { get; } = new ServiceEvent(); - HostType IHost.HostType => HostType.DotnetDump; + public IServiceEvent OnTargetCreate { get; } = new ServiceEvent(); - IServiceProvider IHost.Services => throw new NotImplementedException(); + public HostType HostType => HostType.DotnetDump; - IEnumerable IHost.EnumerateTargets() => throw new NotImplementedException(); + public IServiceProvider Services => throw new NotImplementedException(); - void IHost.DestroyTarget(ITarget target) => throw new NotImplementedException(); + public IEnumerable EnumerateTargets() => throw new NotImplementedException(); #endregion } diff --git a/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/WriteTestData.cs b/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/WriteTestData.cs index 3d3722e18..e4851789e 100644 --- a/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/WriteTestData.cs +++ b/src/tests/Microsoft.Diagnostics.DebugServices.UnitTests/WriteTestData.cs @@ -3,13 +3,15 @@ // See the LICENSE file in the project root for more information. using Microsoft.Diagnostics.TestHelpers; +using System; namespace Microsoft.Diagnostics.DebugServices.UnitTests { [Command(Name = "writetestdata", Help = "Writes the test data xml file.")] public class WriteTestDataCommand : CommandBase { - public ITarget Target { get; set; } + [ServiceImport] + public IServiceProvider Services { get; set; } [Argument(Name = "FileName", Help = "Test data file path.")] public string FileName { get; set; } @@ -21,7 +23,7 @@ namespace Microsoft.Diagnostics.DebugServices.UnitTests throw new DiagnosticsException("Test data file parameter needed"); } var testDataWriter = new TestDataWriter(); - testDataWriter.Build(Target); + testDataWriter.Build(Services); testDataWriter.Write(FileName); WriteLine($"Test data written to {FileName} successfully"); }