Skip to content

Commit

Permalink
Invoke startup hook from ApplyStartupHook diagnostic command (#87490)
Browse files Browse the repository at this point in the history
  • Loading branch information
jander-msft authored Jun 17, 2023
1 parent fd99b07 commit 61ce817
Show file tree
Hide file tree
Showing 6 changed files with 203 additions and 44 deletions.
1 change: 1 addition & 0 deletions src/coreclr/vm/corelib.h
Original file line number Diff line number Diff line change
Expand Up @@ -813,6 +813,7 @@ DEFINE_FIELD_U(iFrameCount, StackFrameHelper, iFrameCount)

DEFINE_CLASS(STARTUP_HOOK_PROVIDER, System, StartupHookProvider)
DEFINE_METHOD(STARTUP_HOOK_PROVIDER, MANAGED_STARTUP, ManagedStartup, SM_PtrChar_RetVoid)
DEFINE_METHOD(STARTUP_HOOK_PROVIDER, CALL_STARTUP_HOOK, CallStartupHook, SM_PtrChar_RetVoid)

DEFINE_CLASS(STREAM, IO, Stream)
DEFINE_METHOD(STREAM, BEGIN_READ, BeginRead, IM_ArrByte_Int_Int_AsyncCallback_Object_RetIAsyncResult)
Expand Down
24 changes: 22 additions & 2 deletions src/coreclr/vm/eventing/eventpipe/ds-rt-coreclr.h
Original file line number Diff line number Diff line change
Expand Up @@ -335,13 +335,33 @@ static
uint32_t
ds_rt_apply_startup_hook (const ep_char16_t *startup_hook_path)
{
if (NULL == startup_hook_path)
return DS_IPC_E_INVALIDARG;

HRESULT hr = S_OK;
// This is set to true when the EE has initialized, which occurs after
// the diagnostic suspension point has completed.
if (g_fEEStarted)
{
// TODO: Support loading and executing startup hook after EE has completely initialized.
return DS_IPC_E_INVALIDARG;
// This is not actually starting the EE (the above already checked that),
// but waits for the EE to be started so that the startup hook can be loaded
// and executed.
IfFailRet(EnsureEEStarted());

EX_TRY {
GCX_COOP();

// Load and call startup hook since managed execution is already running.
MethodDescCallSite callStartupHook(METHOD__STARTUP_HOOK_PROVIDER__CALL_STARTUP_HOOK);

ARG_SLOT args[1];
args[0] = PtrToArgSlot(startup_hook_path);

callStartupHook.Call(args);
}
EX_CATCH_HRESULT (hr);

IfFailRet(hr);
}
else
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
<property name="Target">M:Internal.Runtime.InteropServices.ComponentActivator.GetFunctionPointer(System.IntPtr,System.IntPtr,System.IntPtr,System.IntPtr,System.IntPtr,System.IntPtr)</property>
<property name="Justification">This warning is left in the product so developers get an ILLink warning when trimming an app with Internal.Runtime.InteropServices.ComponentActivator.IsSupported=true.</property>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2026</argument>
<property name="Scope">member</property>
<property name="Target">M:System.StartupHookProvider.CallStartupHook(System.Char*)</property>
<property name="Justification">This warning is left in the product so developers get an ILLink warning when trimming an app with System.StartupHookProvider.IsSupported=true.</property>
</attribute>
<attribute fullname="System.Diagnostics.CodeAnalysis.UnconditionalSuppressMessageAttribute">
<argument>ILLink</argument>
<argument>IL2026</argument>
Expand Down
103 changes: 61 additions & 42 deletions src/libraries/System.Private.CoreLib/src/System/StartupHookProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,40 @@ private static void ProcessStartupHooks(string diagnosticStartupHooks)
startupHookParts.AddRange(startupHooksVariable.Split(Path.PathSeparator));
}

// Parse startup hooks variable
StartupHookNameOrPath[] startupHooks = new StartupHookNameOrPath[startupHookParts.Count];
for (int i = 0; i < startupHookParts.Count; i++)
{
ParseStartupHook(ref startupHooks[i], startupHookParts[i]);
}

// Call each startup hook
for (int i = 0; i < startupHooks.Length; i++)
{
#pragma warning disable IL2026 // suppressed in ILLink.Suppressions.LibraryBuild.xml
CallStartupHook(startupHooks[i]);
#pragma warning restore IL2026
}
}

// Parse a string specifying a single entry containing a startup hook,
// and call the hook.
private static unsafe void CallStartupHook(char* pStartupHookPart)
{
if (!IsSupported)
return;

StartupHookNameOrPath startupHook = default(StartupHookNameOrPath);

ParseStartupHook(ref startupHook, new string(pStartupHookPart));

#pragma warning disable IL2026 // suppressed in ILLink.Suppressions.LibraryBuild.xml
CallStartupHook(startupHook);
#pragma warning restore IL2026
}

