From d365ff2e866ff2b9684262d139e79ba1c4641f76 Mon Sep 17 00:00:00 2001 From: Anton Lapounov Date: Wed, 27 Jul 2022 23:38:38 -0700 Subject: [PATCH] Implement ControlledExecution API (#71661) --- docs/project/list-of-diagnostics.md | 1 + .../System.Private.CoreLib.csproj | 1 + .../Runtime/ControlledExecution.CoreCLR.cs | 159 +++++++++++++ .../src/System.Private.CoreLib.csproj | 1 + .../Runtime/ControlledExecution.NativeAot.cs | 16 ++ src/coreclr/vm/arm64/asmhelpers.asm | 27 +++ src/coreclr/vm/arm64/asmmacros.h | 18 ++ src/coreclr/vm/arm64/stubs.cpp | 6 - src/coreclr/vm/arm64/unixstubs.cpp | 5 + src/coreclr/vm/comsynchronizable.cpp | 27 ++- src/coreclr/vm/comsynchronizable.h | 2 + src/coreclr/vm/qcallentrypoints.cpp | 2 + src/coreclr/vm/threads.h | 2 +- src/coreclr/vm/threadsuspend.cpp | 11 +- .../Common/src/System/Obsoletions.cs | 3 + .../src/Resources/Strings.resx | 4 + .../System.Runtime/ref/System.Runtime.cs | 5 + .../tests/System.Runtime.Tests.csproj | 1 + .../Runtime/ControlledExecutionTests.cs | 221 ++++++++++++++++++ .../System.Private.CoreLib.csproj | 1 + .../Runtime/ControlledExecution.Mono.cs | 16 ++ 21 files changed, 516 insertions(+), 13 deletions(-) create mode 100644 src/coreclr/System.Private.CoreLib/src/System/Runtime/ControlledExecution.CoreCLR.cs create mode 100644 src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/ControlledExecution.NativeAot.cs create mode 100644 src/libraries/System.Runtime/tests/System/Runtime/ControlledExecutionTests.cs create mode 100644 src/mono/System.Private.CoreLib/src/System/Runtime/ControlledExecution.Mono.cs diff --git a/docs/project/list-of-diagnostics.md b/docs/project/list-of-diagnostics.md index 888f93dd0c90b..21100607e1731 100644 --- a/docs/project/list-of-diagnostics.md +++ b/docs/project/list-of-diagnostics.md @@ -100,6 +100,7 @@ The PR that reveals the implementation of the ` + diff --git a/src/coreclr/System.Private.CoreLib/src/System/Runtime/ControlledExecution.CoreCLR.cs b/src/coreclr/System.Private.CoreLib/src/System/Runtime/ControlledExecution.CoreCLR.cs new file mode 100644 index 0000000000000..3b419f87f8b52 --- /dev/null +++ b/src/coreclr/System.Private.CoreLib/src/System/Runtime/ControlledExecution.CoreCLR.cs @@ -0,0 +1,159 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Runtime.CompilerServices; +using System.Runtime.ExceptionServices; +using System.Runtime.InteropServices; +using System.Threading; + +namespace System.Runtime +{ + /// + /// Allows to run code and abort it asynchronously. + /// + public static partial class ControlledExecution + { + [ThreadStatic] + private static bool t_executing; + + /// + /// Runs code that may be aborted asynchronously. + /// + /// The delegate that represents the code to execute. + /// The cancellation token that may be used to abort execution. + /// The method is not supported on this platform. + /// The argument is null. + /// + /// The current thread is already running the method. + /// + /// The execution was aborted. + /// + /// This method enables aborting arbitrary managed code in a non-cooperative manner by throwing an exception + /// in the thread executing that code. While the exception may be caught by the code, it is re-thrown at the end + /// of `catch` blocks until the execution flow returns to the `ControlledExecution.Run` method. + /// Execution of the code is not guaranteed to abort immediately, or at all. This situation can occur, for + /// example, if a thread is stuck executing unmanaged code or the `catch` and `finally` blocks that are called as + /// part of the abort procedure, thereby indefinitely delaying the abort. Furthermore, execution may not be + /// aborted immediately if the thread is currently executing a `catch` or `finally` block. + /// Aborting code at an unexpected location may corrupt the state of data structures in the process and lead + /// to unpredictable results. For that reason, this method should not be used in production code and calling it + /// produces a compile-time warning. + /// + [Obsolete(Obsoletions.ControlledExecutionRunMessage, DiagnosticId = Obsoletions.ControlledExecutionRunDiagId, UrlFormat = Obsoletions.SharedUrlFormat)] + public static void Run(Action action, CancellationToken cancellationToken) + { + if (!OperatingSystem.IsWindows()) + { + throw new PlatformNotSupportedException(); + } + + ArgumentNullException.ThrowIfNull(action); + + // ControlledExecution.Run does not support nested invocations. If there's one already in flight + // on this thread, fail. + if (t_executing) + { + throw new InvalidOperationException(SR.InvalidOperation_NestedControlledExecutionRun); + } + + // Store the current thread so that it may be referenced by the Canceler.Cancel callback if one occurs. + Canceler canceler = new(Thread.CurrentThread); + + try + { + // Mark this thread as now running a ControlledExecution.Run to prevent recursive usage. + t_executing = true; + + // Register for aborting. From this moment until ctr.Unregister is called, this thread is subject to being + // interrupted at any moment. This could happen during the call to UnsafeRegister if cancellation has + // already been requested at the time of the registration. + CancellationTokenRegistration ctr = cancellationToken.UnsafeRegister(e => ((Canceler)e!).Cancel(), canceler); + try + { + // Invoke the caller's code. + action(); + } + finally + { + // This finally block may be cloned by JIT for the non-exceptional code flow. In that case the code + // below is not guarded against aborting. That is OK as the outer try block will catch the + // ThreadAbortException and call ResetAbortThread. + + // Unregister the callback. Unlike Dispose, Unregister will not block waiting for an callback in flight + // to complete, and will instead return false if the callback has already been invoked or is currently + // in flight. + if (!ctr.Unregister()) + { + // Wait until the callback has completed. Either the callback is already invoked and completed + // (in which case IsCancelCompleted will be true), or it may still be in flight. If it's in flight, + // the AbortThread call may be waiting for this thread to exit this finally block to exit, so while + // spinning waiting for the callback to complete, we also need to call ResetAbortThread in order to + // reset the flag the AbortThread call is polling in its waiting loop. + SpinWait sw = default; + while (!canceler.IsCancelCompleted) + { + ResetAbortThread(); + sw.SpinOnce(); + } + } + } + } + catch (ThreadAbortException tae) + { + // We don't want to leak ThreadAbortExceptions to user code. Instead, translate the exception into + // an OperationCanceledException, preserving stack trace details from the ThreadAbortException in + // order to aid in diagnostics and debugging. + OperationCanceledException e = cancellationToken.IsCancellationRequested ? new(cancellationToken) : new(); + if (tae.StackTrace is string stackTrace) + { + ExceptionDispatchInfo.SetRemoteStackTrace(e, stackTrace); + } + throw e; + } + finally + { + // Unmark this thread for recursion detection. + t_executing = false; + + if (cancellationToken.IsCancellationRequested) + { + // Reset an abort request that may still be pending on this thread. + ResetAbortThread(); + } + } + } + + [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "ThreadNative_Abort")] + private static partial void AbortThread(ThreadHandle thread); + + [LibraryImport(RuntimeHelpers.QCall, EntryPoint = "ThreadNative_ResetAbort")] + [SuppressGCTransition] + private static partial void ResetAbortThread(); + + private sealed class Canceler + { + private readonly Thread _thread; + private volatile bool _cancelCompleted; + + public Canceler(Thread thread) + { + _thread = thread; + } + + public bool IsCancelCompleted => _cancelCompleted; + + public void Cancel() + { + try + { + // Abort the thread executing the action (which may be the current thread). + AbortThread(_thread.GetNativeHandle()); + } + finally + { + _cancelCompleted = true; + } + } + } + } +} diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj b/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj index 941bb14e9e3e4..3a5a4df522a94 100644 --- a/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System.Private.CoreLib.csproj @@ -190,6 +190,7 @@ + diff --git a/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/ControlledExecution.NativeAot.cs b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/ControlledExecution.NativeAot.cs new file mode 100644 index 0000000000000..b2031db1cec0b --- /dev/null +++ b/src/coreclr/nativeaot/System.Private.CoreLib/src/System/Runtime/ControlledExecution.NativeAot.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; + +namespace System.Runtime +{ + public static class ControlledExecution + { + [Obsolete(Obsoletions.ControlledExecutionRunMessage, DiagnosticId = Obsoletions.ControlledExecutionRunDiagId, UrlFormat = Obsoletions.SharedUrlFormat)] + public static void Run(Action action, CancellationToken cancellationToken) + { + throw new PlatformNotSupportedException(); + } + } +} diff --git a/src/coreclr/vm/arm64/asmhelpers.asm b/src/coreclr/vm/arm64/asmhelpers.asm index aed9a9c02d82c..6b2042d41f9e3 100644 --- a/src/coreclr/vm/arm64/asmhelpers.asm +++ b/src/coreclr/vm/arm64/asmhelpers.asm @@ -20,6 +20,8 @@ #ifdef FEATURE_READYTORUN IMPORT DynamicHelperWorker #endif + IMPORT HijackHandler + IMPORT ThrowControlForThread #ifdef FEATURE_USE_SOFTWARE_WRITE_WATCH_FOR_GC_HEAP IMPORT g_sw_ww_table @@ -1028,6 +1030,31 @@ FaultingExceptionFrame_FrameOffset SETA SIZEOF__GSCookie MEND +; ------------------------------------------------------------------ +; +; Helpers for ThreadAbort exceptions +; + + NESTED_ENTRY RedirectForThreadAbort2,,HijackHandler + PROLOG_SAVE_REG_PAIR fp,lr, #-16! + + ; stack must be 16 byte aligned + CHECK_STACK_ALIGNMENT + + ; On entry: + ; + ; x0 = address of FaultingExceptionFrame + ; + ; Invoke the helper to setup the FaultingExceptionFrame and raise the exception + bl ThrowControlForThread + + ; ThrowControlForThread doesn't return. + EMIT_BREAKPOINT + + NESTED_END RedirectForThreadAbort2 + + GenerateRedirectedStubWithFrame RedirectForThreadAbort, RedirectForThreadAbort2 + ; ------------------------------------------------------------------ ; ResolveWorkerChainLookupAsmStub ; diff --git a/src/coreclr/vm/arm64/asmmacros.h b/src/coreclr/vm/arm64/asmmacros.h index 8bfa79d2d272a..4a0cb5dde49ab 100644 --- a/src/coreclr/vm/arm64/asmmacros.h +++ b/src/coreclr/vm/arm64/asmmacros.h @@ -154,6 +154,24 @@ __EndLabelName SETS "$FuncName":CC:"_End" MEND +;----------------------------------------------------------------------------- +; Macro used to check (in debug builds only) whether the stack is 16-bytes aligned (a requirement before calling +; out into C++/OS code). Invoke this directly after your prolog (if the stack frame size is fixed) or directly +; before a call (if you have a frame pointer and a dynamic stack). A breakpoint will be invoked if the stack +; is misaligned. +; + MACRO + CHECK_STACK_ALIGNMENT + +#ifdef _DEBUG + add x9, sp, xzr + tst x9, #15 + beq %F0 + EMIT_BREAKPOINT +0 +#endif + MEND + ;----------------------------------------------------------------------------- ; The Following sets of SAVE_*_REGISTERS expect the memory to be reserved and ; base address to be passed in $reg diff --git a/src/coreclr/vm/arm64/stubs.cpp b/src/coreclr/vm/arm64/stubs.cpp index ba99ae572fd72..4f680a051c0fc 100644 --- a/src/coreclr/vm/arm64/stubs.cpp +++ b/src/coreclr/vm/arm64/stubs.cpp @@ -918,12 +918,6 @@ PTR_CONTEXT GetCONTEXTFromRedirectedStubStackFrame(T_CONTEXT * pContext) return *ppContext; } -void RedirectForThreadAbort() -{ - // ThreadAbort is not supported in .net core - throw "NYI"; -} - #if !defined(DACCESS_COMPILE) FaultingExceptionFrame *GetFrameFromRedirectedStubStackFrame (DISPATCHER_CONTEXT *pDispatcherContext) { diff --git a/src/coreclr/vm/arm64/unixstubs.cpp b/src/coreclr/vm/arm64/unixstubs.cpp index d51902a949f26..9b313f8475f68 100644 --- a/src/coreclr/vm/arm64/unixstubs.cpp +++ b/src/coreclr/vm/arm64/unixstubs.cpp @@ -9,4 +9,9 @@ extern "C" { PORTABILITY_ASSERT("Implement for PAL"); } + + void RedirectForThreadAbort() + { + PORTABILITY_ASSERT("Implement for PAL"); + } }; diff --git a/src/coreclr/vm/comsynchronizable.cpp b/src/coreclr/vm/comsynchronizable.cpp index 839b957e9d300..1a2c96392e4fd 100644 --- a/src/coreclr/vm/comsynchronizable.cpp +++ b/src/coreclr/vm/comsynchronizable.cpp @@ -1096,15 +1096,38 @@ extern "C" BOOL QCALLTYPE ThreadNative_YieldThread() BOOL ret = FALSE; - BEGIN_QCALL + BEGIN_QCALL; ret = __SwitchToThread(0, CALLER_LIMITS_SPINNING); - END_QCALL + END_QCALL; return ret; } +extern "C" void QCALLTYPE ThreadNative_Abort(QCall::ThreadHandle thread) +{ + QCALL_CONTRACT; + + BEGIN_QCALL; + + thread->UserAbort(EEPolicy::TA_Safe, INFINITE); + + END_QCALL; +} + +// Unmark the current thread for a safe abort. +extern "C" void QCALLTYPE ThreadNative_ResetAbort() +{ + QCALL_CONTRACT_NO_GC_TRANSITION; + + Thread *pThread = GetThread(); + if (pThread->IsAbortRequested()) + { + pThread->UnmarkThreadForAbort(EEPolicy::TA_Safe); + } +} + FCIMPL0(INT32, ThreadNative::GetCurrentProcessorNumber) { FCALL_CONTRACT; diff --git a/src/coreclr/vm/comsynchronizable.h b/src/coreclr/vm/comsynchronizable.h index 889f99c6f3e72..98a2eba9175b2 100644 --- a/src/coreclr/vm/comsynchronizable.h +++ b/src/coreclr/vm/comsynchronizable.h @@ -103,6 +103,8 @@ extern "C" void QCALLTYPE ThreadNative_InformThreadNameChange(QCall::ThreadHandl extern "C" UINT64 QCALLTYPE ThreadNative_GetProcessDefaultStackSize(); extern "C" BOOL QCALLTYPE ThreadNative_YieldThread(); extern "C" UINT64 QCALLTYPE ThreadNative_GetCurrentOSThreadId(); +extern "C" void QCALLTYPE ThreadNative_Abort(QCall::ThreadHandle thread); +extern "C" void QCALLTYPE ThreadNative_ResetAbort(); #endif // _COMSYNCHRONIZABLE_H diff --git a/src/coreclr/vm/qcallentrypoints.cpp b/src/coreclr/vm/qcallentrypoints.cpp index 5b5f4c8a8ac1b..ddaa9a3e98444 100644 --- a/src/coreclr/vm/qcallentrypoints.cpp +++ b/src/coreclr/vm/qcallentrypoints.cpp @@ -209,6 +209,8 @@ static const Entry s_QCall[] = DllImportEntry(ThreadNative_InformThreadNameChange) DllImportEntry(ThreadNative_YieldThread) DllImportEntry(ThreadNative_GetCurrentOSThreadId) + DllImportEntry(ThreadNative_Abort) + DllImportEntry(ThreadNative_ResetAbort) DllImportEntry(ThreadPool_GetCompletedWorkItemCount) DllImportEntry(ThreadPool_RequestWorkerThread) DllImportEntry(ThreadPool_PerformGateActivities) diff --git a/src/coreclr/vm/threads.h b/src/coreclr/vm/threads.h index f50c53f528e21..59588b5dc6af9 100644 --- a/src/coreclr/vm/threads.h +++ b/src/coreclr/vm/threads.h @@ -2496,7 +2496,7 @@ class Thread public: void MarkThreadForAbort(EEPolicy::ThreadAbortTypes abortType); - void UnmarkThreadForAbort(); + void UnmarkThreadForAbort(EEPolicy::ThreadAbortTypes abortType = EEPolicy::TA_Rude); static ULONGLONG GetNextSelfAbortEndTime() { diff --git a/src/coreclr/vm/threadsuspend.cpp b/src/coreclr/vm/threadsuspend.cpp index e1b4a6628df55..558d1775976ac 100644 --- a/src/coreclr/vm/threadsuspend.cpp +++ b/src/coreclr/vm/threadsuspend.cpp @@ -1785,7 +1785,7 @@ void Thread::RemoveAbortRequestBit() } // Make sure that when AbortRequest bit is cleared, we also dec TrapReturningThreads count. -void Thread::UnmarkThreadForAbort() +void Thread::UnmarkThreadForAbort(EEPolicy::ThreadAbortTypes abortType /* = EEPolicy::TA_Rude */) { CONTRACTL { @@ -1794,11 +1794,14 @@ void Thread::UnmarkThreadForAbort() } CONTRACTL_END; - // Switch to COOP (for ClearAbortReason) before acquiring AbortRequestLock - GCX_COOP(); - AbortRequestLockHolder lh(this); + if (m_AbortType > (DWORD)abortType) + { + // Aborting at a higher level + return; + } + m_AbortType = EEPolicy::TA_None; m_AbortEndTime = MAXULONGLONG; m_RudeAbortEndTime = MAXULONGLONG; diff --git a/src/libraries/Common/src/System/Obsoletions.cs b/src/libraries/Common/src/System/Obsoletions.cs index f15282d020d7c..7c193dc133c0b 100644 --- a/src/libraries/Common/src/System/Obsoletions.cs +++ b/src/libraries/Common/src/System/Obsoletions.cs @@ -147,5 +147,8 @@ internal static class Obsoletions internal const string CryptoStringFactoryMessage = "Cryptographic factory methods accepting an algorithm name are obsolete. Use the parameterless Create factory method on the algorithm type instead."; internal const string CryptoStringFactoryDiagId = "SYSLIB0045"; + + internal const string ControlledExecutionRunMessage = "ControlledExecution.Run method may corrupt the process and should not be used in production code."; + internal const string ControlledExecutionRunDiagId = "SYSLIB0046"; } } diff --git a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx index a86cb88896594..1fe307f810650 100644 --- a/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx +++ b/src/libraries/System.Private.CoreLib/src/Resources/Strings.resx @@ -2500,6 +2500,10 @@ NativeOverlapped cannot be reused for multiple operations. + + The thread is already executing the ControlledExecution.Run method. + {Locked="ControlledExecution.Run"} + You cannot have more than one dynamic module in each dynamic assembly in this version of the runtime. diff --git a/src/libraries/System.Runtime/ref/System.Runtime.cs b/src/libraries/System.Runtime/ref/System.Runtime.cs index 4449a3fd35677..1c4378ee33b9b 100644 --- a/src/libraries/System.Runtime/ref/System.Runtime.cs +++ b/src/libraries/System.Runtime/ref/System.Runtime.cs @@ -12136,6 +12136,11 @@ public sealed partial class AssemblyTargetedPatchBandAttribute : System.Attribut public AssemblyTargetedPatchBandAttribute(string targetedPatchBand) { } public string TargetedPatchBand { get { throw null; } } } + public static partial class ControlledExecution + { + [System.ObsoleteAttribute("ControlledExecution.Run method may corrupt the process and should not be used in production code.", DiagnosticId = "SYSLIB0046", UrlFormat = "https://aka.ms/dotnet-warnings/{0}")] + public static void Run(System.Action action, System.Threading.CancellationToken cancellationToken) { throw null; } + } public partial struct DependentHandle : System.IDisposable { private object _dummy; diff --git a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj index 036977e8d25f8..8ff7eb9bc2675 100644 --- a/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj +++ b/src/libraries/System.Runtime/tests/System.Runtime.Tests.csproj @@ -226,6 +226,7 @@ + diff --git a/src/libraries/System.Runtime/tests/System/Runtime/ControlledExecutionTests.cs b/src/libraries/System.Runtime/tests/System/Runtime/ControlledExecutionTests.cs new file mode 100644 index 0000000000000..2d615b36ce904 --- /dev/null +++ b/src/libraries/System.Runtime/tests/System/Runtime/ControlledExecutionTests.cs @@ -0,0 +1,221 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; +using Xunit; + +// Disable warnings for ControlledExecution.Run +#pragma warning disable SYSLIB0046 + +namespace System.Runtime.Tests +{ + public class ControlledExecutionTests + { + private bool _startedExecution, _caughtException, _finishedExecution; + private Exception _exception; + private CancellationTokenSource _cts; + private volatile int _counter; + + // Tests cancellation on timeout. The ThreadAbortException must be mapped to OperationCanceledException. + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime), nameof(PlatformDetection.IsNotNativeAot))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/72703", TestPlatforms.AnyUnix)] + public void CancelOnTimeout() + { + var cts = new CancellationTokenSource(); + cts.CancelAfter(200); + RunTest(LengthyAction, cts.Token); + + Assert.True(_startedExecution); + Assert.True(_caughtException); + Assert.False(_finishedExecution); + Assert.IsType(_exception); + } + + // Tests that catch blocks are not aborted. The action catches the ThreadAbortException and throws an exception of a different type. + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime), nameof(PlatformDetection.IsNotNativeAot))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/72703", TestPlatforms.AnyUnix)] + public void CancelOnTimeout_ThrowFromCatch() + { + var cts = new CancellationTokenSource(); + cts.CancelAfter(200); + RunTest(LengthyAction_ThrowFromCatch, cts.Token); + + Assert.True(_startedExecution); + Assert.True(_caughtException); + Assert.False(_finishedExecution); + Assert.IsType(_exception); + } + + // Tests that finally blocks are not aborted. The action throws an exception from a finally block. + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime), nameof(PlatformDetection.IsNotNativeAot))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/72703", TestPlatforms.AnyUnix)] + public void CancelOnTimeout_ThrowFromFinally() + { + var cts = new CancellationTokenSource(); + cts.CancelAfter(200); + RunTest(LengthyAction_ThrowFromFinally, cts.Token); + + Assert.True(_startedExecution); + Assert.IsType(_exception); + } + + // Tests that finally blocks are not aborted. The action throws an exception from a try block. + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime), nameof(PlatformDetection.IsNotNativeAot))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/72703", TestPlatforms.AnyUnix)] + public void CancelOnTimeout_Finally() + { + var cts = new CancellationTokenSource(); + cts.CancelAfter(200); + RunTest(LengthyAction_Finally, cts.Token); + + Assert.True(_startedExecution); + Assert.True(_finishedExecution); + Assert.IsType(_exception); + } + + // Tests cancellation before calling the Run method + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime), nameof(PlatformDetection.IsNotNativeAot))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/72703", TestPlatforms.AnyUnix)] + public void CancelBeforeRun() + { + var cts = new CancellationTokenSource(); + cts.Cancel(); + Thread.Sleep(100); + RunTest(LengthyAction, cts.Token); + + Assert.IsType(_exception); + } + + // Tests cancellation by the action itself + [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsNotMonoRuntime), nameof(PlatformDetection.IsNotNativeAot))] + [ActiveIssue("https://github.com/dotnet/runtime/issues/72703", TestPlatforms.AnyUnix)] + public void CancelItself() + { + _cts = new CancellationTokenSource(); + RunTest(Action_CancelItself, _cts.Token); + + Assert.True(_startedExecution); + Assert.False(_finishedExecution); + Assert.IsType(_exception); + Assert.IsType(_exception.InnerException); + } + + private void RunTest(Action action, CancellationToken cancellationToken) + { + _startedExecution = _caughtException = _finishedExecution = false; + _exception = null; + + try + { + ControlledExecution.Run(action, cancellationToken); + } + catch (Exception e) + { + _exception = e; + } + } + + private void LengthyAction() + { + _startedExecution = true; + // Redirection via thread suspension is supported on Windows only. + // Make a call in the loop to allow redirection on other platforms. + bool sleep = !PlatformDetection.IsWindows; + + try + { + for (_counter = 0; _counter < int.MaxValue; _counter++) + { + if ((_counter & 0xfffff) == 0 && sleep) + { + Thread.Sleep(0); + } + } + } + catch + { + // Swallow all exceptions to verify that the exception is automatically rethrown + _caughtException = true; + } + + _finishedExecution = true; + } + + private void LengthyAction_ThrowFromCatch() + { + _startedExecution = true; + bool sleep = !PlatformDetection.IsWindows; + + try + { + for (_counter = 0; _counter < int.MaxValue; _counter++) + { + if ((_counter & 0xfffff) == 0 && sleep) + { + Thread.Sleep(0); + } + } + } + catch + { + _caughtException = true; + // The catch block must not be aborted + Thread.Sleep(100); + throw new TimeoutException(); + } + + _finishedExecution = true; + } + + private void LengthyAction_ThrowFromFinally() + { + _startedExecution = true; + + try + { + // Make sure to run the non-inlined finally + throw new Exception(); + } + finally + { + // The finally block must not be aborted + Thread.Sleep(400); + throw new TimeoutException(); + } + } + + private void LengthyAction_Finally() + { + _startedExecution = true; + + try + { + // Make sure to run the non-inlined finally + throw new TimeoutException(); + } + finally + { + // The finally block must not be aborted + Thread.Sleep(400); + _finishedExecution = true; + } + } + + private void Action_CancelItself() + { + _startedExecution = true; + + try + { + // Make sure to run the non-inlined finally + throw new TimeoutException(); + } + finally + { + // The finally block must be aborted + _cts.Cancel(); + _finishedExecution = true; + } + } + } +} diff --git a/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj b/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj index 4945fcc682af3..e9ffd3db21647 100644 --- a/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj +++ b/src/mono/System.Private.CoreLib/System.Private.CoreLib.csproj @@ -254,6 +254,7 @@ + diff --git a/src/mono/System.Private.CoreLib/src/System/Runtime/ControlledExecution.Mono.cs b/src/mono/System.Private.CoreLib/src/System/Runtime/ControlledExecution.Mono.cs new file mode 100644 index 0000000000000..b2031db1cec0b --- /dev/null +++ b/src/mono/System.Private.CoreLib/src/System/Runtime/ControlledExecution.Mono.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Threading; + +namespace System.Runtime +{ + public static class ControlledExecution + { + [Obsolete(Obsoletions.ControlledExecutionRunMessage, DiagnosticId = Obsoletions.ControlledExecutionRunDiagId, UrlFormat = Obsoletions.SharedUrlFormat)] + public static void Run(Action action, CancellationToken cancellationToken) + { + throw new PlatformNotSupportedException(); + } + } +}