Skip to content

Commit 3229db8

Browse files
KevinRansomdsyme
andauthored
Implement Ctrl+C on coreclr (#13428)
* Ctrl+C on coreclr * Update ControlledExecution.fs Co-authored-by: Don Syme <[email protected]>
1 parent 4e29955 commit 3229db8

File tree

4 files changed

+142
-46
lines changed

4 files changed

+142
-46
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,10 +69,11 @@ ossreadme*.txt
6969
*.fsproj.user
7070
*.vbproj.user
7171
*.sln.DotSettings.user
72+
launchSettings.json
7273
*.log
7374
*.jrs
7475
*.chk
75-
*.bak
76+
*.bak
7677
*.vserr
7778
*.err
7879
*.orig

src/Compiler/FSharp.Compiler.Service.fsproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -463,9 +463,9 @@
463463
<Compile Include="Service\ServiceStructure.fs" />
464464
<Compile Include="Service\ServiceAnalysis.fsi" />
465465
<Compile Include="Service\ServiceAnalysis.fs" />
466+
<Compile Include="Interactive\ControlledExecution.fs" />
466467
<Compile Include="Interactive\fsi.fsi" />
467468
<Compile Include="Interactive\fsi.fs" />
468-
469469
<!-- A legacy resolver used to help with scripting diagnostics in the Visual Studio tools -->
470470
<Compile Include="Legacy\LegacyMSBuildReferenceResolver.fsi" Condition="'$(MonoPackaging)' != 'true'" />
471471
<Compile Include="Legacy\LegacyMSBuildReferenceResolver.fs" Condition="'$(MonoPackaging)' != 'true'" />
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) Microsoft Corporation. All Rights Reserved. See License.txt in the project root for license information.
2+
3+
// This wraps System.Runtime.CompilerServices.ControlledExecution
4+
// This class enables scripting engines such as Fsi to abort threads safely in the coreclr
5+
// This functionality will be introduced in .net 7.0.
6+
// because we continue to dupport older coreclrs and the windows desktop framework through netstandard2.0
7+
// we access the features using reflection
8+
9+
namespace FSharp.Compiler.Interactive
10+
11+
open System
12+
open System.Reflection
13+
open System.Threading
14+
15+
open Internal.Utilities.FSharpEnvironment
16+
17+
type internal ControlledExecution (thread:Thread) =
18+
19+
static let ceType: Type option =
20+
Option.ofObj (Type.GetType("System.Runtime.CompilerServices.ControlledExecution, System.Private.CoreLib", false))
21+
22+
static let threadType: Type option =
23+
Option.ofObj (typeof<Threading.Thread>)
24+
25+
static let ceConstructor: ConstructorInfo option =
26+
match ceType with
27+
| None -> None
28+
| Some t -> Option.ofObj (t.GetConstructor([|typeof<Action>|]))
29+
30+
static let ceRun: MethodInfo option =
31+
match ceType with
32+
| None -> None
33+
| Some t -> Option.ofObj (t.GetMethod("Run", [||]) )
34+
35+
static let ceTryAbort: MethodInfo option =
36+
match ceType with
37+
| None -> None
38+
| Some t -> Option.ofObj (t.GetMethod("TryAbort", [|typeof<TimeSpan>|]))
39+
40+
static let threadResetAbort: MethodInfo option =
41+
match isRunningOnCoreClr, threadType with
42+
| false, Some t -> Option.ofObj (t.GetMethod("ResetAbort", [||]))
43+
| _ -> None
44+
45+
let newInstance (action: Action) =
46+
match ceConstructor with
47+
| None -> None
48+
| Some c -> Option.ofObj (c.Invoke([|action|]))
49+
50+
let mutable instance = Unchecked.defaultof<obj option>
51+
52+
member this.Run(action: Action) =
53+
let newinstance = newInstance(action)
54+
match newinstance, ceRun with
55+
| Some inst, Some ceRun ->
56+
instance <- newinstance
57+
ceRun.Invoke(inst, [||]) |> ignore
58+
| _ -> action.Invoke()
59+
60+
member _.TryAbort(timeout: TimeSpan): bool =
61+
match isRunningOnCoreClr, instance, ceTryAbort with
62+
| _, Some instance, Some tryAbort -> tryAbort.Invoke(instance, [|timeout|]) :?> bool
63+
| false, _, _ -> thread.Abort(); true
64+
| true, _, _ -> true
65+
66+
member _.ResetAbort() =
67+
match thread, threadResetAbort with
68+
| thread, Some threadResetAbort -> threadResetAbort.Invoke(thread, [||]) |> ignore
69+
| _ -> ()
70+
71+
static member StripTargetInvocationException(exn: Exception) =
72+
match exn with
73+
| :? TargetInvocationException as e when not(isNull e.InnerException) ->
74+
ControlledExecution.StripTargetInvocationException(e.InnerException)
75+
| _ -> exn

