Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve Fantomas Daemon detection #1987

Merged
merged 5 commits into from
Dec 1, 2021
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.fsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ let owner = "Anh-Dung Phan"
let tags =
"F# fsharp formatting beautifier indentation indenter"

let fantomasClientVersion = "0.4.0"
let fantomasClientVersion = "0.4.1"

// (<solutionFile>.sln is built during the building process)
let solutionFile = "fantomas"
Expand Down
3 changes: 2 additions & 1 deletion paket.dependencies
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,5 @@ group client

nuget FSharp.Core 5
nuget StreamJsonRpc
nuget Microsoft.SourceLink.GitHub copy_local: true
nuget Microsoft.SourceLink.GitHub copy_local: true
nuget SemanticVersioning
279 changes: 234 additions & 45 deletions paket.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/Fantomas.Client/Fantomas.Client.fsproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Version>0.4.0</Version>
<Version>0.4.1</Version>
<Description>Companion library to format using fantomas tool.</Description>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<WarnOn>3390;$(WarnOn)</WarnOn>
Expand Down
159 changes: 121 additions & 38 deletions src/Fantomas.Client/FantomasToolLocator.fs
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,18 @@ open System.ComponentModel
open System.Diagnostics
open System.IO
open System.Text.RegularExpressions
open Fantomas.Client.LSPFantomasServiceTypes
open System.Runtime.InteropServices
open StreamJsonRpc
open Fantomas.Client.LSPFantomasServiceTypes

let private alphaLowerThanFour version =
version = "4.6.0-alpha-001"
|| version = "4.6.0-alpha-002"
|| version = "4.6.0-alpha-003"
// Only 4.6.0-alpha-004 has daemon capabilities
let private supportedRange =
SemanticVersioning.Range(">=v4.6.0-alpha-004")

let private (|CompatibleVersion|_|) (version: string) =
let stripAlphaBeta = version.Split('-').[0]

match Version.TryParse stripAlphaBeta with
match SemanticVersioning.Version.TryParse version with
| true, parsedVersion ->
if parsedVersion.Major = 4
&& parsedVersion.Minor = 6
&& alphaLowerThanFour version then
// Only 4.6.0-alpha-004 has daemon capabilities
None
elif parsedVersion.Major >= 4
&& parsedVersion.Minor >= 6 then
if supportedRange.IsSatisfied parsedVersion then
Some version
else
None
Expand Down Expand Up @@ -141,33 +133,124 @@ let private (|CompatibleTool|_|) lines =
Option.map (snd >> FantomasVersion) tool
| _ -> None

let findFantomasTool (workingDir: Folder) : FantomasToolResult =
let localTools = runToolListCmd workingDir false
let private isWindows =
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)

// Find an executable fantomas-tool file on the PATH
let private fantomasVersionOnPath () : (FantomasExecutableFile * FantomasVersion) option =
let fantomasExecutableOnPathOpt =
match Option.ofObj (Environment.GetEnvironmentVariable("PATH")) with
| Some s -> s.Split([| if isWindows then ';' else ':' |], StringSplitOptions.RemoveEmptyEntries)
| None -> Array.empty
|> Seq.choose
(fun folder ->
if isWindows then
let fantomasExe = Path.Combine(folder, "fantomas.exe")

let fantomasToolExe =
Path.Combine(folder, "fantomas-tool.exe")

if File.Exists fantomasExe then
Some fantomasExe
elif File.Exists fantomasToolExe then
Some fantomasToolExe
else
None
else
let fantomas = Path.Combine(folder, "fantomas")
let fantomasTool = Path.Combine(folder, "fantomas-tool")

match localTools with
| Ok (CompatibleTool version) -> FoundLocalTool(workingDir, version)
| Error err -> DotNetListError err
| Ok _nonCompatibleLocalVersion ->
let globalTools = runToolListCmd workingDir true
if File.Exists fantomas then
Some fantomas
elif File.Exists fantomasTool then
Some fantomasTool
else
None)
|> Seq.tryHead

fantomasExecutableOnPathOpt
|> Option.bind
(fun fantomasExecutablePath ->
let processStart = ProcessStartInfo(fantomasExecutablePath)
processStart.Arguments <- "--version"
processStart.RedirectStandardOutput <- true
processStart.CreateNoWindow <- true
processStart.RedirectStandardOutput <- true
processStart.RedirectStandardError <- true
processStart.UseShellExecute <- false

match startProcess processStart with
| Ok p ->
p.WaitForExit()
let stdOut = p.StandardOutput.ReadToEnd()

stdOut
|> Option.ofObj
|> Option.map
(fun s ->
let version =
s
.ToLowerInvariant()
.Replace("fantomas", String.Empty)
.Trim()

FantomasExecutableFile(fantomasExecutablePath), FantomasVersion(version))
| Error (ProcessStartError.ExecutableFileNotFound _)
| Error (ProcessStartError.UnExpectedException _) -> None)

