Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/design/features/host-startup-hook.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,10 @@ be defined either in the app or within the first hook that uses it:
The type should be made `internal` to prevent exposing it as API
surface to any managed code that happens to have access to the startup
hook dll. However, the feature will also work if the type is `public`.

### Incompatible with trimming

Startup hooks are disabled by default on trimmed apps. The usage of
startup hooks on a trimmed app is potentially dangerous since these
could make use of assemblies, types or members that were removed by
trimming, causing the app to crash.
1 change: 1 addition & 0 deletions docs/workflow/trimming/feature-switches.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ configurations but their defaults might vary as any SDK can set the defaults dif
| InvariantGlobalization | System.Globalization.Invariant | All globalization specific code and data is trimmed when set to true |
| UseSystemResourceKeys | System.Resources.UseSystemResourceKeys | Any localizable resources for system assemblies is trimmed when set to true |
| HttpActivityPropagationSupport | System.Net.Http.EnableActivityPropagation | Any dependency related to diagnostics support for System.Net.Http is trimmed when set to false |
| StartupHookSupport | System.StartupHookProvider.IsSupported | Startup hooks are disabled when set to false. Startup hook related functionality can be trimmed. |

Any feature-switch which defines property can be set in csproj file or
on the command line as any other MSBuild property. Those without predefined property name
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Reflection;
using System.Runtime.Loader;
Expand All @@ -14,6 +16,8 @@ internal static class StartupHookProvider
private const string InitializeMethodName = "Initialize";
private const string DisallowedSimpleAssemblyNameSuffix = ".dll";

private static bool IsSupported => AppContext.TryGetSwitch("System.StartupHookProvider.IsSupported", out bool isSupported) ? isSupported : true;

private struct StartupHookNameOrPath
{
public AssemblyName AssemblyName;
Expand All @@ -24,6 +28,9 @@ private struct StartupHookNameOrPath
// containing a startup hook, and call each hook in turn.
private static void ProcessStartupHooks()
{
if (!IsSupported)
return;
Comment on lines +31 to +32
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would structure this so that if someone specifies a startup hook (there's something actionable in STARTUP_HOOKS) and startup hook support was linked out, they get some sort of feedback about it (exception?).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it is better to ignore it silently (no exception).

Having ability to disable startup hooks per app is useful on its own, independent of linking. For example, STARTUP_HOOKS can be set globally for APM, but there is one utility where you do not want it to kick in.


// Initialize tracing before any user code can be called.
System.Diagnostics.Tracing.RuntimeEventSource.Initialize();

Expand Down Expand Up @@ -96,6 +103,8 @@ private static void ProcessStartupHooks()

// Load the specified assembly, and call the specified type's
// "static void Initialize()" method.
[RequiresUnreferencedCode("The StartupHookSupport feature switch has been enabled for this app which is being trimmed. " +
"Startup hook code is not observable by the trimmer and so required assemblies, types and members may be removed")]
private static void CallStartupHook(StartupHookNameOrPath startupHook)
{
Assembly assembly;
Expand Down
4 changes: 3 additions & 1 deletion src/installer/tests/HostActivation.Tests/RuntimeConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -276,7 +276,9 @@ public void Save()
JObject configProperties = new JObject();
foreach (var property in _properties)
{
configProperties.Add(property.Item1, property.Item2);
var tokenValue = (property.Item2 == "false" || property.Item2 == "true") ?
JToken.Parse(property.Item2) : property.Item2;
configProperties.Add(property.Item1, tokenValue);
}

runtimeOptions.Add("configProperties", configProperties);
Expand Down
27 changes: 27 additions & 0 deletions src/installer/tests/HostActivation.Tests/StartupHooks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ public class StartupHooks : IClassFixture<StartupHooks.SharedTestState>
{
private SharedTestState sharedTestState;
private string startupHookVarName = "DOTNET_STARTUP_HOOKS";
private string startupHookSupport = "System.StartupHookProvider.IsSupported";

public StartupHooks(StartupHooks.SharedTestState fixture)
{
Expand Down Expand Up @@ -639,6 +640,32 @@ public void Muxer_activation_of_StartupHook_With_Assembly_Resolver()
.And.ExitWith(2);
}

[Fact]
public void Muxer_activation_of_StartupHook_With_IsSupported_False()
{
var fixture = sharedTestState.PortableAppFixture.Copy();
var dotnet = fixture.BuiltDotnet;
var appDll = fixture.TestProject.AppDll;

var startupHookFixture = sharedTestState.StartupHookFixture.Copy();
var startupHookDll = startupHookFixture.TestProject.AppDll;

RuntimeConfig.FromFile(fixture.TestProject.RuntimeConfigJson)
.WithProperty(startupHookSupport, "false")
.Save();

// Startup hooks are not executed when the StartupHookSupport
// feature switch is set to false.
dotnet.Exec(appDll)
.EnvironmentVariable(startupHookVarName, startupHookDll)
.CaptureStdOut()
.CaptureStdErr()
.Execute()
.Should().Pass()
.And.NotHaveStdOutContaining("Hello from startup hook!")
.And.HaveStdOutContaining("Hello World");
}

public class SharedTestState : IDisposable
{
// Entry point projects
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,5 +18,8 @@
<type fullname="System.Threading.Tasks.Task" feature="System.Diagnostics.Debugger.IsSupported" featurevalue="false">
<field name="s_asyncDebuggingEnabled" value="false" initialize="false" />
</type>
<type fullname="System.StartupHookProvider" feature="System.StartupHookProvider.IsSupported" featurevalue="false">
<method signature="System.Boolean get_IsSupported()" body="stub" value="false" />
</type>
</assembly>
</linker>
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
<argument>ILLink</argument>
<argument>IL2026</argument>
<property name="Scope">member</property>
<property name="Target">M:System.StartupHookProvider.CallStartupHook(System.StartupHookProvider.StartupHookNameOrPath)</property>
<property name="Target">M:System.StartupHookProvider.ProcessStartupHooks()</property>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
Expand Down