src/Compiler/Interactive/fsi.fs

Lines changed: 64 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ open FSharp.Compiler.EditorServices
4747
open FSharp.Compiler.DiagnosticsLogger
4848
open FSharp.Compiler.Features
4949
open FSharp.Compiler.IlxGen
50+
open FSharp.Compiler.Interactive
5051
open FSharp.Compiler.InfoReader
5152
open FSharp.Compiler.IO
5253
open FSharp.Compiler.Lexhelp
@@ -2208,7 +2209,10 @@ type internal FsiInterruptControllerKillerThreadRequest =
22082209
| ExitRequest
22092210
| PrintInterruptRequest
22102211

2211-
type internal FsiInterruptController(fsiOptions: FsiCommandLineOptions, fsiConsoleOutput: FsiConsoleOutput) =
2212+
type internal FsiInterruptController(
2213+
fsiOptions: FsiCommandLineOptions,
2214+
controlledExecution: ControlledExecution,
2215+
fsiConsoleOutput: FsiConsoleOutput) =
22122216

22132217
let mutable stdinInterruptState = StdinNormal
22142218
let CTRL_C = 0
@@ -2240,7 +2244,12 @@ type internal FsiInterruptController(fsiOptions: FsiCommandLineOptions, fsiConso
22402244

22412245
member _.EventHandlers = ctrlEventHandlers
22422246

2243-
member controller.InstallKillThread(threadToKill:Thread, pauseMilliseconds:int) =
2247+
member _.ControlledExecution() = controlledExecution
2248+
2249+
member controller.InstallKillThread() =
2250+
// Compute how long to pause before a ThreadAbort is actually executed.
2251+
// A somewhat arbitrary choice.
2252+
let pauseMilliseconds = (if fsiOptions.Gui then 400 else 100)
22442253

22452254
// Fsi Interrupt handler
22462255
let raiseCtrlC() =
@@ -2259,8 +2268,11 @@ type internal FsiInterruptController(fsiOptions: FsiCommandLineOptions, fsiConso
22592268
if killThreadRequest = ThreadAbortRequest then
22602269
if progress then fsiConsoleOutput.uprintnfn "%s" (FSIstrings.SR.fsiAbortingMainThread())
22612270
killThreadRequest <- NoRequest
2262-
threadToKill.Abort()
2263-
()),Name="ControlCAbortThread")
2271+
let rec abortLoop n =
2272+
if n > 0 then
2273+
if not (controlledExecution.TryAbort(TimeSpan.FromSeconds(30))) then abortLoop (n-1)
2274+
abortLoop 3
2275+
()), Name="ControlCAbortThread")
22642276
killerThread.IsBackground <- true
22652277
killerThread.Start()
22662278