private static void ParseStartupHook(ref StartupHookNameOrPath startupHook, string startupHookPart)
{
ReadOnlySpan<char> disallowedSimpleAssemblyNameChars = stackalloc char[4]
{
Path.DirectorySeparatorChar,
Expand All @@ -57,57 +91,42 @@ private static void ProcessStartupHooks(string diagnosticStartupHooks)
','
};

// Parse startup hooks variable
StartupHookNameOrPath[] startupHooks = new StartupHookNameOrPath[startupHookParts.Count];
for (int i = 0; i < startupHookParts.Count; i++)
if (string.IsNullOrEmpty(startupHookPart))
{
string startupHookPart = startupHookParts[i];
if (string.IsNullOrEmpty(startupHookPart))
{
// Leave the slot in startupHooks empty (nulls for everything). This is simpler than shifting and resizing the array.
continue;
}
return;
}

if (Path.IsPathFullyQualified(startupHookPart))
{
startupHooks[i].Path = startupHookPart;
}
else
if (Path.IsPathFullyQualified(startupHookPart))
{
startupHook.Path = startupHookPart;
}
else
{
// The intent here is to only support simple assembly names, but AssemblyName .ctor accepts
// lot of other forms (fully qualified assembly name, strings which look like relative paths and so on).
// So add a check on top which will disallow any directory separator, space or comma in the assembly name.
for (int j = 0; j < disallowedSimpleAssemblyNameChars.Length; j++)
{
// The intent here is to only support simple assembly names, but AssemblyName .ctor accepts
// lot of other forms (fully qualified assembly name, strings which look like relative paths and so on).
// So add a check on top which will disallow any directory separator, space or comma in the assembly name.
for (int j = 0; j < disallowedSimpleAssemblyNameChars.Length; j++)
{
if (startupHookPart.Contains(disallowedSimpleAssemblyNameChars[j]))
{
throw new ArgumentException(SR.Format(SR.Argument_InvalidStartupHookSimpleAssemblyName, startupHookPart));
}
}

if (startupHookPart.EndsWith(DisallowedSimpleAssemblyNameSuffix, StringComparison.OrdinalIgnoreCase))
if (startupHookPart.Contains(disallowedSimpleAssemblyNameChars[j]))
{
throw new ArgumentException(SR.Format(SR.Argument_InvalidStartupHookSimpleAssemblyName, startupHookPart));
}
}

try
{
// This will throw if the string is not a valid assembly name.
startupHooks[i].AssemblyName = new AssemblyName(startupHookPart);
}
catch (Exception assemblyNameException)
{
throw new ArgumentException(SR.Format(SR.Argument_InvalidStartupHookSimpleAssemblyName, startupHookPart), assemblyNameException);
}
if (startupHookPart.EndsWith(DisallowedSimpleAssemblyNameSuffix, StringComparison.OrdinalIgnoreCase))
{
throw new ArgumentException(SR.Format(SR.Argument_InvalidStartupHookSimpleAssemblyName, startupHookPart));
}
}

// Call each hook in turn
foreach (StartupHookNameOrPath startupHook in startupHooks)
{
#pragma warning disable IL2026 // suppressed in ILLink.Suppressions.LibraryBuild.xml
CallStartupHook(startupHook);
#pragma warning restore IL2026
try
{
// This will throw if the string is not a valid assembly name.
startupHook.AssemblyName = new AssemblyName(startupHookPart);
}
catch (Exception assemblyNameException)
{
throw new ArgumentException(SR.Format(SR.Argument_InvalidStartupHookSimpleAssemblyName, startupHookPart), assemblyNameException);
}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -648,6 +648,8 @@
</assembly>
<assembly fullname="System.Private.CoreLib" feature="System.StartupHookProvider.IsSupported" featurevalue="true" featuredefault="true">
<type fullname="System.StartupHookProvider">
<!-- ds-rt-coreclr.h: ds_rt_apply_startup_hook -->
<method name="CallStartupHook" />
<!-- object.c: mono_runtime_run_startup_hooks -->
<method name="ProcessStartupHooks" />
</type>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
using System.Threading.Tasks;
using Microsoft.Diagnostics.Tools.RuntimeClient;
using Microsoft.Diagnostics.Tracing;
using Microsoft.Diagnostics.Tracing.Parsers;
using Tracing.Tests.Common;
using DiagnosticsClient = Microsoft.Diagnostics.NETCore.Client.DiagnosticsClient;

Expand Down Expand Up @@ -48,6 +49,7 @@ public static async Task<bool> TEST_ApplyStartupHookAtStartupSuspension()
Logger.logger.Log($"Sent: {message.ToString()}");
IpcMessage response = IpcClient.SendMessage(stream, message);
Logger.logger.Log($"Received: {response.ToString()}");
fSuccess &= CheckResponse(response);
}
Logger.logger.Log("Waiting to accept diagnostic connection.");
Expand All @@ -64,6 +66,90 @@ public static async Task<bool> TEST_ApplyStartupHookAtStartupSuspension()
Logger.logger.Log($"Sent: {message.ToString()}");
IpcMessage response = IpcClient.SendMessage(stream, message);
Logger.logger.Log($"Received: {response.ToString()}");
fSuccess &= CheckResponse(response);
}
}
);

