diff --git a/src/coreclr/System.Private.CoreLib/src/System/StartupHookProvider.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/StartupHookProvider.CoreCLR.cs index d277c177e1ef16..8afaddc63b45e0 100644 --- a/src/coreclr/System.Private.CoreLib/src/System/StartupHookProvider.CoreCLR.cs +++ b/src/coreclr/System.Private.CoreLib/src/System/StartupHookProvider.CoreCLR.cs @@ -12,7 +12,7 @@ namespace System { internal static partial class StartupHookProvider { - private static void ManagedStartup() + private static unsafe void ManagedStartup(char* pDiagnosticStartupHooks) { #if FEATURE_PERFTRACING if (EventSource.IsSupported) @@ -20,7 +20,7 @@ private static void ManagedStartup() #endif if (IsSupported) - ProcessStartupHooks(); + ProcessStartupHooks(new string(pDiagnosticStartupHooks)); } } } diff --git a/src/coreclr/nativeaot/Runtime/eventpipe/ds-rt-aot.h b/src/coreclr/nativeaot/Runtime/eventpipe/ds-rt-aot.h index df255f7186e193..ee20ab37e70668 100644 --- a/src/coreclr/nativeaot/Runtime/eventpipe/ds-rt-aot.h +++ b/src/coreclr/nativeaot/Runtime/eventpipe/ds-rt-aot.h @@ -272,6 +272,13 @@ ds_rt_disable_perfmap (void) return DS_IPC_E_NOTSUPPORTED; } +static +uint32_t +ds_rt_apply_startup_hook (const ep_char16_t *startup_hook_path) +{ + return DS_IPC_E_NOTSUPPORTED; +} + /* * DiagnosticServer. */ diff --git a/src/coreclr/vm/assembly.cpp b/src/coreclr/vm/assembly.cpp index 7561e7d9e50559..d196eee3fd9504 100644 --- a/src/coreclr/vm/assembly.cpp +++ b/src/coreclr/vm/assembly.cpp @@ -52,6 +52,7 @@ //#define STRICT_JITLOCK_ENTRY_LEAK_DETECTION //#define STRICT_CLSINITLOCK_ENTRY_LEAK_DETECTION +LPCWSTR s_wszDiagnosticStartupHookPaths = nullptr; #ifndef DACCESS_COMPILE @@ -1122,7 +1123,47 @@ bool Assembly::IgnoresAccessChecksTo(Assembly *pAccessedAssembly) return GetFriendAssemblyInfo()->IgnoresAccessChecksTo(pAccessedAssembly); } +void Assembly::AddDiagnosticStartupHookPath(LPCWSTR wszPath) +{ + LPCWSTR wszDiagnosticStartupHookPathsLocal = s_wszDiagnosticStartupHookPaths; + + size_t cchPath = u16_strlen(wszPath); + size_t cchDiagnosticStartupHookPathsNew = cchPath; + size_t cchDiagnosticStartupHookPathsLocal = 0; + if (nullptr != wszDiagnosticStartupHookPathsLocal) + { + cchDiagnosticStartupHookPathsLocal = u16_strlen(wszDiagnosticStartupHookPathsLocal); + // Add 1 for the path separator + cchDiagnosticStartupHookPathsNew += cchDiagnosticStartupHookPathsLocal + 1; + } + size_t currentSize = cchDiagnosticStartupHookPathsNew + 1; + LPWSTR wszDiagnosticStartupHookPathsNew = new WCHAR[currentSize]; + LPWSTR wszCurrent = wszDiagnosticStartupHookPathsNew; + + u16_strcpy_s(wszCurrent, currentSize, wszPath); + wszCurrent += cchPath; + currentSize -= cchPath; + + if (cchDiagnosticStartupHookPathsLocal > 0) + { + u16_strcpy_s(wszCurrent, currentSize, PATH_SEPARATOR_STR_W); + wszCurrent += 1; + currentSize -= 1; + + u16_strcpy_s(wszCurrent, currentSize, wszDiagnosticStartupHookPathsLocal); + wszCurrent += cchDiagnosticStartupHookPathsLocal; + currentSize -= cchDiagnosticStartupHookPathsLocal; + } + + // Expect null terminating character + _ASSERTE(currentSize == 1); + _ASSERTE(wszCurrent[0] == W('\0')); + + s_wszDiagnosticStartupHookPaths = wszDiagnosticStartupHookPathsNew; + + delete [] wszDiagnosticStartupHookPathsLocal; +} enum CorEntryPointType { @@ -1376,7 +1417,7 @@ static void RunMainPost() } } -static void RunManagedStartup() +void RunManagedStartup() { CONTRACTL { @@ -1388,7 +1429,11 @@ static void RunManagedStartup() CONTRACTL_END; MethodDescCallSite managedStartup(METHOD__STARTUP_HOOK_PROVIDER__MANAGED_STARTUP); - managedStartup.Call(NULL); + + ARG_SLOT args[1]; + args[0] = PtrToArgSlot(s_wszDiagnosticStartupHookPaths); + + managedStartup.Call(args); } INT32 Assembly::ExecuteMainMethod(PTRARRAYREF *stringArgs, BOOL waitForOtherThreads) diff --git a/src/coreclr/vm/assembly.hpp b/src/coreclr/vm/assembly.hpp index 43e95cfe0902bd..c70c85861b54fd 100644 --- a/src/coreclr/vm/assembly.hpp +++ b/src/coreclr/vm/assembly.hpp @@ -380,6 +380,8 @@ class Assembly } #endif + static void AddDiagnosticStartupHookPath(LPCWSTR wszPath); + protected: #ifdef FEATURE_COMINTEROP diff --git a/src/coreclr/vm/corelib.h b/src/coreclr/vm/corelib.h index ed41cec4fa948a..90cf7a9241d91d 100644 --- a/src/coreclr/vm/corelib.h +++ b/src/coreclr/vm/corelib.h @@ -812,7 +812,7 @@ DEFINE_FIELD_U(rgiLastFrameFromForeignExceptionStackTrace, StackFrame DEFINE_FIELD_U(iFrameCount, StackFrameHelper, iFrameCount) DEFINE_CLASS(STARTUP_HOOK_PROVIDER, System, StartupHookProvider) -DEFINE_METHOD(STARTUP_HOOK_PROVIDER, MANAGED_STARTUP, ManagedStartup, SM_RetVoid) +DEFINE_METHOD(STARTUP_HOOK_PROVIDER, MANAGED_STARTUP, ManagedStartup, SM_PtrChar_RetVoid) DEFINE_CLASS(STREAM, IO, Stream) DEFINE_METHOD(STREAM, BEGIN_READ, BeginRead, IM_ArrByte_Int_Int_AsyncCallback_Object_RetIAsyncResult) diff --git a/src/coreclr/vm/eventing/eventpipe/ds-rt-coreclr.h b/src/coreclr/vm/eventing/eventpipe/ds-rt-coreclr.h index 18d2ce62bdfc69..7ea2201b9787b7 100644 --- a/src/coreclr/vm/eventing/eventpipe/ds-rt-coreclr.h +++ b/src/coreclr/vm/eventing/eventpipe/ds-rt-coreclr.h @@ -329,6 +329,28 @@ ds_rt_disable_perfmap (void) #endif // FEATURE_PERFMAP } +static ep_char16_t * _ds_rt_coreclr_diagnostic_startup_hook_paths = NULL; + +static +uint32_t +ds_rt_apply_startup_hook (const ep_char16_t *startup_hook_path) +{ + 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; + } + else + { + Assembly::AddDiagnosticStartupHookPath(reinterpret_cast(startup_hook_path)); + } + + return DS_IPC_S_OK; +} + /* * DiagnosticServer. */ diff --git a/src/coreclr/vm/metasig.h b/src/coreclr/vm/metasig.h index 913280c839a161..6adf044a0170df 100644 --- a/src/coreclr/vm/metasig.h +++ b/src/coreclr/vm/metasig.h @@ -338,6 +338,7 @@ DEFINE_METASIG(SM(IntPtr_Bool_RetVoid, I F, v)) DEFINE_METASIG(SM(IntPtr_UInt_IntPtr_RetVoid, I K I, v)) DEFINE_METASIG(SM(IntPtr_RetUInt, I, K)) DEFINE_METASIG(SM(PtrChar_RetInt, P(u), i)) +DEFINE_METASIG(SM(PtrChar_RetVoid, P(u), v)) DEFINE_METASIG(SM(IntPtr_IntPtr_RetIntPtr, I I, I)) DEFINE_METASIG(SM(IntPtr_IntPtr_Int_RetIntPtr, I I i, I)) DEFINE_METASIG(SM(PtrVoid_PtrVoid_RetVoid, P(v) P(v), v)) diff --git a/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Suppressions.LibraryBuild.xml b/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Suppressions.LibraryBuild.xml index 1dbf49097f8f5c..278ed933f07043 100644 --- a/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Suppressions.LibraryBuild.xml +++ b/src/libraries/System.Private.CoreLib/src/ILLink/ILLink.Suppressions.LibraryBuild.xml @@ -19,7 +19,7 @@ ILLink IL2026 member - M:System.StartupHookProvider.ProcessStartupHooks() + M:System.StartupHookProvider.ProcessStartupHooks(System.String) This warning is left in the product so developers get an ILLink warning when trimming an app with System.StartupHookProvider.IsSupported=true. diff --git a/src/libraries/System.Private.CoreLib/src/System/StartupHookProvider.cs b/src/libraries/System.Private.CoreLib/src/System/StartupHookProvider.cs index 78b86b6583d9a0..72232ba42d3adc 100644 --- a/src/libraries/System.Private.CoreLib/src/System/StartupHookProvider.cs +++ b/src/libraries/System.Private.CoreLib/src/System/StartupHookProvider.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.Tracing; using System.Diagnostics.CodeAnalysis; @@ -27,15 +28,25 @@ private struct StartupHookNameOrPath // Parse a string specifying a list of assemblies and types // containing a startup hook, and call each hook in turn. - private static void ProcessStartupHooks() + private static void ProcessStartupHooks(string diagnosticStartupHooks) { if (!IsSupported) return; string? startupHooksVariable = AppContext.GetData("STARTUP_HOOKS") as string; - if (startupHooksVariable == null) - { + if (null == startupHooksVariable && string.IsNullOrEmpty(diagnosticStartupHooks)) return; + + List startupHookParts = new(); + + if (!string.IsNullOrEmpty(diagnosticStartupHooks)) + { + startupHookParts.AddRange(diagnosticStartupHooks.Split(Path.PathSeparator)); + } + + if (null != startupHooksVariable) + { + startupHookParts.AddRange(startupHooksVariable.Split(Path.PathSeparator)); } ReadOnlySpan disallowedSimpleAssemblyNameChars = stackalloc char[4] @@ -47,9 +58,8 @@ private static void ProcessStartupHooks() }; // Parse startup hooks variable - string[] startupHookParts = startupHooksVariable.Split(Path.PathSeparator); - StartupHookNameOrPath[] startupHooks = new StartupHookNameOrPath[startupHookParts.Length]; - for (int i = 0; i < startupHookParts.Length; i++) + StartupHookNameOrPath[] startupHooks = new StartupHookNameOrPath[startupHookParts.Count]; + for (int i = 0; i < startupHookParts.Count; i++) { string startupHookPart = startupHookParts[i]; if (string.IsNullOrEmpty(startupHookPart)) diff --git a/src/mono/mono/eventpipe/ds-rt-mono.h b/src/mono/mono/eventpipe/ds-rt-mono.h index 3eea7f1ebe04cd..86898cfccb0dd9 100644 --- a/src/mono/mono/eventpipe/ds-rt-mono.h +++ b/src/mono/mono/eventpipe/ds-rt-mono.h @@ -239,6 +239,14 @@ ds_rt_disable_perfmap (void) return DS_IPC_E_NOTSUPPORTED; } +static +uint32_t +ds_rt_apply_startup_hook (const ep_char16_t *startup_hook_path) +{ + // TODO: Implement. + return DS_IPC_E_NOTSUPPORTED; +} + /* * DiagnosticServer. */ diff --git a/src/mono/mono/metadata/object.c b/src/mono/mono/metadata/object.c index fab53233699b6a..8604114fe520f5 100644 --- a/src/mono/mono/metadata/object.c +++ b/src/mono/mono/metadata/object.c @@ -8135,7 +8135,11 @@ mono_runtime_run_startup_hooks (void) mono_error_cleanup (error); if (!method) return; - mono_runtime_invoke_checked (method, NULL, NULL, error); + + gpointer args [1]; + args[0] = mono_string_empty_internal (mono_domain_get ()); + + mono_runtime_invoke_checked (method, NULL, args, error); // runtime hooks design doc says not to catch exceptions from the hooks mono_error_raise_exception_deprecated (error); } diff --git a/src/native/eventpipe/ds-process-protocol.c b/src/native/eventpipe/ds-process-protocol.c index a40403a03f7897..4fdd912449abf6 100644 --- a/src/native/eventpipe/ds-process-protocol.c +++ b/src/native/eventpipe/ds-process-protocol.c @@ -89,6 +89,12 @@ process_protocol_helper_disable_perfmap ( DiagnosticsIpcMessage *message, DiagnosticsIpcStream *stream); +static +bool +process_protocol_helper_apply_startup_hook ( + DiagnosticsIpcMessage *message, + DiagnosticsIpcStream *stream); + static bool process_protocol_helper_unknown_command ( @@ -872,6 +878,87 @@ process_protocol_helper_disable_perfmap ( ep_exit_error_handler (); } +DiagnosticsApplyStartupHookPayload * +ds_apply_startup_hook_payload_alloc (void) +{ + return ep_rt_object_alloc (DiagnosticsApplyStartupHookPayload); +} + +void +ds_apply_startup_hook_payload_free (DiagnosticsApplyStartupHookPayload *payload) +{ + ep_return_void_if_nok (payload != NULL); + ep_rt_byte_array_free (payload->incoming_buffer); + ep_rt_object_free (payload); +} + +static +uint8_t * +apply_startup_hook_command_try_parse_payload ( + uint8_t *buffer, + uint16_t buffer_len) +{ + EP_ASSERT (buffer != NULL); + + uint8_t * buffer_cursor = buffer; + uint32_t buffer_cursor_len = buffer_len; + + DiagnosticsApplyStartupHookPayload *instance = ds_apply_startup_hook_payload_alloc (); + ep_raise_error_if_nok (instance != NULL); + + instance->incoming_buffer = buffer; + + if (!ds_ipc_message_try_parse_string_utf16_t (&buffer_cursor, &buffer_cursor_len, &instance->startup_hook_path)) + ep_raise_error (); + +ep_on_exit: + return (uint8_t *)instance; + +ep_on_error: + ds_apply_startup_hook_payload_free (instance); + instance = NULL; + ep_exit_error_handler (); +} + +static +bool +process_protocol_helper_apply_startup_hook ( + DiagnosticsIpcMessage *message, + DiagnosticsIpcStream *stream) +{ + EP_ASSERT (message != NULL); + EP_ASSERT (stream != NULL); + + if (!stream) + return false; + + bool result = false; + DiagnosticsApplyStartupHookPayload *payload = (DiagnosticsApplyStartupHookPayload *)ds_ipc_message_try_parse_payload (message, apply_startup_hook_command_try_parse_payload); + if (!payload) { + ds_ipc_message_send_error (stream, DS_IPC_E_BAD_ENCODING); + ep_raise_error (); + } + + ds_ipc_result_t ipc_result; + ipc_result = ds_rt_apply_startup_hook (payload->startup_hook_path); + if (ipc_result != DS_IPC_S_OK) { + ds_ipc_message_send_error (stream, ipc_result); + ep_raise_error (); + } else { + ds_ipc_message_send_success (stream, ipc_result); + } + + result = true; + +ep_on_exit: + ds_ipc_stream_free (stream); + return result; + +ep_on_error: + EP_ASSERT (!result); + ep_exit_error_handler (); +} + static bool process_protocol_helper_unknown_command ( @@ -916,6 +1003,9 @@ ds_process_protocol_helper_handle_ipc_message ( case DS_PROCESS_COMMANDID_DISABLE_PERFMAP: result = process_protocol_helper_disable_perfmap (message, stream); break; + case DS_PROCESS_COMMANDID_APPLY_STARTUP_HOOK: + result = process_protocol_helper_apply_startup_hook (message, stream); + break; default: result = process_protocol_helper_unknown_command (message, stream); break; diff --git a/src/native/eventpipe/ds-process-protocol.h b/src/native/eventpipe/ds-process-protocol.h index 04430bbb72b85e..dc9b1250618c52 100644 --- a/src/native/eventpipe/ds-process-protocol.h +++ b/src/native/eventpipe/ds-process-protocol.h @@ -190,6 +190,32 @@ ds_enable_perfmap_payload_alloc (void); void ds_enable_perfmap_payload_free (DiagnosticsEnablePerfmapPayload *payload); +/* +* DiagnosticsApplyStartupHookPayload +*/ + +#if defined(DS_INLINE_GETTER_SETTER) || defined(DS_IMPL_PROCESS_PROTOCOL_GETTER_SETTER) +struct _DiagnosticsApplyStartupHookPayload { +#else +struct _DiagnosticsApplyStartupHookPayload_Internal { +#endif + uint8_t * incoming_buffer; + + const ep_char16_t *startup_hook_path; +}; + +#if !defined(DS_INLINE_GETTER_SETTER) && !defined(DS_IMPL_PROCESS_PROTOCOL_GETTER_SETTER) +struct _DiagnosticsApplyStartupHookPayload { + uint8_t _internal [sizeof (struct _DiagnosticsApplyStartupHookPayload_Internal)]; +}; +#endif + +DiagnosticsApplyStartupHookPayload * +ds_apply_startup_hook_payload_alloc (void); + +void +ds_apply_startup_hook_payload_free (DiagnosticsApplyStartupHookPayload *payload); + /* * DiagnosticsProcessProtocolHelper. */ diff --git a/src/native/eventpipe/ds-rt.h b/src/native/eventpipe/ds-rt.h index f4a54b3d52df5d..ff33cf71b99869 100644 --- a/src/native/eventpipe/ds-rt.h +++ b/src/native/eventpipe/ds-rt.h @@ -118,6 +118,10 @@ static uint32_t ds_rt_disable_perfmap (void); +static +uint32_t +ds_rt_apply_startup_hook (const ep_char16_t *startup_hook_path); + /* * DiagnosticServer. */ diff --git a/src/native/eventpipe/ds-types.h b/src/native/eventpipe/ds-types.h index 85363f499e4046..16675059cf6cf4 100644 --- a/src/native/eventpipe/ds-types.h +++ b/src/native/eventpipe/ds-types.h @@ -24,6 +24,7 @@ typedef struct _DiagnosticsGenerateCoreDumpResponsePayload DiagnosticsGenerateCo typedef struct _DiagnosticsSetEnvironmentVariablePayload DiagnosticsSetEnvironmentVariablePayload; typedef struct _DiagnosticsGetEnvironmentVariablePayload DiagnosticsGetEnvironmentVariablePayload; typedef struct _DiagnosticsEnablePerfmapPayload DiagnosticsEnablePerfmapPayload; +typedef struct _DiagnosticsApplyStartupHookPayload DiagnosticsApplyStartupHookPayload; typedef struct _DiagnosticsIpcHeader DiagnosticsIpcHeader; typedef struct _DiagnosticsIpcMessage DiagnosticsIpcMessage; typedef struct _DiagnosticsListenPort DiagnosticsListenPort; @@ -74,7 +75,8 @@ typedef enum { DS_PROCESS_COMMANDID_SET_ENV_VAR = 0x03, DS_PROCESS_COMMANDID_GET_PROCESS_INFO_2 = 0x04, DS_PROCESS_COMMANDID_ENABLE_PERFMAP = 0x05, - DS_PROCESS_COMMANDID_DISABLE_PERFMAP = 0x06 + DS_PROCESS_COMMANDID_DISABLE_PERFMAP = 0x06, + DS_PROCESS_COMMANDID_APPLY_STARTUP_HOOK = 0x07 // future } DiagnosticsProcessCommandId; diff --git a/src/tests/Loader/StartupHooks/StartupHookTests.cs b/src/tests/Loader/StartupHooks/StartupHookTests.cs index 9bab54da74b162..e0801f335222f3 100644 --- a/src/tests/Loader/StartupHooks/StartupHookTests.cs +++ b/src/tests/Loader/StartupHooks/StartupHookTests.cs @@ -14,7 +14,7 @@ public unsafe class StartupHookTests private static Type s_startupHookProvider = typeof(object).Assembly.GetType("System.StartupHookProvider", throwOnError: true); - private static delegate* ProcessStartupHooks = (delegate*)s_startupHookProvider.GetMethod("ProcessStartupHooks", BindingFlags.NonPublic | BindingFlags.Static).MethodHandle.GetFunctionPointer(); + private static delegate* ProcessStartupHooks = (delegate*)s_startupHookProvider.GetMethod("ProcessStartupHooks", BindingFlags.NonPublic | BindingFlags.Static).MethodHandle.GetFunctionPointer(); private static bool IsUnsupportedPlatform = // these platforms need special setup for startup hooks @@ -38,7 +38,7 @@ public static void ValidHookName() hook.CallCount = 0; Assert.Equal(0, hook.CallCount); - ProcessStartupHooks(); + ProcessStartupHooks(string.Empty); Assert.Equal(1, hook.CallCount); } @@ -54,7 +54,7 @@ public static void ValidHookPath() hook.CallCount = 0; Assert.Equal(0, hook.CallCount); - ProcessStartupHooks(); + ProcessStartupHooks(string.Empty); Assert.Equal(1, hook.CallCount); } @@ -73,7 +73,47 @@ public static void MultipleValidHooksAndSeparators() Assert.Equal(0, hook1.CallCount); Assert.Equal(0, hook2.CallCount); - ProcessStartupHooks(); + ProcessStartupHooks(string.Empty); + Assert.Equal(1, hook1.CallCount); + Assert.Equal(1, hook2.CallCount); + } + + [Fact] + public static void MultipleValidDiagnosticHooksAndSeparators() + { + Console.WriteLine($"Running {nameof(MultipleValidDiagnosticHooksAndSeparators)}..."); + + Hook hook1 = Hook.Basic; + Hook hook2 = Hook.PrivateInitialize; + // Use multiple diagnostic hooks with an empty entry and leading/trailing separators + string diagnosticStartupHooks = $"{Path.PathSeparator}{hook1.Value}{Path.PathSeparator}{Path.PathSeparator}{hook2.Value}{Path.PathSeparator}"; + + AppContext.SetData(StartupHookKey, null); + hook1.CallCount = 0; + hook2.CallCount = 0; + + Assert.Equal(0, hook1.CallCount); + Assert.Equal(0, hook2.CallCount); + ProcessStartupHooks(diagnosticStartupHooks); + Assert.Equal(1, hook1.CallCount); + Assert.Equal(1, hook2.CallCount); + } + + [Fact] + public static void MultipleValidDiagnosticAndStandardHooks() + { + Console.WriteLine($"Running {nameof(MultipleValidDiagnosticAndStandardHooks)}..."); + + Hook hook1 = Hook.Basic; + Hook hook2 = Hook.PrivateInitialize; + + AppContext.SetData(StartupHookKey, hook2.Value); + hook1.CallCount = 0; + hook2.CallCount = 0; + + Assert.Equal(0, hook1.CallCount); + Assert.Equal(0, hook2.CallCount); + ProcessStartupHooks(hook1.Value); Assert.Equal(1, hook1.CallCount); Assert.Equal(1, hook2.CallCount); } @@ -89,7 +129,7 @@ public static void MissingAssembly(bool useAssemblyName) AppContext.SetData(StartupHookKey, $"{Hook.Basic.Value}{Path.PathSeparator}{hook}"); Hook.Basic.CallCount = 0; - var ex = Assert.Throws(() => ProcessStartupHooks()); + var ex = Assert.Throws(() => ProcessStartupHooks(string.Empty)); Assert.Equal($"Startup hook assembly '{hook}' failed to load. See inner exception for details.", ex.Message); Assert.IsType(ex.InnerException); @@ -109,7 +149,7 @@ public static void InvalidAssembly() AppContext.SetData(StartupHookKey, $"{Hook.Basic.Value}{Path.PathSeparator}{hook}"); Hook.Basic.CallCount = 0; - var ex = Assert.Throws(() => ProcessStartupHooks()); + var ex = Assert.Throws(() => ProcessStartupHooks(string.Empty)); Assert.Equal($"Startup hook assembly '{hook}' failed to load. See inner exception for details.", ex.Message); var innerEx = ex.InnerException; Assert.IsType(ex.InnerException); @@ -142,7 +182,7 @@ public static void InvalidSimpleAssemblyName(string name, bool failsSimpleNameCh AppContext.SetData(StartupHookKey, $"{Hook.Basic.Value}{Path.PathSeparator}{name}"); Hook.Basic.CallCount = 0; - var ex = Assert.Throws(() => ProcessStartupHooks()); + var ex = Assert.Throws(() => ProcessStartupHooks(string.Empty)); Assert.StartsWith($"The startup hook simple assembly name '{name}' is invalid.", ex.Message); if (failsSimpleNameCheck) { @@ -167,7 +207,7 @@ public static void MissingStartupHookType() var asm = typeof(StartupHookTests).Assembly; string hook = asm.Location; AppContext.SetData(StartupHookKey, hook); - var ex = Assert.Throws(() => ProcessStartupHooks()); + var ex = Assert.Throws(() => ProcessStartupHooks(string.Empty)); Assert.StartsWith($"Could not load type 'StartupHook' from assembly '{asm.GetName().Name}", ex.Message); } @@ -177,7 +217,7 @@ public static void MissingInitializeMethod() Console.WriteLine($"Running {nameof(MissingInitializeMethod)}..."); AppContext.SetData(StartupHookKey, Hook.NoInitializeMethod.Value); - var ex = Assert.Throws(() => ProcessStartupHooks()); + var ex = Assert.Throws(() => ProcessStartupHooks(string.Empty)); Assert.Equal($"Method 'StartupHook.Initialize' not found.", ex.Message); } @@ -196,7 +236,7 @@ public static void IncorrectInitializeSignature(Hook hook) Console.WriteLine($"Running {nameof(IncorrectInitializeSignature)}({hook.Name})..."); AppContext.SetData(StartupHookKey, hook.Value); - var ex = Assert.Throws(() => ProcessStartupHooks()); + var ex = Assert.Throws(() => ProcessStartupHooks(string.Empty)); Assert.Equal($"The signature of the startup hook 'StartupHook.Initialize' in assembly '{hook.Value}' was invalid. It must be 'public static void Initialize()'.", ex.Message); } } diff --git a/src/tests/tracing/eventpipe/applystartuphook/ApplyStartupHookValidation.cs b/src/tests/tracing/eventpipe/applystartuphook/ApplyStartupHookValidation.cs new file mode 100644 index 00000000000000..c62579fbdd3790 --- /dev/null +++ b/src/tests/tracing/eventpipe/applystartuphook/ApplyStartupHookValidation.cs @@ -0,0 +1,124 @@ +// 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.Tracing; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Diagnostics.Tools.RuntimeClient; +using Microsoft.Diagnostics.Tracing; +using Tracing.Tests.Common; +using DiagnosticsClient = Microsoft.Diagnostics.NETCore.Client.DiagnosticsClient; + +namespace Tracing.Tests.ApplyStartupHookValidation +{ + public class ApplyStartupHookValidation + { + public static async Task TEST_ApplyStartupHookAtStartupSuspension() + { + bool fSuccess = true; + string serverName = ReverseServer.MakeServerAddress(); + Logger.logger.Log($"Server name is '{serverName}'"); + Task subprocessTask = Utils.RunSubprocess( + currentAssembly: Assembly.GetExecutingAssembly(), + environment: new Dictionary + { + { Utils.DiagnosticPortsEnvKey, serverName } + }, + duringExecution: async (_) => + { + 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}"); + + 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()}"); + } + + 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}"); + + Logger.logger.Log($"Send ResumeRuntime Diagnostics IPC Command"); + // send ResumeRuntime command (0x04=ProcessCommandSet, 0x01=ResumeRuntime commandid) + IpcMessage message = new(0x04,0x01); + Logger.logger.Log($"Sent: {message.ToString()}"); + IpcMessage response = IpcClient.SendMessage(stream, message); + Logger.logger.Log($"Received: {response.ToString()}"); + } + } + ); + + fSuccess &= await subprocessTask; + + return fSuccess; + } + + private static IpcMessage CreateApplyStartupHookMessage(string startupHookPath) + { + if (string.IsNullOrEmpty(startupHookPath)) + throw new ArgumentException($"{nameof(startupHookPath)} required"); + + byte[] serializedConfiguration = DiagnosticsClient.SerializePayload(startupHookPath); + return new IpcMessage(0x04, 0x07, serializedConfiguration); + } + + public static async Task Main(string[] args) + { + if (args.Length >= 1) + { + 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}'"); + + // Validate the startup hook was executed + int callCount = Hook.Basic.CallCount; + Console.Out.WriteLine($"Startup hook call count: {callCount}"); + return callCount > 0 ? 0 : -1; + } + + bool fSuccess = true; + if (!IpcTraceTest.EnsureCleanEnvironment()) + return -1; + IEnumerable tests = typeof(ApplyStartupHookValidation).GetMethods().Where(mi => mi.Name.StartsWith("TEST_")); + foreach (var test in tests) + { + Logger.logger.Log($"::== Running test: {test.Name}"); + bool result = true; + try + { + result = await (Task)test.Invoke(null, new object[] {}); + } + catch (Exception e) + { + result = false; + Logger.logger.Log(e.ToString()); + } + fSuccess &= result; + Logger.logger.Log($"Test passed: {result}"); + Logger.logger.Log($""); + + } + return fSuccess ? 100 : -1; + } + } +} diff --git a/src/tests/tracing/eventpipe/applystartuphook/Hook.cs b/src/tests/tracing/eventpipe/applystartuphook/Hook.cs new file mode 100644 index 00000000000000..bd49fc6c39a007 --- /dev/null +++ b/src/tests/tracing/eventpipe/applystartuphook/Hook.cs @@ -0,0 +1,59 @@ +// 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.IO; +using System.Reflection; + +public class Hook +{ + public static Hook Basic = new Hook(nameof(Basic)); + + public Hook(string name) + { + Name = name; + AssemblyPath = Path.Combine(AppContext.BaseDirectory, "hooks", $"{name}.dll"); + } + + public string Name { get; } + + public string AssemblyPath { get; } + + public unsafe int CallCount + { + get + { + if (TryGetCallCountProperty(out PropertyInfo callCount)) + { + delegate* getCallCount = (delegate*)callCount.GetMethod.MethodHandle.GetFunctionPointer(); + return getCallCount(); + } + + return 0; + } + } + + private bool TryGetCallCountProperty(out PropertyInfo callCount) + { + callCount = null; + Assembly asm = null; + foreach(Assembly loaded in AppDomain.CurrentDomain.GetAssemblies()) + { + if (loaded.GetName().Name == Name && loaded.Location == AssemblyPath) + { + asm = loaded; + break; + } + } + + if (asm == null) + return false; + + Type hook = asm.GetType("StartupHook"); + if (hook == null) + return false; + + callCount = hook.GetProperty(nameof(CallCount), BindingFlags.NonPublic | BindingFlags.Static); + return callCount != null; + } +} \ No newline at end of file diff --git a/src/tests/tracing/eventpipe/applystartuphook/applystartuphook.csproj b/src/tests/tracing/eventpipe/applystartuphook/applystartuphook.csproj new file mode 100644 index 00000000000000..54aa82d39fd6b6 --- /dev/null +++ b/src/tests/tracing/eventpipe/applystartuphook/applystartuphook.csproj @@ -0,0 +1,26 @@ + + + .NETCoreApp + exe + true + true + true + + true + true + true + + + + + + + + + + false + Content + Always + + + diff --git a/src/tests/tracing/eventpipe/applystartuphook/hooks/Basic.cs b/src/tests/tracing/eventpipe/applystartuphook/hooks/Basic.cs new file mode 100644 index 00000000000000..3603c747737d1c --- /dev/null +++ b/src/tests/tracing/eventpipe/applystartuphook/hooks/Basic.cs @@ -0,0 +1,21 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; + +internal class StartupHook +{ + private static int CallCount { get; set; } + + public static void Initialize() + { + // Normal success case with a simple startup hook. + Initialize(123); + } + + public static void Initialize(int input) + { + CallCount++; + Console.WriteLine($"-- Hello from startup hook with overload! Call count: {CallCount}"); + } +} diff --git a/src/tests/tracing/eventpipe/applystartuphook/hooks/Basic.csproj b/src/tests/tracing/eventpipe/applystartuphook/hooks/Basic.csproj new file mode 100644 index 00000000000000..7caea741aa659e --- /dev/null +++ b/src/tests/tracing/eventpipe/applystartuphook/hooks/Basic.csproj @@ -0,0 +1,9 @@ + + + Library + BuildOnly + + + + + diff --git a/src/tests/tracing/eventpipe/common/IpcUtils.cs b/src/tests/tracing/eventpipe/common/IpcUtils.cs index e2130f4525fe8e..9165a1348537e5 100644 --- a/src/tests/tracing/eventpipe/common/IpcUtils.cs +++ b/src/tests/tracing/eventpipe/common/IpcUtils.cs @@ -137,6 +137,7 @@ public static async Task RunSubprocess(Assembly currentAssembly, Dictionar Logger.logger.Log("Subprocess didn't exit in 5 seconds!"); } Logger.logger.Log($"SubProcess exited - Exit code: {process.ExitCode}"); + fSuccess &= process.ExitCode == 0; } catch (Exception e) { diff --git a/src/tests/tracing/eventpipe/common/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs b/src/tests/tracing/eventpipe/common/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs index 2d58ef8626d115..20d8ccfb5d2f14 100644 --- a/src/tests/tracing/eventpipe/common/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs +++ b/src/tests/tracing/eventpipe/common/Microsoft.Diagnostics.NETCore.Client/DiagnosticsClient/DiagnosticsClient.cs @@ -296,6 +296,20 @@ internal async Task> GetProcessEnvironmentAsync(Cance return await helper.ReadEnvironmentAsync(response.Continuation, token).ConfigureAwait(false); } + public void ApplyStartupHook(string startupHookPath) + { + IpcMessage message = CreateApplyStartupHookMessage(startupHookPath); + IpcMessage response = IpcClient.SendMessage(_endpoint, message); + ValidateResponseMessage(response, nameof(ApplyStartupHook)); + } + + internal async Task ApplyStartupHookAsync(string startupHookPath, CancellationToken token) + { + IpcMessage message = CreateApplyStartupHookMessage(startupHookPath); + IpcMessage response = await IpcClient.SendMessageAsync(_endpoint, message, token).ConfigureAwait(false); + ValidateResponseMessage(response, nameof(ApplyStartupHookAsync)); + } + /// /// Get all the active processes that can be attached to. /// @@ -364,7 +378,7 @@ private async Task TryGetProcessInfo2Async(CancellationToken token) return TryGetProcessInfo2FromResponse(response2, nameof(GetProcessInfoAsync)); } - private static byte[] SerializePayload(T arg) + public static byte[] SerializePayload(T arg) { using (var stream = new MemoryStream()) using (var writer = new BinaryWriter(stream)) @@ -541,6 +555,15 @@ private static IpcMessage CreateWriteDumpMessage(DumpCommandId command, DumpType return new IpcMessage(DiagnosticsServerCommandSet.Dump, (byte)command, payload); } + private static IpcMessage CreateApplyStartupHookMessage(string startupHookPath) + { + if (string.IsNullOrEmpty(startupHookPath)) + throw new ArgumentException($"{nameof(startupHookPath)} required"); + + byte[] serializedConfiguration = SerializePayload(startupHookPath); + return new IpcMessage(DiagnosticsServerCommandSet.Process, (byte)ProcessCommandId.ApplyStartupHook, serializedConfiguration); + } + private static ProcessInfo GetProcessInfoFromResponse(IpcResponse response, string operationName) { ValidateResponseMessage(response.Message, operationName); diff --git a/src/tests/tracing/eventpipe/common/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs b/src/tests/tracing/eventpipe/common/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs index 241acaee04eca8..d084272a481cba 100644 --- a/src/tests/tracing/eventpipe/common/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs +++ b/src/tests/tracing/eventpipe/common/Microsoft.Diagnostics.NETCore.Client/DiagnosticsIpc/IpcCommands.cs @@ -50,6 +50,7 @@ internal enum ProcessCommandId : byte ResumeRuntime = 0x01, GetProcessEnvironment = 0x02, SetEnvironmentVariable = 0x03, - GetProcessInfo2 = 0x04 + GetProcessInfo2 = 0x04, + ApplyStartupHook = 0x07 } }