@@ -2853,24 +2865,35 @@ type FsiInteractionProcessor
28532865
/// Execute a single parsed interaction on the parser/execute thread.
28542866
let mainThreadProcessAction ctok action istate =
28552867
try
2856-
let tcConfig = TcConfig.Create(tcConfigB,validate=false)
2857-
if progress then fprintfn fsiConsoleOutput.Out "In mainThreadProcessAction...";
2858-
fsiInterruptController.InterruptAllowed <- InterruptCanRaiseException;
2859-
let res = action ctok tcConfig istate
2860-
fsiInterruptController.ClearInterruptRequest()
2861-
fsiInterruptController.InterruptAllowed <- InterruptIgnored;
2862-
res
2868+
let mutable result = Unchecked.defaultof<'a * FsiInteractionStepStatus>
2869+
fsiInterruptController.ControlledExecution().Run(
2870+
fun () ->
2871+
let tcConfig = TcConfig.Create(tcConfigB,validate=false)
2872+
if progress then fprintfn fsiConsoleOutput.Out "In mainThreadProcessAction..."
2873+
fsiInterruptController.InterruptAllowed <- InterruptCanRaiseException;
2874+
let res = action ctok tcConfig istate
2875+
fsiInterruptController.ClearInterruptRequest()
2876+
fsiInterruptController.InterruptAllowed <- InterruptIgnored
2877+
result <- res)
2878+
result
28632879
with
28642880
| :? ThreadAbortException ->
2865-
fsiInterruptController.ClearInterruptRequest()
2866-
fsiInterruptController.InterruptAllowed <- InterruptIgnored;
2867-
(try Thread.ResetAbort() with _ -> ());
2868-
(istate,CtrlC)
2881+
fsiInterruptController.ClearInterruptRequest()
2882+
fsiInterruptController.InterruptAllowed <- InterruptIgnored
2883+
fsiInterruptController.ControlledExecution().ResetAbort()
2884+
(istate,CtrlC)
2885+
2886+
| :? TargetInvocationException as e when (ControlledExecution.StripTargetInvocationException(e)).GetType().Name = "ThreadAbortException" ->
2887+
fsiInterruptController.ClearInterruptRequest()
2888+
fsiInterruptController.InterruptAllowed <- InterruptIgnored
2889+
fsiInterruptController.ControlledExecution().ResetAbort()
2890+
(istate,CtrlC)
2891+
28692892
| e ->
2870-
fsiInterruptController.ClearInterruptRequest()
2871-
fsiInterruptController.InterruptAllowed <- InterruptIgnored;
2872-
stopProcessingRecovery e range0;
2873-
istate, CompletedWithReportedError e
2893+
fsiInterruptController.ClearInterruptRequest()
2894+
fsiInterruptController.InterruptAllowed <- InterruptIgnored;
2895+
stopProcessingRecovery e range0;
2896+
istate, CompletedWithReportedError e
28742897