let findFantomasTool (workingDir: Folder) : Result<FantomasToolFound, FantomasToolError> =
// First try and find a local tool for the folder.
// Next see if there is a global tool.
// Lastly check if an executable `fantomas` is present on the PATH.
let localToolsListResult = runToolListCmd workingDir false

match localToolsListResult with
| Ok (CompatibleTool version) -> Ok(FantomasToolFound(version, FantomasToolStartInfo.LocalTool workingDir))
| Error err -> Error(FantomasToolError.DotNetListError err)
| Ok _localToolListResult ->
let globalToolsListResult = runToolListCmd workingDir true

match globalToolsListResult with
| Ok (CompatibleTool version) -> Ok(FantomasToolFound(version, FantomasToolStartInfo.GlobalTool))
| Error err -> Error(FantomasToolError.DotNetListError err)
| Ok _nonCompatibleGlobalVersion ->
let fantomasOnPathVersion = fantomasVersionOnPath ()

match fantomasOnPathVersion with
| Some (executableFile, FantomasVersion (CompatibleVersion version)) ->
Ok(FantomasToolFound((FantomasVersion(version)), FantomasToolStartInfo.ToolOnPath executableFile))
| _ -> Error FantomasToolError.NoCompatibleVersionFound

let createFor (startInfo: FantomasToolStartInfo) : Result<RunningFantomasTool, ProcessStartError> =
let processStart =
match startInfo with
| FantomasToolStartInfo.LocalTool (Folder workingDirectory) ->
let ps = ProcessStartInfo("dotnet")
ps.WorkingDirectory <- workingDirectory
ps.Arguments <- "fantomas --daemon"
ps
| FantomasToolStartInfo.GlobalTool ->
baronfel marked this conversation as resolved.
Show resolved Hide resolved
let userProfile =
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)

let fantomasExecutable =
let fileName =
if isWindows then
"fantomas.exe"
else
"fantomas"

match globalTools with
| Ok (CompatibleTool version) -> FoundGlobalTool(workingDir, version)
| Error err -> DotNetListError err
| Ok _nonCompatibleGlobalVersion -> NoCompatibleVersionFound
Path.Combine(userProfile, ".dotnet", "tools", fileName)

let createForWorkingDirectory
(Folder workingDirectory)
(isGlobal: bool)
: Result<RunningFantomasTool, ProcessStartError> =
let processStart =
if isGlobal then
ProcessStartInfo("fantomas")
else
ProcessStartInfo("dotnet")
let ps = ProcessStartInfo(fantomasExecutable)
ps.Arguments <- "--daemon"
ps
| FantomasToolStartInfo.ToolOnPath (FantomasExecutableFile executableFile) ->
let ps = ProcessStartInfo(executableFile)
ps.Arguments <- "--daemon"
ps

processStart.UseShellExecute <- false
processStart.Arguments <- sprintf "fantomas --daemon"
processStart.WorkingDirectory <- workingDirectory
processStart.RedirectStandardInput <- true
processStart.RedirectStandardOutput <- true
processStart.RedirectStandardError <- true
Expand All @@ -190,7 +273,7 @@ let createForWorkingDirectory
Ok
{ RpcClient = client
Process = daemonProcess
IsGlobal = isGlobal }
StartInfo = startInfo }
with
| ex ->
let error =
Expand Down
70 changes: 23 additions & 47 deletions src/Fantomas.Client/LSPFantomasService.fs
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ let private createAgent (ct: CancellationToken) =
let nextState =
match msg with
| GetDaemon (folder, replyChannel) ->
// get the version of that folder
// get the version for that folder
// look in the cache first
let versionFromCache = Map.tryFind folder state.FolderToVersion

Expand All @@ -33,13 +33,13 @@ let private createAgent (ct: CancellationToken) =

match daemon with
| Some daemon ->
// We have a daemon for the required version in the cache, check if we can still use it.
if daemon.Process.HasExited then
// weird situation where the process has crashed.
// Trying to reboot
(daemon :> IDisposable).Dispose()

let newDaemonResult =
createForWorkingDirectory folder daemon.IsGlobal
let newDaemonResult = createFor daemon.StartInfo

