Skip to content

Commit 702b0fd

Browse files
authored
[release/2.2] Startup hook provider (dotnet#20005)
* Add startup hook in System.Private.CoreLib (dotnet#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 * Include in alphabetical order * Move StartupHookProvider to old mscorlib source directory
1 parent f6c04f1 commit 702b0fd

File tree

5 files changed

+146
-1
lines changed

5 files changed

+146
-1
lines changed

src/mscorlib/Resources/Strings.resx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1252,6 +1252,12 @@
12521252
<data name="Argument_InvalidSerializedString" xml:space="preserve">
12531253
<value>The specified serialized string '{0}' is not supported.</value>
12541254
</data>
1255+
<data name="Argument_InvalidStartupHookSyntax" xml:space="preserve">
1256+
<value>The syntax of the startup hook variable was invalid.</value>
1257+
</data>
1258+
<data name="Argument_InvalidStartupHookSignature" xml:space="preserve">
1259+
<value>The signature of the startup hook '{0}' in assembly '{1}' was invalid. It must be 'public static void Initialize()'.</value>
1260+
</data>
12551261
<data name="Argument_InvalidTimeSpanStyles" xml:space="preserve">
12561262
<value>An undefined TimeSpanStyles value is being used.</value>
12571263
</data>

src/mscorlib/System.Private.CoreLib.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,7 @@
355355
<Compile Include="$(BclSourcesRoot)\System\RuntimeArgumentHandle.cs" />
356356
<Compile Include="$(BclSourcesRoot)\System\RuntimeHandles.cs" />
357357
<Compile Include="$(BclSourcesRoot)\System\SharedStatics.cs" />
358+
<Compile Include="$(BclSourcesRoot)\System\StartupHookProvider.cs" />
358359
<Compile Include="$(BclSourcesRoot)\System\StubHelpers.cs" />
359360
<Compile Include="$(BclSourcesRoot)\System\Type.CoreCLR.cs" />
360361
<Compile Include="$(BclSourcesRoot)\System\TypeNameParser.cs" />
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
// See the LICENSE file in the project root for more information.
4+
5+
using System;
6+
using System.Diagnostics;
7+
using System.IO;
8+
using System.Collections.Generic;
9+
using System.Reflection;
10+
using System.Runtime.InteropServices;
11+
using System.Runtime.Loader;
12+
13+
namespace System
14+
{
15+
internal static class StartupHookProvider
16+
{
17+
private const string StartupHookTypeName = "StartupHook";
18+
private const string InitializeMethodName = "Initialize";
19+
20+
// Parse a string specifying a list of assemblies and types
21+
// containing a startup hook, and call each hook in turn.
22+
private static void ProcessStartupHooks()
23+
{
24+
string startupHooksVariable = (string)AppContext.GetData("STARTUP_HOOKS");
25+
if (startupHooksVariable == null)
26+
{
27+
return;
28+
}
29+
30+
// Parse startup hooks variable
31+
string[] startupHooks = startupHooksVariable.Split(Path.PathSeparator);
32+
foreach (string startupHook in startupHooks)
33+
{
34+
if (String.IsNullOrEmpty(startupHook))
35+
{
36+
throw new ArgumentException(SR.Argument_InvalidStartupHookSyntax);
37+
}
38+
if (PathInternal.IsPartiallyQualified(startupHook))
39+
{
40+
throw new ArgumentException(SR.Argument_AbsolutePathRequired);
41+
}
42+
}
43+
44+
// Call each hook in turn
45+
foreach (string startupHook in startupHooks)
46+
{
47+
CallStartupHook(startupHook);
48+
}
49+
}
50+
51+
// Load the specified assembly, and call the specified type's
52+
// "static void Initialize()" method.
53+
private static void CallStartupHook(string assemblyPath)
54+
{
55+
Debug.Assert(!String.IsNullOrEmpty(assemblyPath));
56+
Debug.Assert(!PathInternal.IsPartiallyQualified(assemblyPath));
57+
58+
Assembly assembly = AssemblyLoadContext.Default.LoadFromAssemblyPath(assemblyPath);
59+
Debug.Assert(assembly != null);
60+
Type type = assembly.GetType(StartupHookTypeName, throwOnError: true);
61+
62+
// Look for a static method without any parameters
63+
MethodInfo initializeMethod = type.GetMethod(InitializeMethodName,
64+
BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Static,
65+
null, // use default binder
66+
Type.EmptyTypes, // parameters
67+
null); // no parameter modifiers
68+
69+
bool wrongSignature = false;
70+
if (initializeMethod == null)
71+
{
72+
// There weren't any static methods without
73+
// parameters. Look for any methods with the correct
74+
// name, to provide precise error handling.
75+
try
76+
{
77+
// This could find zero, one, or multiple methods
78+
// with the correct name.
79+
initializeMethod = type.GetMethod(InitializeMethodName,
80+
BindingFlags.Public | BindingFlags.NonPublic |
81+
BindingFlags.Static | BindingFlags.Instance);
82+
}
83+
catch (AmbiguousMatchException)
84+
{
85+
// Found multiple
86+
Debug.Assert(initializeMethod == null);
87+
wrongSignature = true;
88+
}
89+
if (initializeMethod != null)
90+
{
91+
// Found one
92+
wrongSignature = true;
93+
}
94+
else
95+
{
96+
// Didn't find any
97+
throw new MissingMethodException(StartupHookTypeName, InitializeMethodName);
98+
}
99+
}
100+
else if (initializeMethod.ReturnType != typeof(void))
101+
{
102+
wrongSignature = true;
103+
}
104+
105+
if (wrongSignature)
106+
{
107+
throw new ArgumentException(SR.Format(SR.Argument_InvalidStartupHookSignature,
108+
StartupHookTypeName + Type.Delimiter + InitializeMethodName,
109+
assemblyPath));
110+
}
111+
112+
Debug.Assert(initializeMethod != null &&
113+
initializeMethod.IsStatic &&
114+
initializeMethod.ReturnType == typeof(void) &&
115+
initializeMethod.GetParameters().Length == 0);
116+
117+
initializeMethod.Invoke(null, null);
118+
}
119+
}
120+
}

src/vm/assembly.cpp

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1764,6 +1764,21 @@ static void RunMainPost()
17641764
}
17651765
}
17661766

