From 8f638c4915be77a12fa9ce651521d891aa32ec12 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Fri, 12 Oct 2018 13:21:16 -0700 Subject: [PATCH 1/3] Add startup hook in System.Private.CoreLib (#19486) * Add startup hook in System.Private.CoreLib ProcessStartupHooks can be called from the host before the user's Main entry point. It receives a list of dlls and types containing Initialize() methods that will be called, making it possible to inject managed code early during startup. * Allow ! in assembly path for startup hook and other changes Also: - Report full assembly path when startup hook assembly is not found - Remove unnecessary assert - use Type.Delimiter instead of "." * Use C# 7 tuple syntax and remove assert * Improve error handling Throw MissingMethodException only when there aren't any Initialize methods at all. When there are Initialize methods with incorrect signatures (parameters, return type, visibility, or instance methods), throw invalid signature error. This should improve diagnosability of this feature. * Remove eager check for missing startup hook assemblies * Require full assembly path and use Split(char) overload. * Remove startup hook type syntax The type is now required to be "StartupHook" (in the global namespace). * Add assembly path to startup signature exception With a hard-coded type name, printing the type.method of the startup hook in the exception will no longer be much of an aid in debugging startup hook signature issues. Adding the assembly path makes it clear which startup hook had the problem. * Use const strings * Call startup hook inside ExecuteMainMethod This way it will be called when the application is executed, but not during other uses of hosting apis that go through coreclr_create_delegate. This change will ensure that the threading state is set based on attributes in the main method, before the startup hooks run. * Run startup hooks after setting root assembly and other fixes - Run startup hooks after setting the appdomain's root assembly (visible in Assembly.GetEntryAssembly() - Make the class static - Remove debug output - Don't allocate an empty ARG_SLOT array * Allow non-public Initialize method, adjust coding style * Remove overly-specific assert --- .../src/System/StartupHookProvider.cs | 120 ++++++++++++++++++ src/mscorlib/Resources/Strings.resx | 6 + src/mscorlib/System.Private.CoreLib.csproj | 1 + src/vm/assembly.cpp | 17 ++- src/vm/mscorlib.h | 3 + 5 files changed, 146 insertions(+), 1 deletion(-) create mode 100644 src/System.Private.CoreLib/src/System/StartupHookProvider.cs diff --git a/src/System.Private.CoreLib/src/System/StartupHookProvider.cs b/src/System.Private.CoreLib/src/System/StartupHookProvider.cs new file mode 100644 index 000000000000..cc6f8d41fdac --- /dev/null +++ b/src/System.Private.CoreLib/src/System/StartupHookProvider.cs @@ -0,0 +1,120 @@ +// 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.Diagnostics; +using System.IO; +using System.Collections.Generic; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Runtime.Loader; + +namespace System +{ + internal static class StartupHookProvider + { + private const string StartupHookTypeName = "StartupHook"; + private const string InitializeMethodName = "Initialize"; + + // Parse a string specifying a list of assemblies and types + // containing a startup hook, and call each hook in turn. + private static void ProcessStartupHooks() + { + string startupHooksVariable = (string)AppContext.GetData("STARTUP_HOOKS"); + if (startupHooksVariable == null) + { + return; + } + + // Parse startup hooks variable + string[] startupHooks = startupHooksVariable.Split(Path.PathSeparator); + foreach (string startupHook in startupHooks) + { + if (String.IsNullOrEmpty(startupHook)) + { + throw new ArgumentException(SR.Argument_InvalidStartupHookSyntax); + } + if (PathInternal.IsPartiallyQualified(startupHook)) + { + throw new ArgumentException(SR.Argument_AbsolutePathRequired); + } + } + + // Call each hook in turn + foreach (string startupHook in startupHooks) + { + CallStartupHook(startupHook); + } + } + + // Load the specified assembly, and call the specified type's + // "static void Initialize()" method. + private static void CallStartupHook(string assemblyPath) + { + Debug.Assert(!String.IsNullOrEmpty(assemblyPath)); + Debug.Assert(!PathInternal.IsPartiallyQualified(assemblyPath)); + + Assembly assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath); + Debug.Assert(assembly != null); + Type type = assembly.GetType(StartupHookTypeName, throwOnError: true); + + // Look for a static method without any parameters + MethodInfo initializeMethod = type.GetMethod(InitializeMethodName, + BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static, + null, // use default binder + Type.EmptyTypes, // parameters + null); // no parameter modifiers + + bool wrongSignature = false; + if (initializeMethod == null) + { + // There weren't any static methods without + // parameters. Look for any methods with the correct + // name, to provide precise error handling. + try + { + // This could find zero, one, or multiple methods + // with the correct name. + initializeMethod = type.GetMethod(InitializeMethodName, + BindingFlags.Public | BindingFlags.NonPublic | + BindingFlags.Static | BindingFlags.Instance); + } + catch (AmbiguousMatchException) + { + // Found multiple + Debug.Assert(initializeMethod == null); + wrongSignature = true; + } + if (initializeMethod != null) + { + // Found one + wrongSignature = true; + } + else + { + // Didn't find any + throw new MissingMethodException(StartupHookTypeName, InitializeMethodName); + } + } + else if (initializeMethod.ReturnType != typeof(void)) + { + wrongSignature = true; + } + + if (wrongSignature) + { + throw new ArgumentException(SR.Format(SR.Argument_InvalidStartupHookSignature, + StartupHookTypeName + Type.Delimiter + InitializeMethodName, + assemblyPath)); + } + + Debug.Assert(initializeMethod != null && + initializeMethod.IsStatic && + initializeMethod.ReturnType == typeof(void) && + initializeMethod.GetParameters().Length == 0); + + initializeMethod.Invoke(null, null); + } + } +} diff --git a/src/mscorlib/Resources/Strings.resx b/src/mscorlib/Resources/Strings.resx index 49c8e932498f..45036473f334 100644 --- a/src/mscorlib/Resources/Strings.resx +++ b/src/mscorlib/Resources/Strings.resx @@ -1252,6 +1252,12 @@ The specified serialized string '{0}' is not supported. + + The syntax of the startup hook variable was invalid. + + + The signature of the startup hook '{0}' in assembly '{1}' was invalid. It must be 'public static void Initialize()'. + An undefined TimeSpanStyles value is being used. diff --git a/src/mscorlib/System.Private.CoreLib.csproj b/src/mscorlib/System.Private.CoreLib.csproj index b40bb53cb53b..4906a01bbb27 100644 --- a/src/mscorlib/System.Private.CoreLib.csproj +++ b/src/mscorlib/System.Private.CoreLib.csproj @@ -364,6 +364,7 @@ + diff --git a/src/vm/assembly.cpp b/src/vm/assembly.cpp index d2551b5683a5..a88be6b2e4b3 100644 --- a/src/vm/assembly.cpp +++ b/src/vm/assembly.cpp @@ -1764,6 +1764,21 @@ static void RunMainPost() } } +static void RunStartupHooks() +{ + CONTRACTL + { + THROWS; + GC_TRIGGERS; + MODE_COOPERATIVE; + INJECT_FAULT(COMPlusThrowOM();); + } + CONTRACTL_END; + + MethodDescCallSite processStartupHooks(METHOD__STARTUP_HOOK_PROVIDER__PROCESS_STARTUP_HOOKS); + processStartupHooks.Call(NULL); +} + INT32 Assembly::ExecuteMainMethod(PTRARRAYREF *stringArgs, BOOL waitForOtherThreads) { CONTRACTL @@ -1808,7 +1823,6 @@ INT32 Assembly::ExecuteMainMethod(PTRARRAYREF *stringArgs, BOOL waitForOtherThre RunMainPre(); - // Set the root assembly as the assembly that is containing the main method // The root assembly is used in the GetEntryAssembly method that on CoreCLR is used // to get the TargetFrameworkMoniker for the app @@ -1819,6 +1833,7 @@ INT32 Assembly::ExecuteMainMethod(PTRARRAYREF *stringArgs, BOOL waitForOtherThre // Initialize the managed components of EventPipe and allow tracing to be started before Main. EventPipe::InitializeManaged(); #endif + RunStartupHooks(); hr = RunMain(pMeth, 1, &iRetVal, stringArgs); } diff --git a/src/vm/mscorlib.h b/src/vm/mscorlib.h index 810cc72b633c..6b65a0b692b5 100644 --- a/src/vm/mscorlib.h +++ b/src/vm/mscorlib.h @@ -854,6 +854,9 @@ DEFINE_CLASS(EVENTPIPE_CONTROLLER, Tracing, EventPipeController) DEFINE_METHOD(EVENTPIPE_CONTROLLER, INITIALIZE, Initialize, SM_RetVoid) #endif +DEFINE_CLASS(STARTUP_HOOK_PROVIDER, System, StartupHookProvider) +DEFINE_METHOD(STARTUP_HOOK_PROVIDER, PROCESS_STARTUP_HOOKS, ProcessStartupHooks, SM_RetVoid) + DEFINE_CLASS(STREAM, IO, Stream) DEFINE_METHOD(STREAM, BEGIN_READ, BeginRead, IM_ArrByte_Int_Int_AsyncCallback_Object_RetIAsyncResult) DEFINE_METHOD(STREAM, END_READ, EndRead, IM_IAsyncResult_RetInt) From 1162fe9f75f5be6542222142602242a8b8e6dd95 Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Tue, 18 Sep 2018 09:44:54 -0700 Subject: [PATCH 2/3] Include in alphabetical order --- src/mscorlib/System.Private.CoreLib.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/mscorlib/System.Private.CoreLib.csproj b/src/mscorlib/System.Private.CoreLib.csproj index 4906a01bbb27..b365ff50ad29 100644 --- a/src/mscorlib/System.Private.CoreLib.csproj +++ b/src/mscorlib/System.Private.CoreLib.csproj @@ -355,6 +355,7 @@ + @@ -364,7 +365,6 @@ - From 84a6d437159b996f2543339aabb3f093a8b7d9ae Mon Sep 17 00:00:00 2001 From: Sven Boemer Date: Fri, 12 Oct 2018 16:12:14 -0700 Subject: [PATCH 3/3] Move StartupHookProvider to old mscorlib source directory --- .../src/System/StartupHookProvider.cs | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/{System.Private.CoreLib => mscorlib}/src/System/StartupHookProvider.cs (100%) diff --git a/src/System.Private.CoreLib/src/System/StartupHookProvider.cs b/src/mscorlib/src/System/StartupHookProvider.cs similarity index 100% rename from src/System.Private.CoreLib/src/System/StartupHookProvider.cs rename to src/mscorlib/src/System/StartupHookProvider.cs