28752898
let mainThreadProcessParsedInteractions ctok diagnosticsLogger (action, istate) cancellationToken =
28762899
istate |> mainThreadProcessAction ctok (fun ctok tcConfig istate ->
@@ -3180,27 +3203,32 @@ let internal SpawnInteractiveServer
31803203
/// Repeatedly drive the event loop (e.g. Application.Run()) but catching ThreadAbortException and re-running.
31813204
///
31823205
/// This gives us a last chance to catch an abort on the main execution thread.
3183-
let internal DriveFsiEventLoop (fsi: FsiEvaluationSessionHostConfig, fsiConsoleOutput: FsiConsoleOutput) =
3206+
let internal DriveFsiEventLoop (fsi: FsiEvaluationSessionHostConfig, fsiInterruptController: FsiInterruptController, fsiConsoleOutput: FsiConsoleOutput) =
3207+
3208+
if progress then fprintfn fsiConsoleOutput.Out "GUI thread runLoop"
3209+
fsiInterruptController.InstallKillThread()
3210+
31843211
let rec runLoop() =
3185-
if progress then fprintfn fsiConsoleOutput.Out "GUI thread runLoop";
3212+
31863213
let restart =
31873214
try
3188-
// BLOCKING POINT: The GUI Thread spends most (all) of its time this event loop
3189-
if progress then fprintfn fsiConsoleOutput.Out "MAIN: entering event loop...";
3190-
fsi.EventLoopRun()
3215+
fsi.EventLoopRun()
31913216
with
3192-
| :? ThreadAbortException ->
3217+
| :? TargetInvocationException as e when (ControlledExecution.StripTargetInvocationException(e)).GetType().Name = "ThreadAbortException" ->
31933218
// If this TAE handler kicks it's almost certainly too late to save the
31943219
// state of the process - the state of the message loop may have been corrupted
3195-
fsiConsoleOutput.uprintnfn "%s" (FSIstrings.SR.fsiUnexpectedThreadAbortException());
3196-
(try Thread.ResetAbort() with _ -> ());
3220+
fsiInterruptController.ControlledExecution().ResetAbort()
31973221
true
3198-
// Try again, just case we can restart
3199-
| e ->
3200-
stopProcessingRecovery e range0;
3222+
| :? ThreadAbortException ->
3223+
// If this TAE handler kicks it's almost certainly too late to save the
3224+
// state of the process - the state of the message loop may have been corrupted
3225+
fsiInterruptController.ControlledExecution().ResetAbort()
32013226
true
3202-
// Try again, just case we can restart
3203-
if progress then fprintfn fsiConsoleOutput.Out "MAIN: exited event loop...";
3227+
| e ->
3228+
stopProcessingRecovery e range0
3229+
true
3230+
// Try again, just case we can restart
3231+
if progress then fprintfn fsiConsoleOutput.Out "MAIN: exited event loop..."
32043232
if restart then runLoop()
32053233

32063234
runLoop();
@@ -3380,7 +3408,9 @@ type FsiEvaluationSession (fsi: FsiEvaluationSessionHostConfig, argv:string[], i
33803408

33813409
let fsiDynamicCompiler = FsiDynamicCompiler(fsi, timeReporter, tcConfigB, tcLockObject, outWriter, tcImports, tcGlobals, fsiOptions, fsiConsoleOutput, fsiCollectible, niceNameGen, resolveAssemblyRef)
33823410

3383-
let fsiInterruptController = FsiInterruptController(fsiOptions, fsiConsoleOutput)
3411+
let controlledExecution = ControlledExecution(Thread.CurrentThread)
3412+
3413+
let fsiInterruptController = FsiInterruptController(fsiOptions, controlledExecution, fsiConsoleOutput)
33843414

33853415
let uninstallMagicAssemblyResolution = MagicAssemblyResolution.Install(tcConfigB, tcImports, fsiDynamicCompiler, fsiConsoleOutput)
33863416

@@ -3640,14 +3670,6 @@ type FsiEvaluationSession (fsi: FsiEvaluationSessionHostConfig, argv:string[], i
36403670
if fsiOptions.Interact then
36413671
// page in the type check env
36423672
fsiInteractionProcessor.LoadDummyInteraction(ctokStartup, diagnosticsLogger)
3643-
if progress then fprintfn fsiConsoleOutput.Out "MAIN: InstallKillThread!";
3644-
3645-
// Compute how long to pause before a ThreadAbort is actually executed.
3646-
// A somewhat arbitrary choice.
3647-
let pauseMilliseconds = (if fsiOptions.Gui then 400 else 100)
3648-
3649-
// Request that ThreadAbort interrupts be performed on this (current) thread
3650-
fsiInterruptController.InstallKillThread(Thread.CurrentThread, pauseMilliseconds)
36513673
if progress then fprintfn fsiConsoleOutput.Out "MAIN: got initial state, creating form";
36523674

36533675
#if !FX_NO_APP_DOMAINS
@@ -3657,12 +3679,10 @@ type FsiEvaluationSession (fsi: FsiEvaluationSessionHostConfig, argv:string[], i
36573679
| :? System.Exception as err -> x.ReportUnhandledExceptionSafe false err
36583680
| _ -> ())
36593681
#endif
3660-
36613682
fsiInteractionProcessor.LoadInitialFiles(ctokRun, diagnosticsLogger)
3662-
36633683
fsiInteractionProcessor.StartStdinReadAndProcessThread(diagnosticsLogger)
36643684

3665-
DriveFsiEventLoop (fsi, fsiConsoleOutput )
3685+
DriveFsiEventLoop (fsi, fsiInterruptController, fsiConsoleOutput)
36663686

36673687
else // not interact
36683688
if progress then fprintfn fsiConsoleOutput.Out "Run: not interact, loading initial files..."

0 commit comments

Comments
 (0)