1767+
static void RunStartupHooks()
1768+
{
1769+
CONTRACTL
1770+
{
1771+
THROWS;
1772+
GC_TRIGGERS;
1773+
MODE_COOPERATIVE;
1774+
INJECT_FAULT(COMPlusThrowOM(););
1775+
}
1776+
CONTRACTL_END;
1777+
1778+
MethodDescCallSite processStartupHooks(METHOD__STARTUP_HOOK_PROVIDER__PROCESS_STARTUP_HOOKS);
1779+
processStartupHooks.Call(NULL);
1780+
}
1781+
17671782
INT32 Assembly::ExecuteMainMethod(PTRARRAYREF *stringArgs, BOOL waitForOtherThreads)
17681783
{
17691784
CONTRACTL
@@ -1808,7 +1823,6 @@ INT32 Assembly::ExecuteMainMethod(PTRARRAYREF *stringArgs, BOOL waitForOtherThre
18081823

18091824
RunMainPre();
18101825

1811-
18121826
// Set the root assembly as the assembly that is containing the main method
18131827
// The root assembly is used in the GetEntryAssembly method that on CoreCLR is used
18141828
// to get the TargetFrameworkMoniker for the app
@@ -1819,6 +1833,7 @@ INT32 Assembly::ExecuteMainMethod(PTRARRAYREF *stringArgs, BOOL waitForOtherThre
18191833
// Initialize the managed components of EventPipe and allow tracing to be started before Main.
18201834
EventPipe::InitializeManaged();
18211835
#endif
1836+
RunStartupHooks();
18221837

18231838
hr = RunMain(pMeth, 1, &iRetVal, stringArgs);
18241839
}

src/vm/mscorlib.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -854,6 +854,9 @@ DEFINE_CLASS(EVENTPIPE_CONTROLLER, Tracing, EventPipeController)
854854
DEFINE_METHOD(EVENTPIPE_CONTROLLER, INITIALIZE, Initialize, SM_RetVoid)
855855
#endif
856856

857+
DEFINE_CLASS(STARTUP_HOOK_PROVIDER, System, StartupHookProvider)
858+
DEFINE_METHOD(STARTUP_HOOK_PROVIDER, PROCESS_STARTUP_HOOKS, ProcessStartupHooks, SM_RetVoid)
859+
857860
DEFINE_CLASS(STREAM, IO, Stream)
858861
DEFINE_METHOD(STREAM, BEGIN_READ, BeginRead, IM_ArrByte_Int_Int_AsyncCallback_Object_RetIAsyncResult)
859862
DEFINE_METHOD(STREAM, END_READ, EndRead, IM_IAsyncResult_RetInt)

0 commit comments

Comments
 (0)