match newDaemonResult with
| Ok newDaemon ->
Expand All @@ -66,54 +66,30 @@ let private createAgent (ct: CancellationToken) =

state
| None ->
let version = findFantomasTool folder
// Try and find a version of fantomas daemon for our current folder
let fantomasToolResult: Result<FantomasToolFound, FantomasToolError> =
findFantomasTool folder

match version with
| FantomasToolResult.DotNetListError dntl ->
replyChannel.Reply(Error(GetDaemonError.DotNetToolListError dntl))
state
| FantomasToolResult.NoCompatibleVersionFound ->
// No compatible version was found
replyChannel.Reply(Error GetDaemonError.InCompatibleVersionFound)
state
| FantomasToolResult.FoundLocalTool (_, version) ->
match Map.tryFind version state.Daemons with
| Some daemon ->
replyChannel.Reply(Ok daemon.RpcClient)
state
| None ->
let createDaemonResult = createForWorkingDirectory folder false

match createDaemonResult with
| Ok daemon ->
replyChannel.Reply(Ok daemon.RpcClient)
match fantomasToolResult with
| Ok (FantomasToolFound (version, startInfo)) ->
let createDaemonResult = createFor startInfo

{ state with
Daemons = Map.add version daemon state.Daemons
FolderToVersion = Map.add folder version state.FolderToVersion }
| Error pse ->
replyChannel.Reply(Error(GetDaemonError.FantomasProcessStart pse))
state

| FantomasToolResult.FoundGlobalTool (_, version) ->
match Map.tryFind version state.Daemons with
| Some daemon ->
match createDaemonResult with
| Ok daemon ->
replyChannel.Reply(Ok daemon.RpcClient)
state
| None ->
let startDaemonResult = createForWorkingDirectory folder true

match startDaemonResult with
| Ok daemon ->
replyChannel.Reply(Ok daemon.RpcClient)

{ state with
Daemons = Map.add version daemon state.Daemons
FolderToVersion = Map.add folder version state.FolderToVersion }
| Error pse ->
replyChannel.Reply(Error(GetDaemonError.FantomasProcessStart pse))
state

{ state with
Daemons = Map.add version daemon state.Daemons
FolderToVersion = Map.add folder version state.FolderToVersion }
| Error pse ->
replyChannel.Reply(Error(GetDaemonError.FantomasProcessStart pse))
state
| Error FantomasToolError.NoCompatibleVersionFound ->
replyChannel.Reply(Error GetDaemonError.InCompatibleVersionFound)
state
| Error (FantomasToolError.DotNetListError dotNetToolListError) ->
replyChannel.Reply(Error(GetDaemonError.DotNetToolListError dotNetToolListError))
state
| Reset replyChannel ->
Map.toList state.Daemons
|> List.iter (fun (_, daemon) -> (daemon :> IDisposable).Dispose())
Expand Down
17 changes: 12 additions & 5 deletions src/Fantomas.Client/LSPFantomasServiceTypes.fs
Original file line number Diff line number Diff line change
Expand Up @@ -61,13 +61,19 @@ type FormatDocumentResponse =
Content = None }

type FantomasVersion = FantomasVersion of string

type FantomasExecutableFile = FantomasExecutableFile of string
type Folder = Folder of path: string

[<RequireQualifiedAccess>]
type FantomasToolStartInfo =
| LocalTool of workingDirectory: Folder
| GlobalTool
| ToolOnPath of executableFile: FantomasExecutableFile

type RunningFantomasTool =
{ Process: Process
RpcClient: JsonRpc
IsGlobal: bool }
StartInfo: FantomasToolStartInfo }

interface IDisposable with
member this.Dispose() : unit =
Expand Down Expand Up @@ -100,9 +106,10 @@ type DotNetToolListError =
| ProcessStartError of ProcessStartError
| ExitCodeNonZero of executableFile: string * arguments: string * exitCode: int * error: string

type FantomasToolResult =
| FoundLocalTool of (Folder * FantomasVersion)
| FoundGlobalTool of (Folder * FantomasVersion)
type FantomasToolFound = FantomasToolFound of version: FantomasVersion * startInfo: FantomasToolStartInfo

[<RequireQualifiedAccess>]
type FantomasToolError =
| NoCompatibleVersionFound
| DotNetListError of DotNetToolListError

Expand Down
3 changes: 2 additions & 1 deletion src/Fantomas.Client/paket.references
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
group client
FSharp.Core
StreamJsonRpc
Microsoft.SourceLink.GitHub
Microsoft.SourceLink.GitHub
SemanticVersioning