fSuccess &= await subprocessTask;

return fSuccess;
}

public static async Task<bool> TEST_ApplyStartupHookDuringExecution()
{
bool fSuccess = true;
string serverName = ReverseServer.MakeServerAddress();
Logger.logger.Log($"Server name is '{serverName}'");
Task<bool> subprocessTask = Utils.RunSubprocess(
currentAssembly: Assembly.GetExecutingAssembly(),
environment: new Dictionary<string,string>
{
{ Utils.DiagnosticPortsEnvKey, serverName }
},
duringExecution: async (pid) =>
{
ReverseServer server = new ReverseServer(serverName);
Logger.logger.Log("Waiting to accept diagnostic connection.");
using (Stream stream = await server.AcceptAsync())
{
Logger.logger.Log("Accepted diagnostic connection.");
IpcAdvertise advertise = IpcAdvertise.Parse(stream);
Logger.logger.Log($"IpcAdvertise: {advertise}");
SessionConfiguration config = new(
circularBufferSizeMB: 1000,
format: EventPipeSerializationFormat.NetTrace,
providers: new List<Provider> {
new Provider(AppEventSource.SourceName, 0, EventLevel.Verbose)
});
Logger.logger.Log("Starting EventPipeSession over standard connection");
using Stream eventStream = EventPipeClient.CollectTracing(pid, config, out ulong sessionId);
Logger.logger.Log($"Started EventPipeSession over standard connection with session id: 0x{sessionId:X}");
using EventPipeEventSource source = new(eventStream);
TaskCompletionSource completionSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
source.Dynamic.All += (TraceEvent traceEvent) =>
{
if (AppEventSource.SourceName.Equals(traceEvent.ProviderName) && nameof(AppEventSource.Running).Equals(traceEvent.EventName))
completionSource.TrySetResult();
};
_ = Task.Run(() => source.Process());
Logger.logger.Log($"Send ResumeRuntime Diagnostics IPC Command");
// send ResumeRuntime command (0x04=ProcessCommandSet, 0x01=ResumeRuntime commandid)
var message = new IpcMessage(0x04,0x01);
Logger.logger.Log($"Sent: {message.ToString()}");
IpcMessage response = IpcClient.SendMessage(stream, message);
Logger.logger.Log($"received: {response.ToString()}");
fSuccess &= CheckResponse(response);
Logger.logger.Log("Start waiting for any event that indicates managed code is running.");
await completionSource.Task.ConfigureAwait(false);
Logger.logger.Log("Stopping trace.");
EventPipeClient.StopTracing(pid, sessionId);
}
Logger.logger.Log("Waiting to accept diagnostic connection.");
using (Stream stream = await server.AcceptAsync())
{
Logger.logger.Log("Accepted diagnostic connection.");
IpcAdvertise advertise = IpcAdvertise.Parse(stream);
Logger.logger.Log($"IpcAdvertise: {advertise}");
string startupHookPath = Hook.Basic.AssemblyPath;
Logger.logger.Log($"Send ApplyStartupHook Diagnostic IPC: {startupHookPath}");
IpcMessage message = CreateApplyStartupHookMessage(startupHookPath);
Logger.logger.Log($"Sent: {message.ToString()}");
IpcMessage response = IpcClient.SendMessage(stream, message);
Logger.logger.Log($"Received: {response.ToString()}");
fSuccess &= CheckResponse(response);
}
}
);
Expand All @@ -82,10 +168,19 @@ private static IpcMessage CreateApplyStartupHookMessage(string startupHookPath)
return new IpcMessage(0x04, 0x07, serializedConfiguration);
}

private static bool CheckResponse(IpcMessage response)
{
Logger.logger.Log($"Response CommandId: {response.Header.CommandId}");
return response.Header.CommandId == (byte)0; // DiagnosticsServerResponseId.OK;
}

public static async Task<int> Main(string[] args)
{
if (args.Length >= 1)
{
AppEventSource source = new();
source.Running();

Console.Out.WriteLine("Subprocess started! Waiting for input...");
var input = Console.In.ReadLine(); // will block until data is sent across stdin
Console.Out.WriteLine($"Received '{input}'");
Expand Down Expand Up @@ -120,5 +215,20 @@ public static async Task<int> Main(string[] args)
}
return fSuccess ? 100 : -1;
}

[EventSource(Name = AppEventSource.SourceName)]
private class AppEventSource : EventSource
{
public const string SourceName = nameof(AppEventSource);
public const int RunningEventId = 1;

public AppEventSource() : base(EventSourceSettings.EtwSelfDescribingEventFormat) { }

[Event(RunningEventId)]
public void Running()
{
WriteEvent(RunningEventId);
}
}
}
}

0 comments on commit 61ce817

Please sign in to comment.