Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.

Commit

Permalink
[release/2.2] Startup hook provider (#20005)
Browse files Browse the repository at this point in the history
* 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

* Include in alphabetical order

* Move StartupHookProvider to old mscorlib source directory
  • Loading branch information
sbomer authored Oct 13, 2018
1 parent f6c04f1 commit 702b0fd
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 1 deletion.
6 changes: 6 additions & 0 deletions src/mscorlib/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1252,6 +1252,12 @@
<data name="Argument_InvalidSerializedString" xml:space="preserve">
<value>The specified serialized string '{0}' is not supported.</value>
</data>
<data name="Argument_InvalidStartupHookSyntax" xml:space="preserve">
<value>The syntax of the startup hook variable was invalid.</value>
</data>
<data name="Argument_InvalidStartupHookSignature" xml:space="preserve">
<value>The signature of the startup hook '{0}' in assembly '{1}' was invalid. It must be 'public static void Initialize()'.</value>
</data>
<data name="Argument_InvalidTimeSpanStyles" xml:space="preserve">
<value>An undefined TimeSpanStyles value is being used.</value>
</data>
Expand Down
1 change: 1 addition & 0 deletions src/mscorlib/System.Private.CoreLib.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,7 @@
<Compile Include="$(BclSourcesRoot)\System\RuntimeArgumentHandle.cs" />
<Compile Include="$(BclSourcesRoot)\System\RuntimeHandles.cs" />
<Compile Include="$(BclSourcesRoot)\System\SharedStatics.cs" />
<Compile Include="$(BclSourcesRoot)\System\StartupHookProvider.cs" />
<Compile Include="$(BclSourcesRoot)\System\StubHelpers.cs" />
<Compile Include="$(BclSourcesRoot)\System\Type.CoreCLR.cs" />
<Compile Include="$(BclSourcesRoot)\System\TypeNameParser.cs" />
Expand Down
120 changes: 120 additions & 0 deletions src/mscorlib/src/System/StartupHookProvider.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
17 changes: 16 additions & 1 deletion src/vm/assembly.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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);
}
Expand Down
3 changes: 3 additions & 0 deletions src/vm/mscorlib.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down

0 comments on commit 702b0fd

Please sign in to comment.