From b0858277c9b4d4db1532b4326f592b5f57076d5c Mon Sep 17 00:00:00 2001 From: "Kevin Ransom (msft)" Date: Mon, 17 Apr 2023 13:10:08 -0700 Subject: [PATCH] Fix send to vsfsi when using older dotnet sdks (#15036) * Fix send to vsfsi when using older dotnet sdks * Update vsintegration/src/FSharp.VS.FSI/sessions.fs Co-authored-by: Tomas Grosup * Update vsintegration/src/FSharp.VS.FSI/fsiSessionToolWindow.fs Co-authored-by: Petr Pokorny * Update vsintegration/src/FSharp.VS.FSI/sessions.fs Co-authored-by: Tomas Grosup * Update vsintegration/src/FSharp.VS.FSI/sessions.fs Co-authored-by: Tomas Grosup * Feedback + fixes * cultureinvariant * Update sessions.fs --------- Co-authored-by: Tomas Grosup Co-authored-by: Petr Pokorny --- .../src/FSharp.VS.FSI/fsiSessionToolWindow.fs | 24 ++++-- vsintegration/src/FSharp.VS.FSI/sessions.fs | 81 ++++++++++++++----- 2 files changed, 80 insertions(+), 25 deletions(-) diff --git a/vsintegration/src/FSharp.VS.FSI/fsiSessionToolWindow.fs b/vsintegration/src/FSharp.VS.FSI/fsiSessionToolWindow.fs index 2b876ade1..53432cd84 100644 --- a/vsintegration/src/FSharp.VS.FSI/fsiSessionToolWindow.fs +++ b/vsintegration/src/FSharp.VS.FSI/fsiSessionToolWindow.fs @@ -300,10 +300,10 @@ type internal FsiToolWindow() as this = strHandle.Free() ) - let executeTextNoHistory sourceFile (text:string) = + let executeTextNoHistory sourceFile (text: unit -> string) = sessions.Ensure(sourceFile) textStream.DirectWriteLine() - sessions.SendInput(text) + sessions.SendInput(text()) setCursorAtEndOfBuffer() let executeUserInput() = @@ -530,19 +530,31 @@ type internal FsiToolWindow() as this = try showNoActivate() let directiveC = sprintf "# 1 \"stdin\"" (* stdin line number reset code *) - let text = "\n" + text + "\n" + directiveC + "\n;;\n" + let text() = "\n" + text + "\n" + directiveC + "\n;;\n" executeTextNoHistory null text with _ -> () + let hide () = + if sessions.SupportsInteractivePrompt then + """#interactiveprompt "hide" """ + else + "" + + let show () = + if sessions.SupportsInteractivePrompt then + """#interactiveprompt "show" """ + else + "" + let executeInteraction dbgBreak dir filename topLine (text:string) = - let interaction = $""" -#interactiveprompt "hide" + let interaction() = $""" +{ hide()} #silentCd @"{dir}";; {if dbgBreak then "#dbgbreak" else ""} #{topLine} @"{filename}" {text.ToString()} #1 "stdin" -#interactiveprompt "show";; +{ show()};; """ executeTextNoHistory filename interaction diff --git a/vsintegration/src/FSharp.VS.FSI/sessions.fs b/vsintegration/src/FSharp.VS.FSI/sessions.fs index c552c0eda..c30f2090c 100644 --- a/vsintegration/src/FSharp.VS.FSI/sessions.fs +++ b/vsintegration/src/FSharp.VS.FSI/sessions.fs @@ -3,6 +3,7 @@ module internal Microsoft.VisualStudio.FSharp.Interactive.Session open System +open System.Globalization open System.IO open System.Text open System.Diagnostics @@ -287,7 +288,7 @@ let fsiStartInfo channelName sourceFile = let nonNull = function null -> false | (s:string) -> true /// Represents an active F# Interactive process to which Visual Studio is connected via stdin/stdout/stderr and a remoting channel -type FsiSession(sourceFile: string) = +type FsiSession(sourceFile) = let randomSalt = System.Random() let channelName = let pid = Process.GetCurrentProcess().Id @@ -306,7 +307,6 @@ type FsiSession(sourceFile: string) = do cmdProcess.Start() |> ignore let mutable cmdProcessPid = cmdProcess.Id - let mutable trueProcessPid = if usingNetCore then None else Some cmdProcessPid let trueProcessIdFile = Path.GetTempFileName() + ".pid" let mutable seenPidJunkOutput = false @@ -348,7 +348,7 @@ type FsiSession(sourceFile: string) = }) do if usingNetCore then - inputQueue.Post($"""System.IO.File.WriteAllText(@"{trueProcessIdFile}", string (System.Diagnostics.Process.GetCurrentProcess().Id));;""") + inputQueue.Post($"""System.IO.File.WriteAllLines(@"{trueProcessIdFile}", [| string (System.Diagnostics.Process.GetCurrentProcess().Id); System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription|]);;""") do cmdProcess.EnableRaisingEvents <- true @@ -360,6 +360,51 @@ type FsiSession(sourceFile: string) = /// interrupt timeout in miliseconds let interruptTimeoutMS = 1000 + // system.runtime.interopservices.runtimeinformation.frameworkdescription + // https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.runtimeinformation.frameworkdescription?view=net-7.0 + let splitPidFile = + lazy + let mutable trueProcessPid = if usingNetCore then None else Some cmdProcessPid + let mutable trueFrameworkVersion = Single.MaxValue + let getTfmNumber (v: string) = + let arr = v.Split([| '.' |], 3) + match Single.TryParse(arr[0] + "." + arr[1], NumberStyles.Float, CultureInfo.InvariantCulture) with + | true, value -> value + | _ -> Single.MaxValue + + let frameworkVersion (frameworkDescription: string) = + let arr = frameworkDescription.Split([| ' ' |], 3) + + match arr[0], arr[1] with + | ".NET", "Core" when arr.Length >= 3 -> getTfmNumber arr[2] + | ".NET", "Framework" when arr.Length >= 3 -> Single.MaxValue + | ".NET", "Native" -> Single.MaxValue + | ".NET", _ when arr.Length >= 2 -> getTfmNumber arr[1] + | _ -> Single.MaxValue + + // When using .NET Core, allow up to 2 seconds to allow detection of process ID + // of inner process to complete on startup. The only scenario where we ask for the process ID immediately after + // process startup is when the user clicks "Start Debugging" before the process has started. + for i in 0..10 do + if SessionsProperties.fsiUseNetCore && trueProcessPid.IsNone then + if File.Exists(trueProcessIdFile) then + let lines = File.ReadAllLines trueProcessIdFile + trueProcessPid <- + if lines.Length <= 0 then + None + else + Some (lines[0] |> int) + trueFrameworkVersion <- + if lines.Length <= 1 then + Single.MaxValue + else + (frameworkVersion lines[1]) |> float32 + + File.Delete(trueProcessIdFile) + else + System.Threading.Thread.Sleep(200) + trueProcessPid, trueFrameworkVersion + // Create session object member _.Interrupt() = match timeoutApp "VFSI interrupt" interruptTimeoutMS (fun () -> client.Interrupt()) () with @@ -378,25 +423,18 @@ type FsiSession(sourceFile: string) = member _.SupportsInterrupt = not cmdProcess.HasExited - member _.ProcessID = - // When using .NET Core, allow up to 2 seconds to allow detection of process ID - // of inner process to complete on startup. The only scenario where we ask for the process ID immediately after - // process startup is when the user clicks "Start Debugging" before the process has started. - for i in 0..10 do - if SessionsProperties.fsiUseNetCore && trueProcessPid.IsNone then - if File.Exists(trueProcessIdFile) then - trueProcessPid <- Some (File.ReadAllText trueProcessIdFile |> int) - File.Delete(trueProcessIdFile) - else - System.Threading.Thread.Sleep(200) - - match trueProcessPid with - | None -> cmdProcessPid - | Some pid -> pid + member _.ProcessID = + match splitPidFile.Force() with + | None, _ -> cmdProcessPid + | Some pid, _ -> pid + + member _.SupportsInteractivePrompt = + let pid, version = splitPidFile.Force() + pid.IsNone || version >= 7.0f member _.ProcessArgs = procInfo.Arguments - member _.Kill() = + member _.Kill() = let verboseSession = false try if verboseSession then fsiOutput.Trigger ("Kill process " + cmdProcess.Id.ToString()) @@ -464,6 +502,11 @@ type FsiSessions() = | None -> -1 (* -1 assumed to never be a valid process ID *) | Some session -> session.ProcessID + member _.SupportsInteractivePrompt = + match sessionR with + | None -> false + | Some session -> session.SupportsInteractivePrompt + member _.ProcessArgs = match sessionR with | None -> "" -- GitLab