From d6c3b09c4dea7ca3ed1bb3bae4cc733657d7b7b4 Mon Sep 17 00:00:00 2001 From: Ademar Gonzalez Date: Tue, 10 Sep 2024 16:09:56 -0400 Subject: [PATCH] clean up --- .paket/Paket.Restore.targets | 557 ++++++++++++++++ examples/CORS/Program.fs | 12 +- examples/Example/CounterDemo.fs | 10 +- examples/Example/Program.fs | 65 +- examples/Fibonacci/Program.fs | 3 +- examples/Load/Program.fs | 5 +- examples/Stream/Program.fs | 7 +- examples/WebSocket/Program.fs | 3 +- paket.dependencies | 4 + paket.lock | 616 +++++++++-------- src/Suave.IO/Program.fs | 22 +- src/Suave.IO/paket.references | 2 + src/Suave.Tests/Auth.fs | 23 +- src/Suave.Tests/CORS.fs | 9 +- src/Suave.Tests/Cookie.fs | 57 +- src/Suave.Tests/ExpectoExtensions.fs | 7 +- src/Suave.Tests/Program.fs | 14 +- src/Suave.Tests/Suave.Tests.fsproj | 2 - src/Suave.Tests/TestUtilities.fs | 5 +- src/Suave.Tests/Testing.fs | 14 +- src/Suave.Tests/Web.fs | 8 +- src/Suave/Authentication.fs | 6 - src/Suave/Combinators.fs | 156 ++--- src/Suave/Combinators.fsi | 31 +- src/Suave/Compression.fs | 6 +- src/Suave/ConnectionFacade.fs | 164 ++--- src/Suave/Cookie.fs | 38 +- src/Suave/Globals.fs | 16 + src/Suave/Http.fs | 7 +- src/Suave/Http.fsi | 6 +- src/Suave/HttpOutput.fs | 52 +- src/Suave/Proxy.fs | 11 +- src/Suave/Sockets/AsyncSocket.fs | 60 +- src/Suave/Sockets/Connection.fs | 43 +- src/Suave/Sockets/HttpReader.fs | 40 +- src/Suave/Sockets/TcpTransport.fs | 31 +- src/Suave/State.fs | 33 - src/Suave/Stream.fs | 9 +- src/Suave/Suave.fsproj | 1 - src/Suave/SuaveConfig.fs | 11 - src/Suave/Tcp.fs | 36 +- src/Suave/Utils/Logging.fs | 963 --------------------------- src/Suave/Web.fs | 8 - src/Suave/WebSocket.fs | 60 +- 44 files changed, 1268 insertions(+), 1965 deletions(-) create mode 100644 .paket/Paket.Restore.targets delete mode 100644 src/Suave/Utils/Logging.fs diff --git a/.paket/Paket.Restore.targets b/.paket/Paket.Restore.targets new file mode 100644 index 00000000..4deb15bc --- /dev/null +++ b/.paket/Paket.Restore.targets @@ -0,0 +1,557 @@ + + + + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + $(MSBuildVersion) + 15.0.0 + false + true + + true + $(MSBuildThisFileDirectory) + $(MSBuildThisFileDirectory)..\ + $(PaketRootPath)paket-files\paket.restore.cached + $(PaketRootPath)paket.lock + classic + proj + assembly + native + /Library/Frameworks/Mono.framework/Commands/mono + mono + + + $(PaketRootPath)paket.bootstrapper.exe + $(PaketToolsPath)paket.bootstrapper.exe + $([System.IO.Path]::GetDirectoryName("$(PaketBootStrapperExePath)"))\ + + "$(PaketBootStrapperExePath)" + $(MonoPath) --runtime=v4.0.30319 "$(PaketBootStrapperExePath)" + + + + + true + true + + + True + + + False + + $(BaseIntermediateOutputPath.TrimEnd('\').TrimEnd('\/')) + + + + + + + + + $(PaketRootPath)paket + $(PaketToolsPath)paket + + + + + + $(PaketRootPath)paket.exe + $(PaketToolsPath)paket.exe + + + + + + <_DotnetToolsJson Condition="Exists('$(PaketRootPath)/.config/dotnet-tools.json')">$([System.IO.File]::ReadAllText("$(PaketRootPath)/.config/dotnet-tools.json")) + <_ConfigContainsPaket Condition=" '$(_DotnetToolsJson)' != ''">$(_DotnetToolsJson.Contains('"paket"')) + <_ConfigContainsPaket Condition=" '$(_ConfigContainsPaket)' == ''">false + + + + + + + + + + + <_PaketCommand>dotnet paket + + + + + + $(PaketToolsPath)paket + $(PaketBootStrapperExeDir)paket + + + paket + + + + + <_PaketExeExtension>$([System.IO.Path]::GetExtension("$(PaketExePath)")) + <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(_PaketExeExtension)' == '.dll' ">dotnet "$(PaketExePath)" + <_PaketCommand Condition=" '$(_PaketCommand)' == '' AND '$(OS)' != 'Windows_NT' AND '$(_PaketExeExtension)' == '.exe' ">$(MonoPath) --runtime=v4.0.30319 "$(PaketExePath)" + <_PaketCommand Condition=" '$(_PaketCommand)' == '' ">"$(PaketExePath)" + + + + + + + + + + + + + + + + + + + + + true + $(NoWarn);NU1603;NU1604;NU1605;NU1608 + false + true + + + + + + + + + $([System.IO.File]::ReadAllText('$(PaketRestoreCacheFile)')) + + + + + + + $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[0].Replace(`"`, ``).Replace(` `, ``)) + $([System.Text.RegularExpressions.Regex]::Split(`%(Identity)`, `": "`)[1].Replace(`"`, ``).Replace(` `, ``)) + + + + + %(PaketRestoreCachedKeyValue.Value) + %(PaketRestoreCachedKeyValue.Value) + + + + + true + false + true + + + + + true + + + + + + + + + + + + + + + + + + + $(PaketIntermediateOutputPath)\$(MSBuildProjectFile).paket.references.cached + + $(MSBuildProjectFullPath).paket.references + + $(MSBuildProjectDirectory)\$(MSBuildProjectName).paket.references + + $(MSBuildProjectDirectory)\paket.references + + false + true + true + references-file-or-cache-not-found + + + + + $([System.IO.File]::ReadAllText('$(PaketReferencesCachedFilePath)')) + $([System.IO.File]::ReadAllText('$(PaketOriginalReferencesFilePath)')) + references-file + false + + + + + false + + + + + true + target-framework '$(TargetFramework)' or '$(TargetFrameworks)' files @(PaketResolvedFilePaths) + + + + + + + + + + + false + true + + + + + + + + + + + $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',').Length) + $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[0]) + $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[1]) + $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[4]) + $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[5]) + $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[6]) + $([System.String]::Copy('%(PaketReferencesFileLines.Identity)').Split(',')[7]) + + + %(PaketReferencesFileLinesInfo.PackageVersion) + All + runtime + $(ExcludeAssets);contentFiles + $(ExcludeAssets);build;buildMultitargeting;buildTransitive + true + true + + + + + $(PaketIntermediateOutputPath)/$(MSBuildProjectFile).paket.clitools + + + + + + + + + $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[0]) + $([System.String]::Copy('%(PaketCliToolFileLines.Identity)').Split(',')[1]) + + + %(PaketCliToolFileLinesInfo.PackageVersion) + + + + + + + + + + false + + + + + + <_NuspecFilesNewLocation Include="$(PaketIntermediateOutputPath)\$(Configuration)\*.nuspec"/> + + + + + + $(MSBuildProjectDirectory)/$(MSBuildProjectFile) + true + false + true + false + true + false + true + false + true + false + true + $(PaketIntermediateOutputPath)\$(Configuration) + $(PaketIntermediateOutputPath) + + + + <_NuspecFiles Include="$(AdjustedNuspecOutputPath)\*.$(PackageVersion.Split(`+`)[0]).nuspec"/> + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/CORS/Program.fs b/examples/CORS/Program.fs index 8de0ec3f..04cb4a94 100644 --- a/examples/CORS/Program.fs +++ b/examples/CORS/Program.fs @@ -1,27 +1,19 @@ -module Program +module Program -open System -open System.Net open Suave -open Suave.Logging open Suave.Filters -open Suave.Writers -open Suave.Files open Suave.Successful open Suave.CORS -open Suave.State.CookieStateStore -open Suave.Utils open Suave.Operators -let logger = Targets.create Verbose [||] let corsConfig = { defaultCORSConfig with allowedUris = InclusiveOption.Some [ "http://localhost:8085" ] } let app = choose [ GET >=> path "/hello" >=> cors corsConfig >=> OK "CORS request accepted." - ] >=> logStructured logger logFormatStructured + ] [] let main argv = diff --git a/examples/Example/CounterDemo.fs b/examples/Example/CounterDemo.fs index 710ba783..f7bfc8da 100644 --- a/examples/Example/CounterDemo.fs +++ b/examples/Example/CounterDemo.fs @@ -19,8 +19,6 @@ module private Helpers = open Suave open Suave.Sockets -open Suave.Sockets.Control -open Suave.Http open Suave.EventSource open Suave.Utils @@ -29,13 +27,13 @@ open Helpers let counterDemo (req : HttpRequest) (out : Connection) = let write i = - socket { + task { let msg = { id = i; data = string i; ``type`` = None } - do! msg |> send out - return! SocketOp.ofAsync (Async.Sleep 100) + let! _ = send out msg + return! Async.Sleep 100 } - socket { + task { let lastEvtId = (req.header "last-event-id" |> Choice.bind muint32) ((req.queryParam "lastEventId") |> Choice.bind muint32) <.> diff --git a/examples/Example/Program.fs b/examples/Example/Program.fs index 5364462b..7af49160 100644 --- a/examples/Example/Program.fs +++ b/examples/Example/Program.fs @@ -1,11 +1,8 @@ module Program open System -open System.Net open Suave -open Suave.Sockets.Control -open Suave.Logging open Suave.Operators open Suave.EventSource open Suave.Filters @@ -17,31 +14,6 @@ open Suave.State.CookieStateStore let basicAuth = Authentication.authenticateBasic ((=) ("foo", "bar")) -// This demonstrates how to customise the console logger output. -// In most cases you wont need this. Instead you can use the more succinct: -// `let logger = Targets.create Verbose [||]` -let loggingOptions = - { Literate.LiterateOptions.create() with - getLogLevelText = function Verbose->"V" | Debug->"D" | Info->"I" | Warn->"W" | Error->"E" | Fatal->"F" } - -let logger = LiterateConsoleTarget( - name = [|"Suave";"Examples";"Example"|], - minLevel = Verbose, - options = loggingOptions, - outputTemplate = "[{level}] {timestampUtc:o} {message} [{source}]{exceptions}" - ) :> Logger - -/// With this workflow you can write WebParts like this -let task : WebPart = - fun ctx -> WebPart.asyncOption { - let! ctx = GET ctx - let! ctx = Writers.setHeader "foo" "bar" ctx - return ctx - } - -/// we can still use the old symbol but now has a new meaning -let foo : WebPart = fun ctx -> GET ctx >>= OK "hello" - let myApp = choose [ GET >=> choose @@ -57,7 +29,6 @@ let myApp = // typed routes let testApp = choose [ - logStructured logger logFormatStructured >=> never pathScan "/add/%d/%d" (fun (a,b) -> OK((a + b).ToString())) pathScan "/minus/%d/%d" (fun (a,b) -> OK((a - b).ToString())) pathScan "/divide/%d/%d" (fun (a,b) -> OK((a / b).ToString())) @@ -86,9 +57,20 @@ let unzipBody : WebPart = else return ctx } -open System.IO open Suave.Sockets -open Suave.Sockets.Control +open System.IO + +let write (conn:Connection, _) = + task { + use ms = new MemoryStream() + ms.Write([| 1uy; 2uy; 3uy |], 0, 3) + ms.Seek(0L, SeekOrigin.Begin) |> ignore + // do things here + do! conn.asyncWriteLn (sprintf "Content-Length: %d\r\n" ms.Length) + do! conn.flush() + do! transferStream conn ms + return () +} let app = choose [ @@ -99,15 +81,7 @@ let app = path "/hello" >=> OK "Hello World" path "/byte-stream" >=> (fun ctx -> - let write (conn:Connection, _) = socket { - use ms = new MemoryStream() - ms.Write([| 1uy; 2uy; 3uy |], 0, 3) - ms.Seek(0L, SeekOrigin.Begin) |> ignore - // do things here - do! conn.asyncWriteLn (sprintf "Content-Length: %d\r\n" ms.Length) - do! conn.flush() - do! transferStream conn ms - } + { ctx with response = @@ -145,11 +119,12 @@ let app = basicAuth <| choose [ // from here on it will require authentication // surf to: http://localhost:8082/es.html to view the ES GET >=> path "/events2" >=> request (fun _ -> EventSource.handShake (fun out -> - socket { + task { let msg = { id = "1"; data = "First Message"; ``type`` = None } - do! msg |> send out + let! _ = send out msg let msg = { id = "2"; data = "Second Message"; ``type`` = None } - do! msg |> send out + let! _ = send out msg + () })) GET >=> path "/events" >=> request (fun r -> EventSource.handShake (CounterDemo.counterDemo r)) GET >=> browseHome //serves file if exists @@ -177,7 +152,7 @@ let app = >=> OK "Doooooge" RequestErrors.NOT_FOUND "Found no handlers" ] - ] >=> logStructured logger logFormatStructured + ] //>=> logStructured logger logFormatStructured open System.Security.Cryptography.X509Certificates @@ -195,7 +170,7 @@ let main argv = mimeTypesMap = mimeTypes homeFolder = None compressedFilesFolder = None - logger = logger + //logger = logger cookieSerialiser = new BinaryFormatterSerialiser() hideHeader = false maxContentLength = 1000000 } diff --git a/examples/Fibonacci/Program.fs b/examples/Fibonacci/Program.fs index 17216908..ae3cd66e 100644 --- a/examples/Fibonacci/Program.fs +++ b/examples/Fibonacci/Program.fs @@ -1,5 +1,4 @@ -open Suave -open Suave.Http +open Suave open Suave.Filters open Suave.Successful diff --git a/examples/Load/Program.fs b/examples/Load/Program.fs index 16bc8fe5..fa0cdc1b 100644 --- a/examples/Load/Program.fs +++ b/examples/Load/Program.fs @@ -3,20 +3,17 @@ open System.Net open Suave open Suave.Operators -open Suave.Http open Suave.Filters open Suave.Files -open Suave.Logging open System.Threading.Tasks -let logger = Targets.create Verbose [||] let config = { defaultConfig with bindings = [ HttpBinding.createSimple HTTP "127.0.0.1" 8082 ] bufferSize = 2048 maxOps = 10000 - logger = logger } + } let listening, server = startWebServerAsync config (choose [ GET >=> browseHome ]) Task.WaitAll server diff --git a/examples/Stream/Program.fs b/examples/Stream/Program.fs index b9f74a9a..b2a47196 100644 --- a/examples/Stream/Program.fs +++ b/examples/Stream/Program.fs @@ -1,15 +1,12 @@ -module Program +module Program open System.IO open Suave -open Suave.Logging open Suave.Filters open Suave.Stream open Suave.Operators -let logger = Targets.create Verbose [||] - let makeStream = async { let fileStream = File.Open("./kandinsky-composition-8.jpg", FileMode.Open, FileAccess.Read, FileShare.Read) @@ -20,7 +17,7 @@ let makeStream = let app = choose [ GET >=> path "/art" >=> Writers.setMimeType "image/jpeg" >=> okStream makeStream - ] >=> logStructured logger logFormatStructured + ] [] let main argv = diff --git a/examples/WebSocket/Program.fs b/examples/WebSocket/Program.fs index 9459c1e7..22e93e76 100644 --- a/examples/WebSocket/Program.fs +++ b/examples/WebSocket/Program.fs @@ -1,4 +1,3 @@ - open Suave open Suave.Http open Suave.Operators @@ -89,7 +88,7 @@ let app : WebPart = [] let main _ = - startWebServer { defaultConfig with logger = Targets.create Verbose [||] } app + startWebServer defaultConfig app 0 // diff --git a/paket.dependencies b/paket.dependencies index 2b5d5fac..97d9edc4 100644 --- a/paket.dependencies +++ b/paket.dependencies @@ -20,6 +20,8 @@ group Examples nuget FSharp.Core nuget Topshelf.FSharp nuget Topshelf + nuget Microsoft.Extensions.Logging + nuget Microsoft.Extensions.Logging.Console group Docs source https://api.nuget.org/v3/index.json @@ -28,6 +30,8 @@ group Docs nuget Argu nuget FSharp.Core nuget FsLibTool + nuget Microsoft.Extensions.Logging + nuget Microsoft.Extensions.Logging.Console group SourceLink source https://api.nuget.org/v3/index.json diff --git a/paket.lock b/paket.lock index 13fa57f3..faf20c65 100644 --- a/paket.lock +++ b/paket.lock @@ -2,132 +2,212 @@ STORAGE: NONE RESTRICTION: == net7.0 NUGET remote: https://api.nuget.org/v3/index.json - BenchmarkDotNet (0.13.5) - BenchmarkDotNet.Annotations (>= 0.13.5) - CommandLineParser (>= 2.4.3) + BenchmarkDotNet (0.13.12) + BenchmarkDotNet.Annotations (>= 0.13.12) + CommandLineParser (>= 2.9.1) Gee.External.Capstone (>= 2.3) Iced (>= 1.17) - Microsoft.CodeAnalysis.CSharp (>= 3.0) + Microsoft.CodeAnalysis.CSharp (>= 4.1) Microsoft.Diagnostics.Runtime (>= 2.2.332302) Microsoft.Diagnostics.Tracing.TraceEvent (>= 3.0.2) Microsoft.DotNet.PlatformAbstractions (>= 3.1.6) - Perfolizer (>= 0.2.1) - System.Management (>= 6.0) - BenchmarkDotNet.Annotations (0.13.5) + Perfolizer (0.2.1) + System.Management (>= 5.0) + BenchmarkDotNet.Annotations (0.14) CommandLineParser (2.9.1) - DotLiquid (2.2.685) - Expecto (9.0.4) - FSharp.Core (>= 4.6) - Mono.Cecil (>= 0.11.3) - Expecto.BenchmarkDotNet (9.0.4) - BenchmarkDotNet (>= 0.12.1) - FSharp.Core (>= 4.6) - Expecto.FsCheck (9.0.4) - Expecto (>= 9.0.4) - FsCheck (>= 2.14.3) - FsCheck (2.16.5) + DotLiquid (2.2.692) + Expecto (10.2.1) + FSharp.Core (>= 7.0.200) + Mono.Cecil (>= 0.11.4 < 1.0) + Expecto.BenchmarkDotNet (10.2.1) + BenchmarkDotNet (>= 0.13.5 < 0.14) + FSharp.Core (>= 7.0.200) + Expecto.FsCheck (10.2.1) + Expecto (>= 10.2.1) + FsCheck (>= 2.16.5 < 3.0) + FsCheck (2.16.6) FSharp.Core (>= 4.2.3) - FSharp.Core (7.0.200) + FSharp.Core (8.0.400) Gee.External.Capstone (2.3) - Iced (1.18) - Microsoft.Bcl.AsyncInterfaces (7.0) + Iced (1.21) Microsoft.CodeAnalysis.Analyzers (3.3.4) - Microsoft.CodeAnalysis.Common (4.5) - Microsoft.CodeAnalysis.Analyzers (>= 3.3.3) - System.Collections.Immutable (>= 6.0) - System.Reflection.Metadata (>= 6.0.1) + Microsoft.CodeAnalysis.Common (4.11) + Microsoft.CodeAnalysis.Analyzers (>= 3.3.4) + System.Collections.Immutable (>= 8.0) + System.Reflection.Metadata (>= 8.0) + Microsoft.CodeAnalysis.CSharp (4.11) + Microsoft.CodeAnalysis.Analyzers (>= 3.3.4) + Microsoft.CodeAnalysis.Common (4.11) + System.Collections.Immutable (>= 8.0) + System.Reflection.Metadata (>= 8.0) + Microsoft.Diagnostics.NETCore.Client (0.2.532401) + Microsoft.Extensions.Logging.Abstractions (>= 6.0.4) + Microsoft.Diagnostics.Runtime (3.1.512801) + Microsoft.Diagnostics.NETCore.Client (>= 0.2.410101) + Microsoft.Diagnostics.Tracing.TraceEvent (3.1.15) + Microsoft.Diagnostics.NETCore.Client (>= 0.2.510501) + Microsoft.Win32.Registry (>= 5.0) + System.Collections.Immutable (>= 8.0) + System.Reflection.Metadata (>= 8.0) + System.Reflection.TypeExtensions (>= 4.7) System.Runtime.CompilerServices.Unsafe (>= 6.0) - System.Text.Encoding.CodePages (>= 6.0) - Microsoft.CodeAnalysis.CSharp (4.5) - Microsoft.CodeAnalysis.Common (4.5) - Microsoft.Diagnostics.NETCore.Client (0.2.410101) - Microsoft.Bcl.AsyncInterfaces (>= 1.1) - Microsoft.Extensions.Logging (>= 2.1.1) - Microsoft.Diagnostics.Runtime (2.3.405304) - Microsoft.Diagnostics.NETCore.Client (>= 0.2.251802) - System.Collections.Immutable (>= 5.0) - System.Runtime.CompilerServices.Unsafe (>= 5.0) - Microsoft.Diagnostics.Tracing.TraceEvent (3.0.8) - System.Runtime.CompilerServices.Unsafe (>= 5.0) Microsoft.DotNet.PlatformAbstractions (3.1.6) - Microsoft.Extensions.DependencyInjection (7.0) - Microsoft.Extensions.DependencyInjection.Abstractions (>= 7.0) - Microsoft.Extensions.DependencyInjection.Abstractions (7.0) - Microsoft.Extensions.FileProviders.Abstractions (7.0) - Microsoft.Extensions.Primitives (>= 7.0) - Microsoft.Extensions.FileProviders.Embedded (7.0.10) - Microsoft.Extensions.FileProviders.Abstractions (>= 7.0) - Microsoft.Extensions.Logging (7.0) - Microsoft.Extensions.DependencyInjection (>= 7.0) - Microsoft.Extensions.DependencyInjection.Abstractions (>= 7.0) - Microsoft.Extensions.Logging.Abstractions (>= 7.0) - Microsoft.Extensions.Options (>= 7.0) - Microsoft.Extensions.Logging.Abstractions (7.0) - Microsoft.Extensions.Options (7.0.1) - Microsoft.Extensions.DependencyInjection.Abstractions (>= 7.0) - Microsoft.Extensions.Primitives (>= 7.0) - Microsoft.Extensions.Primitives (7.0) - Mono.Cecil (0.11.4) + Microsoft.Extensions.DependencyInjection.Abstractions (8.0.1) + Microsoft.Extensions.FileProviders.Abstractions (8.0) + Microsoft.Extensions.Primitives (>= 8.0) + Microsoft.Extensions.FileProviders.Embedded (8.0.8) + Microsoft.Extensions.FileProviders.Abstractions (>= 8.0) + Microsoft.Extensions.Logging.Abstractions (8.0.1) + Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.1) + Microsoft.Extensions.Primitives (8.0) + Microsoft.IO.RecyclableMemoryStream (3.0.1) + Microsoft.Win32.Registry (5.0) + System.Security.AccessControl (>= 5.0) + System.Security.Principal.Windows (>= 5.0) + Mono.Cecil (0.11.5) Perfolizer (0.2.1) System.Memory (>= 4.5.3) - System.CodeDom (7.0) - System.Collections.Immutable (7.0) - System.IO.Pipelines (7.0) - System.Management (7.0) - System.CodeDom (>= 7.0) + System.CodeDom (8.0) + System.Collections.Immutable (8.0) + System.IO.Pipelines (8.0) + System.Management (8.0) + System.CodeDom (>= 8.0) System.Memory (4.5.5) - System.Reactive (5.0) - System.Reflection.Metadata (7.0) - System.Collections.Immutable (>= 7.0) + System.Reactive (6.0.1) + System.Reflection.Metadata (8.0) + System.Collections.Immutable (>= 8.0) + System.Reflection.TypeExtensions (4.7) System.Runtime.CompilerServices.Unsafe (6.0) - System.Text.Encoding.CodePages (7.0) - System.Threading.Channels (7.0) - Websocket.Client (4.6.1) - System.Reactive (>= 5.0) - System.Threading.Channels (>= 5.0) + System.Security.AccessControl (6.0.1) + System.Security.Principal.Windows (5.0) + System.Threading.Channels (8.0) + Websocket.Client (5.1.2) + Microsoft.Extensions.Logging.Abstractions (>= 8.0) + Microsoft.IO.RecyclableMemoryStream (>= 3.0) + System.Reactive (>= 6.0) + System.Threading.Channels (>= 8.0) GROUP Docs RESTRICTION: == net7.0 NUGET remote: https://api.nuget.org/v3/index.json - Argu (6.1.1) - FSharp.Core (>= 4.3.2) + Argu (6.2.4) + FSharp.Core (>= 6.0.7) System.Configuration.ConfigurationManager (>= 4.4) - FSharp.Core (7.0.200) + FSharp.Core (8.0.400) FsLibTool (0.1.1) FSharp.Core (>= 4.0.0.1) PPrint (>= 1.4.3) - Microsoft.Win32.SystemEvents (7.0) + Microsoft.Extensions.Configuration (8.0) + Microsoft.Extensions.Configuration.Abstractions (>= 8.0) + Microsoft.Extensions.Primitives (>= 8.0) + Microsoft.Extensions.Configuration.Abstractions (8.0) + Microsoft.Extensions.Primitives (>= 8.0) + Microsoft.Extensions.Configuration.Binder (8.0.2) + Microsoft.Extensions.Configuration.Abstractions (>= 8.0) + Microsoft.Extensions.DependencyInjection (8.0) + Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0) + Microsoft.Extensions.DependencyInjection.Abstractions (8.0.1) + Microsoft.Extensions.Logging (8.0) + Microsoft.Extensions.DependencyInjection (>= 8.0) + Microsoft.Extensions.Logging.Abstractions (>= 8.0) + Microsoft.Extensions.Options (>= 8.0) + Microsoft.Extensions.Logging.Abstractions (8.0.1) + Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.1) + Microsoft.Extensions.Logging.Configuration (8.0) + Microsoft.Extensions.Configuration (>= 8.0) + Microsoft.Extensions.Configuration.Abstractions (>= 8.0) + Microsoft.Extensions.Configuration.Binder (>= 8.0) + Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0) + Microsoft.Extensions.Logging (>= 8.0) + Microsoft.Extensions.Logging.Abstractions (>= 8.0) + Microsoft.Extensions.Options (>= 8.0) + Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0) + Microsoft.Extensions.Logging.Console (8.0) + Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0) + Microsoft.Extensions.Logging (>= 8.0) + Microsoft.Extensions.Logging.Abstractions (>= 8.0) + Microsoft.Extensions.Logging.Configuration (>= 8.0) + Microsoft.Extensions.Options (>= 8.0) + System.Text.Json (>= 8.0) + Microsoft.Extensions.Options (8.0.2) + Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0) + Microsoft.Extensions.Primitives (>= 8.0) + Microsoft.Extensions.Options.ConfigurationExtensions (8.0) + Microsoft.Extensions.Configuration.Abstractions (>= 8.0) + Microsoft.Extensions.Configuration.Binder (>= 8.0) + Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0) + Microsoft.Extensions.Options (>= 8.0) + Microsoft.Extensions.Primitives (>= 8.0) + Microsoft.Extensions.Primitives (8.0) PPrint (2.0) FSharp.Core (>= 3.1.2.5) - System.Configuration.ConfigurationManager (7.0) - System.Diagnostics.EventLog (>= 7.0) - System.Security.Cryptography.ProtectedData (>= 7.0) - System.Security.Permissions (>= 7.0) - System.Diagnostics.EventLog (7.0) - System.Drawing.Common (7.0) - Microsoft.Win32.SystemEvents (>= 7.0) - System.Security.Cryptography.ProtectedData (7.0.1) - System.Security.Permissions (7.0) - System.Windows.Extensions (>= 7.0) - System.Windows.Extensions (7.0) - System.Drawing.Common (>= 7.0) + System.Configuration.ConfigurationManager (8.0) + System.Diagnostics.EventLog (>= 8.0) + System.Security.Cryptography.ProtectedData (>= 8.0) + System.Diagnostics.EventLog (8.0) + System.Security.Cryptography.ProtectedData (8.0) + System.Text.Encodings.Web (8.0) + System.Text.Json (8.0.4) + System.Text.Encodings.Web (>= 8.0) GROUP Examples RESTRICTION: == net7.0 NUGET remote: https://api.nuget.org/v3/index.json - FSharp.Core (7.0.200) - Microsoft.NETCore.Platforms (7.0) + FSharp.Core (8.0.400) + Microsoft.Extensions.Configuration (8.0) + Microsoft.Extensions.Configuration.Abstractions (>= 8.0) + Microsoft.Extensions.Primitives (>= 8.0) + Microsoft.Extensions.Configuration.Abstractions (8.0) + Microsoft.Extensions.Primitives (>= 8.0) + Microsoft.Extensions.Configuration.Binder (8.0.2) + Microsoft.Extensions.Configuration.Abstractions (>= 8.0) + Microsoft.Extensions.DependencyInjection (8.0) + Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0) + Microsoft.Extensions.DependencyInjection.Abstractions (8.0.1) + Microsoft.Extensions.Logging (8.0) + Microsoft.Extensions.DependencyInjection (>= 8.0) + Microsoft.Extensions.Logging.Abstractions (>= 8.0) + Microsoft.Extensions.Options (>= 8.0) + Microsoft.Extensions.Logging.Abstractions (8.0.1) + Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0.1) + Microsoft.Extensions.Logging.Configuration (8.0) + Microsoft.Extensions.Configuration (>= 8.0) + Microsoft.Extensions.Configuration.Abstractions (>= 8.0) + Microsoft.Extensions.Configuration.Binder (>= 8.0) + Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0) + Microsoft.Extensions.Logging (>= 8.0) + Microsoft.Extensions.Logging.Abstractions (>= 8.0) + Microsoft.Extensions.Options (>= 8.0) + Microsoft.Extensions.Options.ConfigurationExtensions (>= 8.0) + Microsoft.Extensions.Logging.Console (8.0) + Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0) + Microsoft.Extensions.Logging (>= 8.0) + Microsoft.Extensions.Logging.Abstractions (>= 8.0) + Microsoft.Extensions.Logging.Configuration (>= 8.0) + Microsoft.Extensions.Options (>= 8.0) + System.Text.Json (>= 8.0) + Microsoft.Extensions.Options (8.0.2) + Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0) + Microsoft.Extensions.Primitives (>= 8.0) + Microsoft.Extensions.Options.ConfigurationExtensions (8.0) + Microsoft.Extensions.Configuration.Abstractions (>= 8.0) + Microsoft.Extensions.Configuration.Binder (>= 8.0) + Microsoft.Extensions.DependencyInjection.Abstractions (>= 8.0) + Microsoft.Extensions.Options (>= 8.0) + Microsoft.Extensions.Primitives (>= 8.0) + Microsoft.Extensions.Primitives (8.0) + Microsoft.NETCore.Platforms (7.0.4) Microsoft.NETCore.Targets (5.0) Microsoft.Win32.Registry (5.0) System.Security.AccessControl (>= 5.0) System.Security.Principal.Windows (>= 5.0) - Microsoft.Win32.SystemEvents (7.0) + Microsoft.Win32.SystemEvents (8.0) runtime.native.System (4.3.1) Microsoft.NETCore.Platforms (>= 1.1.1) Microsoft.NETCore.Targets (>= 1.1.3) - System.Diagnostics.EventLog (7.0) + System.Diagnostics.EventLog (8.0) System.Globalization (4.3) Microsoft.NETCore.Platforms (>= 1.1) Microsoft.NETCore.Targets (>= 1.1) @@ -181,14 +261,17 @@ NUGET System.Runtime (>= 4.3) System.Runtime.InteropServices (>= 4.3) System.Threading (>= 4.3) - System.Security.AccessControl (6.0) + System.Security.AccessControl (6.0.1) System.Security.Principal.Windows (5.0) - System.ServiceProcess.ServiceController (7.0) - System.Diagnostics.EventLog (>= 7.0) + System.ServiceProcess.ServiceController (8.0) + System.Diagnostics.EventLog (>= 8.0) System.Text.Encoding (4.3) Microsoft.NETCore.Platforms (>= 1.1) Microsoft.NETCore.Targets (>= 1.1) System.Runtime (>= 4.3) + System.Text.Encodings.Web (8.0) + System.Text.Json (8.0.4) + System.Text.Encodings.Web (>= 8.0) System.Threading (4.3) System.Runtime (>= 4.3) System.Threading.Tasks (>= 4.3) @@ -214,209 +297,202 @@ NUGET BlackFox.VsWhere (1.1) FSharp.Core (>= 4.2.3) Microsoft.Win32.Registry (>= 4.7) - Fake.Api.GitHub (6.0) - FSharp.Core (>= 6.0.3) - Octokit (>= 0.50) - Fake.BuildServer.TeamFoundation (6.0) - Fake.Core.Environment (>= 6.0) - Fake.Core.String (>= 6.0) - Fake.Core.Trace (>= 6.0) - Fake.Core.Vault (>= 6.0) - Fake.IO.FileSystem (>= 6.0) - FSharp.Core (>= 6.0.3) - Fake.Core.CommandLineParsing (6.0) + Fake.Api.GitHub (6.1.1) + FSharp.Core (>= 8.0.301) + Octokit (>= 13.0.1) + Fake.BuildServer.TeamFoundation (6.1.1) + Fake.Core.Environment (>= 6.1.1) + Fake.Core.String (>= 6.1.1) + Fake.Core.Trace (>= 6.1.1) + Fake.Core.Vault (>= 6.1.1) + Fake.IO.FileSystem (>= 6.1.1) + FSharp.Core (>= 8.0.301) + Fake.Core.CommandLineParsing (6.1.1) FParsec (>= 1.1.1) - FSharp.Core (>= 6.0.3) - Fake.Core.Context (6.0) - FSharp.Core (>= 6.0.3) - Fake.Core.Environment (6.0) - FSharp.Core (>= 6.0.3) - Fake.Core.FakeVar (6.0) - Fake.Core.Context (>= 6.0) - FSharp.Core (>= 6.0.3) - Fake.Core.Process (6.0) - Fake.Core.Environment (>= 6.0) - Fake.Core.FakeVar (>= 6.0) - Fake.Core.String (>= 6.0) - Fake.Core.Trace (>= 6.0) - Fake.IO.FileSystem (>= 6.0) - FSharp.Core (>= 6.0.3) - System.Collections.Immutable (>= 6.0) - Fake.Core.ReleaseNotes (6.0) - Fake.Core.SemVer (>= 6.0) - Fake.Core.String (>= 6.0) - FSharp.Core (>= 6.0.3) - Fake.Core.SemVer (6.0) - FSharp.Core (>= 6.0.3) - Fake.Core.String (6.0) - FSharp.Core (>= 6.0.3) - Fake.Core.Target (6.0) - Fake.Core.CommandLineParsing (>= 6.0) - Fake.Core.Context (>= 6.0) - Fake.Core.Environment (>= 6.0) - Fake.Core.FakeVar (>= 6.0) - Fake.Core.Process (>= 6.0) - Fake.Core.String (>= 6.0) - Fake.Core.Trace (>= 6.0) + FSharp.Core (>= 8.0.301) + Fake.Core.Context (6.1.1) + FSharp.Core (>= 8.0.301) + Fake.Core.Environment (6.1.1) + FSharp.Core (>= 8.0.301) + Fake.Core.FakeVar (6.1.1) + Fake.Core.Context (>= 6.1.1) + FSharp.Core (>= 8.0.301) + Fake.Core.Process (6.1.1) + Fake.Core.Environment (>= 6.1.1) + Fake.Core.FakeVar (>= 6.1.1) + Fake.Core.String (>= 6.1.1) + Fake.Core.Trace (>= 6.1.1) + Fake.IO.FileSystem (>= 6.1.1) + FSharp.Core (>= 8.0.301) + System.Collections.Immutable (>= 8.0) + Fake.Core.ReleaseNotes (6.1.1) + Fake.Core.SemVer (>= 6.1.1) + Fake.Core.String (>= 6.1.1) + FSharp.Core (>= 8.0.301) + Fake.Core.SemVer (6.1.1) + FSharp.Core (>= 8.0.301) + Fake.Core.String (6.1.1) + FSharp.Core (>= 8.0.301) + Fake.Core.Target (6.1.1) + Fake.Core.CommandLineParsing (>= 6.1.1) + Fake.Core.Context (>= 6.1.1) + Fake.Core.Environment (>= 6.1.1) + Fake.Core.FakeVar (>= 6.1.1) + Fake.Core.Process (>= 6.1.1) + Fake.Core.String (>= 6.1.1) + Fake.Core.Trace (>= 6.1.1) FSharp.Control.Reactive (>= 5.0.2) - FSharp.Core (>= 6.0.3) - Fake.Core.Tasks (6.0) - Fake.Core.Trace (>= 6.0) - FSharp.Core (>= 6.0.3) - Fake.Core.Trace (6.0) - Fake.Core.Environment (>= 6.0) - Fake.Core.FakeVar (>= 6.0) - FSharp.Core (>= 6.0.3) - Fake.Core.Vault (6.0) - FSharp.Core (>= 6.0.3) - Newtonsoft.Json (>= 13.0.1) - Fake.Core.Xml (6.0) - Fake.Core.String (>= 6.0) - FSharp.Core (>= 6.0.3) - Fake.DotNet.AssemblyInfoFile (6.0) - Fake.Core.Environment (>= 6.0) - Fake.Core.String (>= 6.0) - Fake.Core.Trace (>= 6.0) - Fake.IO.FileSystem (>= 6.0) - FSharp.Core (>= 6.0.3) - Fake.DotNet.Cli (6.0) - Fake.Core.Environment (>= 6.0) - Fake.Core.Process (>= 6.0) - Fake.Core.String (>= 6.0) - Fake.Core.Trace (>= 6.0) - Fake.DotNet.MSBuild (>= 6.0) - Fake.DotNet.NuGet (>= 6.0) - Fake.IO.FileSystem (>= 6.0) - FSharp.Core (>= 6.0.3) + FSharp.Core (>= 8.0.301) + Fake.Core.Tasks (6.1.1) + Fake.Core.Trace (>= 6.1.1) + FSharp.Core (>= 8.0.301) + Fake.Core.Trace (6.1.1) + Fake.Core.Environment (>= 6.1.1) + Fake.Core.FakeVar (>= 6.1.1) + FSharp.Core (>= 8.0.301) + Fake.Core.Vault (6.1.1) + FSharp.Core (>= 8.0.301) + Newtonsoft.Json (>= 13.0.3) + Fake.Core.Xml (6.1.1) + Fake.Core.String (>= 6.1.1) + FSharp.Core (>= 8.0.301) + Fake.DotNet.AssemblyInfoFile (6.1.1) + Fake.Core.Environment (>= 6.1.1) + Fake.Core.String (>= 6.1.1) + Fake.Core.Trace (>= 6.1.1) + Fake.IO.FileSystem (>= 6.1.1) + FSharp.Core (>= 8.0.301) + Fake.DotNet.Cli (6.1.1) + Fake.Core.Environment (>= 6.1.1) + Fake.Core.Process (>= 6.1.1) + Fake.Core.String (>= 6.1.1) + Fake.Core.Trace (>= 6.1.1) + Fake.DotNet.MSBuild (>= 6.1.1) + Fake.Dotnet.NuGet (>= 6.1.1) + Fake.IO.FileSystem (>= 6.1.1) + FSharp.Core (>= 8.0.301) Mono.Posix.NETStandard (>= 1.0) - Newtonsoft.Json (>= 13.0.1) - Fake.DotNet.MSBuild (6.0) + Newtonsoft.Json (>= 13.0.3) + Fake.DotNet.MSBuild (6.1.1) BlackFox.VsWhere (>= 1.1) - Fake.Core.Environment (>= 6.0) - Fake.Core.Process (>= 6.0) - Fake.Core.String (>= 6.0) - Fake.Core.Trace (>= 6.0) - Fake.IO.FileSystem (>= 6.0) - FSharp.Core (>= 6.0.3) - MSBuild.StructuredLogger (>= 2.1.545) - Fake.DotNet.NuGet (6.0) - Fake.Core.Environment (>= 6.0) - Fake.Core.Process (>= 6.0) - Fake.Core.SemVer (>= 6.0) - Fake.Core.String (>= 6.0) - Fake.Core.Tasks (>= 6.0) - Fake.Core.Trace (>= 6.0) - Fake.Core.Xml (>= 6.0) - Fake.IO.FileSystem (>= 6.0) - Fake.Net.Http (>= 6.0) - FSharp.Core (>= 6.0.3) - Newtonsoft.Json (>= 13.0.1) - NuGet.Protocol (>= 6.0) - Fake.DotNet.Paket (6.0) - Fake.Core.Process (>= 6.0) - Fake.Core.String (>= 6.0) - Fake.Core.Trace (>= 6.0) - Fake.DotNet.Cli (>= 6.0) - Fake.IO.FileSystem (>= 6.0) - FSharp.Core (>= 6.0.3) - Fake.DotNet.Testing.XUnit2 (6.0) - Fake.Core.Process (>= 6.0) - Fake.Core.String (>= 6.0) - Fake.Core.Trace (>= 6.0) - Fake.IO.FileSystem (>= 6.0) - Fake.Testing.Common (>= 6.0) - FSharp.Core (>= 6.0.3) - Fake.IO.FileSystem (6.0) - Fake.Core.String (>= 6.0) - Fake.Core.Trace (>= 6.0) - FSharp.Core (>= 6.0.3) - Fake.IO.Zip (6.0) - Fake.Core.String (>= 6.0) - Fake.IO.FileSystem (>= 6.0) - FSharp.Core (>= 6.0.3) - Fake.Net.Http (6.0) - Fake.Core.Trace (>= 6.0) - FSharp.Core (>= 6.0.3) - Fake.Testing.Common (6.0) - Fake.Core.Trace (>= 6.0) - FSharp.Core (>= 6.0.3) - Fake.Tools.Git (6.0) - Fake.Core.Environment (>= 6.0) - Fake.Core.Process (>= 6.0) - Fake.Core.SemVer (>= 6.0) - Fake.Core.String (>= 6.0) - Fake.Core.Trace (>= 6.0) - Fake.IO.FileSystem (>= 6.0) - FSharp.Core (>= 6.0.3) + Fake.Core.Environment (>= 6.1.1) + Fake.Core.Process (>= 6.1.1) + Fake.Core.String (>= 6.1.1) + Fake.Core.Trace (>= 6.1.1) + Fake.IO.FileSystem (>= 6.1.1) + FSharp.Core (>= 8.0.301) + MSBuild.StructuredLogger (>= 2.1.815) + Fake.Dotnet.NuGet (6.1.1) + Fake.Core.Environment (>= 6.1.1) + Fake.Core.Process (>= 6.1.1) + Fake.Core.SemVer (>= 6.1.1) + Fake.Core.String (>= 6.1.1) + Fake.Core.Tasks (>= 6.1.1) + Fake.Core.Trace (>= 6.1.1) + Fake.Core.Xml (>= 6.1.1) + Fake.IO.FileSystem (>= 6.1.1) + Fake.Net.Http (>= 6.1.1) + FSharp.Core (>= 8.0.301) + Newtonsoft.Json (>= 13.0.3) + NuGet.Protocol (>= 6.10.1) + Fake.DotNet.Paket (6.1.1) + Fake.Core.Process (>= 6.1.1) + Fake.Core.String (>= 6.1.1) + Fake.Core.Trace (>= 6.1.1) + Fake.DotNet.Cli (>= 6.1.1) + Fake.IO.FileSystem (>= 6.1.1) + FSharp.Core (>= 8.0.301) + Fake.DotNet.Testing.XUnit2 (6.1.1) + Fake.Core.Process (>= 6.1.1) + Fake.Core.String (>= 6.1.1) + Fake.Core.Trace (>= 6.1.1) + Fake.IO.FileSystem (>= 6.1.1) + Fake.Testing.Common (>= 6.1.1) + FSharp.Core (>= 8.0.301) + Fake.IO.FileSystem (6.1.1) + Fake.Core.String (>= 6.1.1) + Fake.Core.Trace (>= 6.1.1) + FSharp.Core (>= 8.0.301) + Fake.IO.Zip (6.1.1) + Fake.Core.String (>= 6.1.1) + Fake.IO.FileSystem (>= 6.1.1) + FSharp.Core (>= 8.0.301) + Fake.Net.Http (6.1.1) + Fake.Core.Trace (>= 6.1.1) + FSharp.Core (>= 8.0.301) + Fake.Testing.Common (6.1.1) + Fake.Core.Trace (>= 6.1.1) + FSharp.Core (>= 8.0.301) + Fake.Tools.Git (6.1.1) + Fake.Core.Environment (>= 6.1.1) + Fake.Core.Process (>= 6.1.1) + Fake.Core.SemVer (>= 6.1.1) + Fake.Core.String (>= 6.1.1) + Fake.Core.Trace (>= 6.1.1) + Fake.IO.FileSystem (>= 6.1.1) + FSharp.Core (>= 8.0.301) FParsec (1.1.1) FSharp.Core (>= 4.3.4) FSharp.Control.Reactive (5.0.5) FSharp.Core (>= 4.7.2) System.Reactive (>= 5.0 < 6.0) - FSharp.Core (7.0.200) - Microsoft.Build.Framework (17.5) + FSharp.Core (8.0.400) + Microsoft.Build.Framework (17.11.4) Microsoft.Win32.Registry (>= 5.0) - System.Security.Permissions (>= 6.0) - Microsoft.Build.Utilities.Core (17.5) - Microsoft.Build.Framework (>= 17.5) - Microsoft.NET.StringTools (>= 17.5) + System.Memory (>= 4.5.5) + System.Runtime.CompilerServices.Unsafe (>= 6.0) + System.Security.Principal.Windows (>= 5.0) + Microsoft.Build.Utilities.Core (17.11.4) + Microsoft.Build.Framework (>= 17.11.4) + Microsoft.NET.StringTools (>= 17.11.4) Microsoft.Win32.Registry (>= 5.0) - System.Collections.Immutable (>= 6.0) - System.Configuration.ConfigurationManager (>= 6.0) - System.Security.Permissions (>= 6.0) - System.Text.Encoding.CodePages (>= 6.0) - Microsoft.NET.StringTools (17.5) + System.Collections.Immutable (>= 8.0) + System.Configuration.ConfigurationManager (>= 8.0) + System.Memory (>= 4.5.5) + System.Runtime.CompilerServices.Unsafe (>= 6.0) + System.Security.Principal.Windows (>= 5.0) + System.Text.Encoding.CodePages (>= 7.0) + Microsoft.NET.StringTools (17.11.4) System.Memory (>= 4.5.5) System.Runtime.CompilerServices.Unsafe (>= 6.0) Microsoft.Win32.Registry (5.0) System.Security.AccessControl (>= 5.0) System.Security.Principal.Windows (>= 5.0) - Microsoft.Win32.SystemEvents (7.0) Mono.Posix.NETStandard (1.0) - MSBuild.StructuredLogger (2.1.787) - Microsoft.Build.Framework (>= 16.10) - Microsoft.Build.Utilities.Core (>= 16.10) - Newtonsoft.Json (13.0.2) - NuGet.Common (6.5) - NuGet.Frameworks (>= 6.5) - NuGet.Configuration (6.5) - NuGet.Common (>= 6.5) + MSBuild.StructuredLogger (2.2.337) + Microsoft.Build.Framework (>= 17.5) + Microsoft.Build.Utilities.Core (>= 17.5) + Newtonsoft.Json (13.0.3) + NuGet.Common (6.11) + NuGet.Frameworks (>= 6.11) + NuGet.Configuration (6.11) + NuGet.Common (>= 6.11) System.Security.Cryptography.ProtectedData (>= 4.4) - NuGet.Frameworks (6.5) - NuGet.Packaging (6.5) - Newtonsoft.Json (>= 13.0.1) - NuGet.Configuration (>= 6.5) - NuGet.Versioning (>= 6.5) - System.Security.Cryptography.Cng (>= 5.0) - System.Security.Cryptography.Pkcs (>= 5.0) - NuGet.Protocol (6.5) - NuGet.Packaging (>= 6.5) - NuGet.Versioning (6.5) - Octokit (5.0) - System.Collections.Immutable (7.0) + NuGet.Frameworks (6.11) + NuGet.Packaging (6.11) + Newtonsoft.Json (>= 13.0.3) + NuGet.Configuration (>= 6.11) + NuGet.Versioning (>= 6.11) + System.Security.Cryptography.Pkcs (>= 6.0.4) + NuGet.Protocol (6.11) + NuGet.Packaging (>= 6.11) + NuGet.Versioning (6.11) + Octokit (13.0.1) + System.Collections.Immutable (8.0) System.Runtime.CompilerServices.Unsafe (>= 6.0) - System.Configuration.ConfigurationManager (7.0) - System.Security.Cryptography.ProtectedData (>= 7.0) - System.Security.Permissions (>= 7.0) - System.Drawing.Common (7.0) - Microsoft.Win32.SystemEvents (>= 7.0) - System.Formats.Asn1 (7.0) + System.Configuration.ConfigurationManager (8.0) + System.Security.Cryptography.ProtectedData (>= 8.0) + System.Formats.Asn1 (8.0.1) System.Memory (4.5.5) System.Reactive (5.0) System.Runtime.CompilerServices.Unsafe (6.0) - System.Security.AccessControl (6.0) - System.Security.Cryptography.Cng (5.0) - System.Formats.Asn1 (>= 5.0) - System.Security.Cryptography.Pkcs (7.0.1) - System.Formats.Asn1 (>= 7.0) - System.Security.Cryptography.ProtectedData (7.0.1) - System.Security.Permissions (7.0) - System.Windows.Extensions (>= 7.0) + System.Security.AccessControl (6.0.1) + System.Security.Cryptography.Pkcs (8.0) + System.Formats.Asn1 (>= 8.0) + System.Security.Cryptography.ProtectedData (8.0) System.Security.Principal.Windows (5.0) - System.Text.Encoding.CodePages (7.0) + System.Text.Encoding.CodePages (8.0) System.Runtime.CompilerServices.Unsafe (>= 6.0) - System.Windows.Extensions (7.0) - System.Drawing.Common (>= 7.0) GROUP SourceLink STORAGE: NONE diff --git a/src/Suave.IO/Program.fs b/src/Suave.IO/Program.fs index 0b6b540f..c8da1134 100644 --- a/src/Suave.IO/Program.fs +++ b/src/Suave.IO/Program.fs @@ -1,14 +1,22 @@ -module Suave.IO.Program +module Suave.IO.Program open Argu open Suave open Suave.ServerErrors -open Suave.Logging -open Suave.Logging.Message +open Suave.Operators + open System.IO -open System.Net +open Microsoft.Extensions.Logging +open Suave.Filters + +let factory = LoggerFactory.Create(fun builder -> builder.AddConsole() |> ignore) +let logger = factory.CreateLogger("Suave.IO") -let logger = Log.create "Suave.IO" +let log format : WebPart = + fun ctx -> async{ + logger.LogInformation(format ctx); + return Some ctx + } type Arguments = | [] Binding of string * int @@ -26,7 +34,7 @@ let addExnLogging (fwp: 'a -> WebPart) = try return! fwp input ctx with e -> - do logger.fatal (eventX "Unhandled {exception}" >> setField "exception" e) + do logger.LogCritical ("Unhandled exception", e) return! INTERNAL_ERROR "Unhandled internal exception" ctx } @@ -35,7 +43,7 @@ let app: WebPart = Files.browseHome Files.browseFileHome "index.html" request (fun r -> INTERNAL_ERROR (sprintf "No file found at path %s" r.url.AbsolutePath)) - ] + ] >=> log logFormat; [] let main argv = diff --git a/src/Suave.IO/paket.references b/src/Suave.IO/paket.references index d1fc3721..c8497965 100644 --- a/src/Suave.IO/paket.references +++ b/src/Suave.IO/paket.references @@ -1,3 +1,5 @@ group Docs FSharp.Core Argu +Microsoft.Extensions.Logging +Microsoft.Extensions.Logging.Console diff --git a/src/Suave.Tests/Auth.fs b/src/Suave.Tests/Auth.fs index 8adfbb4f..8dca6d85 100644 --- a/src/Suave.Tests/Auth.fs +++ b/src/Suave.Tests/Auth.fs @@ -1,4 +1,4 @@ -module Suave.Tests.Auth +module Suave.Tests.Auth open System @@ -6,8 +6,6 @@ open System.Net open System.Net.Http open Expecto open Suave -open Suave.Logging -open Suave.Logging.Message open Suave.Cookie open Suave.State.CookieStateStore open Suave.Operators @@ -38,29 +36,12 @@ let reqResp fResult (ctx : SuaveTestCtx) = - let event message = - eventX message >> setSingleName "Suave.Tests" - - let logger = - ctx.suaveConfig.logger - - logger.debug ( - event "{method} {resource}" - >> setFieldValue "method" methd - >> setFieldValue "resource" resource) - let defaultTimeout = TimeSpan.FromSeconds 5. use handler = createHandler DecompressionMethods.None cookies use client = createClient handler use request = createRequest methd resource "" None (endpointUri ctx.suaveConfig) |> fRequest - for h in request.Headers do - logger.debug (event "{headerName}: {headerValue}" - >> setFieldValue "headerName" h.Key - >> setFieldValue "headerValue" (String.Join(", ", h.Value))) - - // use -> let!!! let result = request |> send client defaultTimeout ctx fResult result @@ -95,7 +76,7 @@ let sessionState f = [] let authTests cfg = - let runWithConfig = runWith { cfg with logger = Targets.create Warn [||] } + let runWithConfig = runWith cfg //{ cfg with logger = Targets.create Warn [||] } testList "auth tests" [ testCase "baseline, no auth cookie" <| fun _ -> let ctx = runWithConfig (OK "ACK") diff --git a/src/Suave.Tests/CORS.fs b/src/Suave.Tests/CORS.fs index 165fa1db..f2b6dc96 100644 --- a/src/Suave.Tests/CORS.fs +++ b/src/Suave.Tests/CORS.fs @@ -1,17 +1,12 @@ -module Suave.Tests.CORS +module Suave.Tests.CORS -open System open System.Net open System.Net.Http open Expecto -open Suave.Tests.TestUtilities open Suave open Suave.Successful open Suave.Operators open Suave.Filters -open Suave.Logging -open Suave.Cookie -open Suave.State.CookieStateStore open Suave.CORS open Suave.Testing @@ -39,7 +34,7 @@ let corsWithDefaultConfig = cors defaultCORSConfig [] let tests cfg = - let runWithConfig = runWith { cfg with logger = Targets.create LogLevel.Warn [| "Suave"; "Tests"; "CORS" |] } + let runWithConfig = runWith cfg //{ cfg with logger = Targets.create LogLevel.Warn [| "Suave"; "Tests"; "CORS" |] } let origin = "http://someorigin.com" diff --git a/src/Suave.Tests/Cookie.fs b/src/Suave.Tests/Cookie.fs index d0438821..0e0cfc5c 100644 --- a/src/Suave.Tests/Cookie.fs +++ b/src/Suave.Tests/Cookie.fs @@ -1,14 +1,10 @@ -module Suave.Tests.Cookie +module Suave.Tests.Cookie open Suave open Suave.Cookie -open Suave.Logging -open Suave.Testing open Expecto -open FsCheck - open Tests.TestUtilities [] @@ -69,11 +65,6 @@ let parseResultCookie (_:SuaveConfig) = let parsed = Cookie.parseResultCookie (HttpCookie.toHeader cookie) Expect.equal parsed cookie "eq" -// FsCheck character gen from RFC slightly painful; let's do that when merging Freya -// testPropertyWithConfig fscheck_config "anything generated" <| fun (cookie : HttpCookie) -> -// let parsed = Cookie.parse_cookie (HttpCookie.to_header cookie) -// Expect.equal parsed cookie "eq" - testCase "set cookie (same name) twice keeps last" <| fun _ -> let force = Async.RunSynchronously >> Option.get let c1 = HttpCookie.createKV "a" "aa" @@ -105,50 +96,4 @@ let parseRequestCookies (_ : SuaveConfig) = Expect.equal result [] "cookies should be ignored" ] -[] -let setCookie (_ : SuaveConfig) = - testList "set cookie" [ - testCase "set cookie - no warning when < 4k" <| fun _ -> - let log = InspectableLog() - let cookie = - { name = "test cookie" - value = String.replicate 4095 "x" - expires = None - path = Some "/" - domain = None - secure = true - httpOnly = false - sameSite = None } - let ctx = Cookie.setCookie cookie { HttpContext.empty with runtime = { HttpRuntime.empty with logger = log }} - Expect.isTrue (List.isEmpty log.logs) "Should be no logs generated" - testCase "set cookie - no warning when = 4k" <| fun _ -> - let log = InspectableLog() - let cookie = - { name = "test cookie" - value = String.replicate 4096 "x" - expires = None - path = Some "/" - domain = None - secure = true - httpOnly = false - sameSite = None } - let ctx = Cookie.setCookie cookie { HttpContext.empty with runtime = { HttpRuntime.empty with logger = log }} - Expect.isTrue (List.isEmpty log.logs) "Should be no logs generated" - testCase "set cookie - warning when > 4k" <| fun _ -> - let log = InspectableLog() - let cookie = - { name = "test cookie" - value = String.replicate 4097 "x" - expires = None - path = Some "/" - domain = None - secure = true - httpOnly = false - sameSite = None } - let ctx = - let input = { HttpContext.empty with runtime = { HttpRuntime.empty with logger = log }} - Cookie.setCookie cookie input |> Async.RunSynchronously - Expect.equal (List.length log.logs) 1 "Should be 1 log generated" - Expect.equal (List.head log.logs).level LogLevel.Warn "should be a warning" - ] diff --git a/src/Suave.Tests/ExpectoExtensions.fs b/src/Suave.Tests/ExpectoExtensions.fs index ffe1146b..f2a525cc 100644 --- a/src/Suave.Tests/ExpectoExtensions.fs +++ b/src/Suave.Tests/ExpectoExtensions.fs @@ -1,10 +1,9 @@ -module Suave.Tests.ExpectoExtensions +module Suave.Tests.ExpectoExtensions open Expecto -open Expecto.Tests +open Expecto.Logging open Expecto.Impl open System -open System.Linq open System.Reflection type MemberInfo with @@ -68,5 +67,5 @@ let defaultMainThisAssemblyWithParam param args = | None -> failwith "Found no tests." match ExpectoConfig.fillFromArgs defaultConfig args with - | ArgsRun cfg -> runTests cfg tests + | ArgsRun cfg -> runTestsWithCLIArgs [ Verbosity Verbose; Sequenced ] [| |] tests | _ -> 1 \ No newline at end of file diff --git a/src/Suave.Tests/Program.fs b/src/Suave.Tests/Program.fs index 5097c602..a3a5cd1b 100644 --- a/src/Suave.Tests/Program.fs +++ b/src/Suave.Tests/Program.fs @@ -2,8 +2,6 @@ module Suave.Tests.Program open System open Suave -open Suave.Web -open Suave.Logging open ExpectoExtensions [] @@ -19,14 +17,6 @@ let main args = let testConfig = { defaultConfig with bindings = [ HttpBinding.createSimple HTTP "127.0.0.1" 9001 ] - logger = Targets.create Warn [| "Suave"; "Tests" |] } - - let mutable firstRun = 0 - let runDefaultEngine() = - Console.WriteLine "Running tests with default TCP engine." - firstRun <- defaultMainThisAssemblyWithParam testConfig args - Console.WriteLine "Done." - - runDefaultEngine() - firstRun + } + defaultMainThisAssemblyWithParam testConfig args diff --git a/src/Suave.Tests/Suave.Tests.fsproj b/src/Suave.Tests/Suave.Tests.fsproj index aed24f34..6c5d3107 100644 --- a/src/Suave.Tests/Suave.Tests.fsproj +++ b/src/Suave.Tests/Suave.Tests.fsproj @@ -41,9 +41,7 @@ - - diff --git a/src/Suave.Tests/TestUtilities.fs b/src/Suave.Tests/TestUtilities.fs index dc514707..d2c26076 100644 --- a/src/Suave.Tests/TestUtilities.fs +++ b/src/Suave.Tests/TestUtilities.fs @@ -1,4 +1,4 @@ -module Suave.Tests.TestUtilities +module Suave.Tests.TestUtilities #nowarn "25" @@ -164,7 +164,7 @@ module Expect = module Assert = let Equal(msg, exp, act) = Expect.equal act exp msg - + (* type LogMethod = | Factory of (LogLevel -> Message) | Plain of Message @@ -190,3 +190,4 @@ type InspectableLog() = member x.logWithAck level msgFactory : Async = x.logs <- { level = level; value = Factory msgFactory } :: x.logs async.Return () + *) \ No newline at end of file diff --git a/src/Suave.Tests/Testing.fs b/src/Suave.Tests/Testing.fs index eef72a1e..565e9ecf 100644 --- a/src/Suave.Tests/Testing.fs +++ b/src/Suave.Tests/Testing.fs @@ -31,8 +31,8 @@ open System.Net.Http open System.Net.Http.Headers open Expecto open Suave -open Suave.Logging -open Suave.Logging.Message +//open Suave.Logging +//open Suave.Logging.Message open Suave.Http [] @@ -105,7 +105,7 @@ let runWithFactory factory config webParts : SuaveTestCtx = let config2 = { config with cancellationToken = cts.Token; bufferSize = 128; maxOps = 10 } - let listening, (server) = factory { config with cancellationToken = cts .Token; logger = Targets.create Warn [||] } webParts + let listening, (server) = factory { config with cancellationToken = cts .Token(*; logger = Targets.create Warn [||]*) } webParts listening |> Async.RunSynchronously |> ignore // wait for the server to start listening { cts = cts @@ -143,10 +143,10 @@ let createClient handler = /// Send the request with the client - returning the result of the request let send (client : HttpClient) (timeout : TimeSpan) (ctx : SuaveTestCtx) (request : HttpRequestMessage) = - ctx.suaveConfig.logger.verbose ( - eventX "Send" - >> setFieldValue "method" request.Method.Method - >> setFieldValue "uri" request.RequestUri) + //ctx.suaveConfig.logger.verbose ( + // eventX "Send" + //>> setFieldValue "method" request.Method.Method + // >> setFieldValue "uri" request.RequestUri) let send = client.SendAsync(request, HttpCompletionOption.ResponseContentRead, ctx.cts.Token) diff --git a/src/Suave.Tests/Web.fs b/src/Suave.Tests/Web.fs index 78c7aa05..6e34065c 100644 --- a/src/Suave.Tests/Web.fs +++ b/src/Suave.Tests/Web.fs @@ -6,13 +6,8 @@ open Suave open Suave.Operators open Suave.Logging open Suave.Testing -open Suave.Sockets.Control -open Suave.Sockets.AsyncSocket open System open System.Net -open System.Net.Http -open System.Threading -open Suave.Http let private (=>) a b = a, b @@ -42,12 +37,11 @@ let parsing_tests (_: SuaveConfig) = Expect.equal (TraceHeader.parseTraceHeaders headers).traceId expected.traceId "should parse trace id" Expect.equal (TraceHeader.parseTraceHeaders headers).reqParentId expected.reqParentId "should parse span id to parent span id" ] -open Suave open Suave.Sockets [] let transferEncodingChunkedTests (cfg : SuaveConfig) = - let writeChunks (conn:Connection) = socket { + let writeChunks (conn:Connection) = task { do! conn.writeChunk "h"B do! conn.writeChunk "e"B diff --git a/src/Suave/Authentication.fs b/src/Suave/Authentication.fs index a2500d91..9e925b68 100644 --- a/src/Suave/Authentication.fs +++ b/src/Suave/Authentication.fs @@ -1,11 +1,8 @@ module Suave.Authentication -open System open System.Text open Suave.RequestErrors open Suave.Utils -open Suave.Logging -open Suave.Logging.Message open Suave.Cookie open Suave.State.CookieStateStore open Suave.Operators @@ -89,9 +86,6 @@ let authenticate relativeExpiry secure : WebPart = context (fun ctx -> - ctx.runtime.logger.debug ( - eventX "Authenticating" - >> setSingleName "Suave.Auth.authenticate") let state = { serverKey = ctx.runtime.serverKey diff --git a/src/Suave/Combinators.fs b/src/Suave/Combinators.fs index 89ac149a..d002a792 100644 --- a/src/Suave/Combinators.fs +++ b/src/Suave/Combinators.fs @@ -2,6 +2,7 @@ namespace Suave open Suave.Operators open Suave.Sockets +open System.Threading.Tasks module Response = @@ -128,7 +129,6 @@ module Intermediate = // 2xx module Successful = open System.Text - open Suave.Utils open Response let ok bytes : WebPart = @@ -288,7 +288,6 @@ module ServerErrors = module Filters = open Suave.Utils.AsyncExtensions - open Suave.Logging open System open System.Text open System.Text.RegularExpressions @@ -380,33 +379,6 @@ module Filters = ] "{clientIp} {processId} {userName} [{utcNow:dd/MMM/yyyy:hh:mm:ss %K}] \"{requestMethod} {requestUrlPath} {httpVersion}\" {httpStatusCode} {responseContentLength}", fieldList |> Map - let logWithLevel (level : LogLevel) (logger : Logger) (messageFun : HttpContext -> string) (ctx : HttpContext) = - async{ - logger.log level (fun _ -> - { value = Event (messageFun ctx) - level = level - name = [| "Suave"; "Http"; "requests" |] - fields = Map.empty - timestamp = Suave.Logging.Global.timestamp() }) - return Some ctx } - - let logWithLevelStructured (level : LogLevel) (logger : Logger) (messageFun : HttpContext -> (string * Map)) (ctx : HttpContext) = - async{ - logger.log level (fun _ -> - let template, fields = messageFun ctx - { value = Event template - level = level - name = [| "Suave"; "Http"; "requests" |] - fields = fields - timestamp = Suave.Logging.Global.timestamp() }) - return Some ctx } - - let logStructured (logger : Logger) (messageFun : HttpContext -> (string * Map)) = - logWithLevelStructured LogLevel.Info logger messageFun - - let log (logger : Logger) (messageFun : HttpContext -> string) = - logWithLevel LogLevel.Info logger messageFun - open Suave.Sscanf let pathScan (pf : PrintfFormat<_,_,_,_,'t>) (h : 't -> WebPart) : WebPart = @@ -463,8 +435,6 @@ module ServeResource = open Redirection open RequestErrors open Suave.Utils - open Suave.Logging - open Suave.Logging.Message // If a response includes both an Expires header and a max-age directive, // the max-age directive overrides the Expires header, even if the Expires header is more restrictive @@ -472,10 +442,6 @@ module ServeResource = let resource key exists getLast getExtension (send : string -> bool -> WebPart) ctx = - let log = - event Verbose - >> setSingleName "Suave.Http.ServeResource.resource" - >> ctx.runtime.logger.logSimple let sendIt name compression = setHeader "Last-Modified" ((getLast key : DateTimeOffset).ToString("R")) @@ -500,11 +466,8 @@ module ServeResource = | Choice2Of2 _ -> sendIt value.name value.compression ctx | None -> - let ext = getExtension key - log ("failed to find matching mime for ext '" + ext + "'") fail else - log ("failed to find resource by key '" + key + "'") fail module ContentRange = @@ -538,45 +501,40 @@ module Files = open System.IO open System.Text - open Suave.Utils - open Suave.Logging - open Suave.Logging.Message - open Suave.Sockets.Control - - open Response - open Writers open Successful - open Redirection open ServeResource open ContentRange let sendFile fileName (compression : bool) (ctx : HttpContext) = let writeFile file = let fs, start, total, status = getFileStream ctx file - fun (conn:Connection, _) -> socket { + fun (conn:Connection, _) -> task { let getLm = fun path -> FileInfo(path).LastWriteTime - let! (encoding,fs) = Compression.transformStream file fs getLm compression ctx.runtime.compressionFolder ctx - let finish = start + fs.Length - 1L - try + match! Compression.transformStream file fs getLm compression ctx.runtime.compressionFolder ctx with + | Ok(encoding,fs) -> + let finish = start + fs.Length - 1L try - match encoding with - | Some n -> - do! conn.asyncWriteLn ("Content-Range: bytes " + start.ToString() + "-" + finish.ToString() + "/*") - do! conn.asyncWriteLn (String.Concat [| "Content-Encoding: "; n.ToString() |]) - do! conn.asyncWriteLn ("Content-Length: " + (fs : Stream).Length.ToString() + "\r\n") - do! conn.flush () - if ctx.request.``method`` <> HttpMethod.HEAD && fs.Length > 0L then - do! transferStream conn fs - | None -> - do! conn.asyncWriteLn ("Content-Range: bytes " + start.ToString() + "-" + finish.ToString() + "/" + total.ToString()) - do! conn.asyncWriteLn ("Content-Length: " + (fs : Stream).Length.ToString() + "\r\n") - do! conn.flush() - if ctx.request.``method`` <> HttpMethod.HEAD && fs.Length > 0L then - do! transferStream conn fs - with ex -> - raise ex - finally - fs.Dispose() + try + match encoding with + | Some n -> + do! conn.asyncWriteLn ("Content-Range: bytes " + start.ToString() + "-" + finish.ToString() + "/*") + do! conn.asyncWriteLn (String.Concat [| "Content-Encoding: "; n.ToString() |]) + do! conn.asyncWriteLn ("Content-Length: " + (fs : Stream).Length.ToString() + "\r\n") + do! conn.flush () + if ctx.request.``method`` <> HttpMethod.HEAD && fs.Length > 0L then + do! transferStream conn fs + | None -> + do! conn.asyncWriteLn ("Content-Range: bytes " + start.ToString() + "-" + finish.ToString() + "/" + total.ToString()) + do! conn.asyncWriteLn ("Content-Length: " + (fs : Stream).Length.ToString() + "\r\n") + do! conn.flush() + if ctx.request.``method`` <> HttpMethod.HEAD && fs.Length > 0L then + do! transferStream conn fs + with ex -> + raise ex + finally + fs.Dispose() + | Result.Error error -> + return failwith "error" }, status let task, status = writeFile fileName { ctx with @@ -614,13 +572,7 @@ module Files = browseFile q.homeDirectory fileName h let browse rootPath : WebPart = - warbler (fun ctx -> - ctx.runtime.logger.verbose ( - eventX "Files.browser trying {localFileUrl} at {rootPath}" - >> setFieldValue "localFileUrl" ctx.request.url.AbsolutePath - >> setFieldValue "rootPath" rootPath - >> setSingleName "Suave.Http.Files.browse") - file (resolvePath rootPath ctx.request.path)) + warbler (fun ctx -> file (resolvePath rootPath ctx.request.path)) let browseHome : WebPart = warbler (fun ctx -> browse ctx.runtime.homeDirectory) @@ -661,11 +613,8 @@ module Embedded = open System.Xml open System.Xml.Linq - open Suave.Utils open Suave.Utils.Option.Operators - open Suave.Sockets.Control - open Response open ServeResource let defaultSourceAssembly = @@ -687,26 +636,29 @@ module Embedded = resourceName (compression : bool) (ctx : HttpContext) = - let writeResource name (conn:Connection, _) = socket { + let writeResource name (conn:Connection, _) = task { let fs = source.GetManifestResourceStream(name) let getLm = fun _ -> lastModified source - let! encoding,fs = Compression.transformStream name fs getLm compression ctx.runtime.compressionFolder ctx - match encoding with - | Some n -> - do! conn.asyncWriteLn (String.Concat [| "Content-Encoding: "; n.ToString() |]) - do! conn.asyncWriteLn ("Content-Length: " + (fs: Stream).Length.ToString() + "\r\n") - do! conn.flush() - if ctx.request.``method`` <> HttpMethod.HEAD && fs.Length > 0L then - do! transferStream conn fs - fs.Dispose() - return () - | None -> - do! conn.asyncWriteLn ("Content-Length: " + (fs: Stream).Length.ToString() + "\r\n") - do! conn.flush () - if ctx.request.``method`` <> HttpMethod.HEAD && fs.Length > 0L then - do! transferStream conn fs - fs.Dispose() - return () + match! Compression.transformStream name fs getLm compression ctx.runtime.compressionFolder ctx with + | Ok (encoding,fs) -> + match encoding with + | Some n -> + do! conn.asyncWriteLn (String.Concat [| "Content-Encoding: "; n.ToString() |]) + do! conn.asyncWriteLn ("Content-Length: " + (fs: Stream).Length.ToString() + "\r\n") + do! conn.flush() + if ctx.request.``method`` <> HttpMethod.HEAD && fs.Length > 0L then + do! transferStream conn fs + fs.Dispose() + return () + | None -> + do! conn.asyncWriteLn ("Content-Length: " + (fs: Stream).Length.ToString() + "\r\n") + do! conn.flush () + if ctx.request.``method`` <> HttpMethod.HEAD && fs.Length > 0L then + do! transferStream conn fs + fs.Dispose() + return () + | Result.Error error -> + return failwith "error" } { ctx with response = @@ -832,7 +784,7 @@ module EventSource = let (<<.) (out : Connection) (data : string) = out.asyncWriteBytes (Encoding.UTF8.GetBytes data) - let dispatch (out : Connection) : SocketOp = + let dispatch (out : Connection) : ValueTask = send out ES_EOL_S let comment (out : Connection) (cmt : string) = @@ -866,7 +818,7 @@ module EventSource = { id = id; data = data; ``type`` = Some typ } let send (out : Connection) (msg : Message) = - socket { + task { do! msg.id |> esId out match msg.``type`` with | Some x -> do! x |> eventType out @@ -876,7 +828,7 @@ module EventSource = } let private handShakeAux f (out : Connection, _) = - socket { + task { do! out.asyncWriteLn "" // newline after headers do! out.flush() // must flush lines buffer before using asyncWriteBytes @@ -886,7 +838,7 @@ module EventSource = return! f out } - let handShake (fCont: Connection -> SocketOp) (ctx : HttpContext) = + let handShake (fCont: Connection -> Task) (ctx : HttpContext) = { ctx with response = { ctx.response with @@ -908,8 +860,8 @@ module TransferEncoding = open Suave open Suave.Sockets.Control - let chunked (asyncWriteChunks: Connection -> SocketOp) (ctx : HttpContext) = - let task (conn:Connection,_) = socket { + let chunked (asyncWriteChunks: Connection -> System.Threading.Tasks.Task) (ctx : HttpContext) = + let task (conn:Connection,_) = task { do! conn.asyncWriteLn "" do! asyncWriteChunks conn } diff --git a/src/Suave/Combinators.fsi b/src/Suave/Combinators.fsi index fc538972..080e808c 100644 --- a/src/Suave/Combinators.fsi +++ b/src/Suave/Combinators.fsi @@ -1,5 +1,6 @@ namespace Suave +open System.Threading.Tasks open Suave.Sockets /// @@ -1153,12 +1154,12 @@ module Filters = /// message formatter that can inspect the context and produce a message to /// send to the logger, along with the structured fields as a name*obj map. /// - val logWithLevelStructured : level:LogLevel -> logger:Logger -> messageFun:(HttpContext -> string * Map) -> WebPart + //val logWithLevelStructured : level:LogLevel -> logger:Logger -> messageFun:(HttpContext -> string * Map) -> WebPart /// /// The function log is equivalent to `logWithLevel LogLevel.Debug`. /// - val logStructured : logger:Logger -> messageFun:(HttpContext -> string * Map) -> WebPart + //val logStructured : logger:Logger -> messageFun:(HttpContext -> string * Map) -> WebPart /// /// The default log format for . NCSA Common log format @@ -1182,12 +1183,12 @@ module Filters = /// message formatter that can inspect the context and produce a message to /// send to the logger. /// - val logWithLevel : level:LogLevel -> logger:Logger -> messageFun:(HttpContext -> string) -> WebPart + //val logWithLevel : level:LogLevel -> logger:Logger -> messageFun:(HttpContext -> string) -> WebPart /// /// The function log is equivalent to `logWithLevel LogLevel.Debug`. /// - val log : logger:Logger -> messageFun:(HttpContext -> string) -> WebPart + //val log : logger:Logger -> messageFun:(HttpContext -> string) -> WebPart /// /// Strongly typed route matching! Matching the uri can be used with the 'parsers' @@ -1603,36 +1604,36 @@ module EventSource = /// the proper framing is used. However, if you have a desire to write raw /// data, this function overrides the Socket.async_write function so that /// you will be writing UTF8 data only, as per the specification. - val asyncWrite : out:Connection -> data:string -> SocketOp + val asyncWrite : out:Connection -> data:string -> Task /// Same as `async_write`; convenience function. - val (<<.) : out:Connection -> data:string -> SocketOp + val (<<.) : out:Connection -> data:string -> Task /// "If the line is empty (a blank line) - dispatch the event." /// Dispatches the event properly to the browser. - val dispatch : out:Connection -> SocketOp + val dispatch : out:Connection -> ValueTask /// "If the line starts with a U+003A COLON character (:) - Ignore the line." /// Writes a comment to the stream - val comment : out:Connection -> cmt:string -> SocketOp + val comment : out:Connection -> cmt:string -> Task /// "If the field name is 'event' - Set the event type buffer to field value." /// Writes the event type to the stream - val eventType : out:Connection -> eventType:string -> SocketOp + val eventType : out:Connection -> eventType:string -> Task /// "If the field name is 'data' - /// Append the field value to the data buffer, then append a single /// U+000A LINE FEED (LF) character to the data buffer." /// Write a piece of data as part of the event - val data : out:Connection -> data:string -> SocketOp + val data : out:Connection -> data:string -> Task /// "If the field name is 'id' - Set the last event ID buffer to the field value." /// Sets the last event id in the stream. - val esId : out:Connection -> lastEventId:string -> SocketOp + val esId : out:Connection -> lastEventId:string -> Task /// Sets the option for the EventSource instance, of how long to wait in ms /// until a new connection is spawned as a retry. - val retry : out:Connection -> retry:uint32 -> SocketOp + val retry : out:Connection -> retry:uint32 -> Task /// A container data type for the output events type Message = @@ -1647,14 +1648,14 @@ module EventSource = static member createType : id:string -> data:string -> typ:string -> Message /// send a message containing data to the output stream - val send : out:Connection -> msg:Message -> SocketOp + val send : out:Connection -> msg:Message -> Task /// This function composes the passed function f with the hand-shake required /// to start a new event-stream protocol session with the browser. - val handShake : fCont:(Connection -> SocketOp) -> WebPart + val handShake : fCont:(Connection -> Task) -> WebPart module TransferEncoding = - val chunked: (Connection -> SocketOp) -> WebPart + val chunked: (Connection -> Task) -> WebPart module Control = diff --git a/src/Suave/Compression.fs b/src/Suave/Compression.fs index aba29768..753c803b 100644 --- a/src/Suave/Compression.fs +++ b/src/Suave/Compression.fs @@ -4,8 +4,6 @@ module Compression = open Suave.Utils open Suave.Sockets - open Suave.Sockets.Control - open System open System.IO @@ -57,8 +55,8 @@ module Compression = | _ -> None) | _ -> None - let transform (content : byte []) (ctx : HttpContext) : SocketOp = - socket { + let transform (content : byte []) (ctx : HttpContext) : Threading.Tasks.Task = + task { if content.Length > MIN_BYTES_TO_COMPRESS && content.Length < MAX_BYTES_TO_COMPRESS then let request = ctx.request let enconding = getEncoder request diff --git a/src/Suave/ConnectionFacade.fs b/src/Suave/ConnectionFacade.fs index 3e378b73..4abea717 100644 --- a/src/Suave/ConnectionFacade.fs +++ b/src/Suave/ConnectionFacade.fs @@ -11,7 +11,6 @@ open Suave open Suave.Utils open Suave.Utils.Parsing open Suave.Logging -open Suave.Logging.Message open Suave.Sockets open Suave.Sockets.Control open Suave.Sockets.SocketOp.Operators @@ -19,7 +18,7 @@ open Suave.Utils.Bytes open System.Threading open System.Threading.Tasks -type ConnectionFacade(connection: Connection, runtime: HttpRuntime, logger:Logger, connectionPool: ConcurrentPool, cancellationToken: CancellationToken,webpart:WebPart) = +type ConnectionFacade(connection: Connection, runtime: HttpRuntime, connectionPool: ConcurrentPool, cancellationToken: CancellationToken,webpart:WebPart) = let httpOutput = new HttpOutput(connection,runtime) @@ -49,7 +48,7 @@ type ConnectionFacade(connection: Connection, runtime: HttpRuntime, logger:Logge let filename = Net.WebUtility.UrlDecode(_filename.Substring(ix + 2)) SocketOp.mreturn (filename) else - SocketOp.abort (InputDataError (None, "Unsupported filename encoding: '" + enc + "'")) + SocketOp.abort (InputDataError (None, "Unsupported filename encoding: '" + enc + "'")) else SocketOp.abort (InputDataError (None, "Invalid filename encoding")) | Choice2Of2 _ -> @@ -84,11 +83,7 @@ type ConnectionFacade(connection: Connection, runtime: HttpRuntime, logger:Logge match partHeaders %% "content-type" with | Choice1Of2 contentType -> let headerParams = headerParams contentDisposition - logger.verbose (eventX "Parsing {contentType}... -> readFilePart" >> setFieldValue "contentType" contentType) - let! res = readFilePart boundary headerParams fieldName contentType - logger.verbose (eventX "Parsed {contentType} <- readFilePart" >> setFieldValue "contentType" contentType) - match res with | Some upload -> files.Add(upload) @@ -113,6 +108,7 @@ type ConnectionFacade(connection: Connection, runtime: HttpRuntime, logger:Logge let parseMultipart (boundary:string) : SocketOp = let parsePart () = socket { let! partHeaders = reader.readHeaders() + let! (contentDisposition : string) = (partHeaders %% "content-disposition") @|! (None, "Missing 'content-disposition'") @@ -123,9 +119,10 @@ type ConnectionFacade(connection: Connection, runtime: HttpRuntime, logger:Logge (headerParams.TryLookup "form-data" |> Choice.map (String.trimc '"')) @|! (None, "Key 'form-data' was not present in 'content-disposition'") - let! fieldName = - (headerParams.TryLookup "name" |> Choice.map (String.trimc '"')) - @|! (None, "Key 'name' was not present in 'content-disposition'") + let fieldName = + match headerParams.TryLookup "name" |> Choice.map (String.trimc '"') with + | Choice1Of2 s -> s + | _ -> failwith "Key 'name' was not present in 'content-disposition'" match partHeaders %% "content-type" with | Choice1Of2 x when String.startsWith "multipart/mixed" x -> @@ -135,16 +132,7 @@ type ConnectionFacade(connection: Connection, runtime: HttpRuntime, logger:Logge return () | Choice1Of2 contentType when headerParams.ContainsKey "filename" -> - logger.verbose ( - eventX "Parsing {contentType}... -> readFilePart" - >> setFieldValue "contentType" contentType - >> setSingleName "Suave.Web.parseMultipart") let! res = readFilePart boundary headerParams fieldName contentType - logger.verbose ( - eventX "Parsed {contentType} <- readFilePart" - >> setFieldValue "contentType" contentType - >> setSingleName "Suave.Web.parseMultipart") - res |> Option.iter files.Add | Choice1Of2 _ | Choice2Of2 _ -> @@ -177,8 +165,7 @@ type ConnectionFacade(connection: Connection, runtime: HttpRuntime, logger:Logge let mutable error = false let result = ref (Ok()) while parsing && not error && not(cancellationToken.IsCancellationRequested) do - let! _line = something () - match _line with + match! something () with | Ok line -> if line.StartsWith("--") then parsing <- false @@ -201,7 +188,7 @@ type ConnectionFacade(connection: Connection, runtime: HttpRuntime, logger:Logge /// Reads raw POST data let getRawPostData contentLength = - socket { + task { let offset = ref 0 let rawForm = Array.zeroCreate contentLength do! reader.readPostData contentLength (fun a count -> @@ -209,55 +196,46 @@ type ConnectionFacade(connection: Connection, runtime: HttpRuntime, logger:Logge let target = new Span(rawForm,!offset,count) source.CopyTo(target) offset := !offset + count) - return rawForm + return Ok (rawForm) } member val Connection = connection with get,set member val Runtime = runtime with get,set - member (*inline*) this.parsePostData maxContentLength (contentLengthHeader : Choice) (contentTypeHeader:Choice) = socket { - match contentLengthHeader with - | Choice1Of2 contentLengthString -> - let contentLength = Convert.ToInt32 contentLengthString - - if contentLength > maxContentLength then - return! SocketOp.abort(InputDataError (Some 413, "Payload too large")) - else - logger.verbose (eventX "Expecting {contentLength} bytes" >> setFieldValue "contentLength" contentLength) - - match contentTypeHeader with - | Choice1Of2 ce when String.startsWith "application/x-www-form-urlencoded" ce -> - let! rawForm = getRawPostData contentLength - _rawForm <- rawForm - return () - - | Choice1Of2 ce when String.startsWith "multipart/form-data" ce -> - let boundary = "--" + parseBoundary ce - - logger.verbose (eventX "Parsing multipart") - do! parseMultipart boundary - logger.verbose (eventX "Done parsing multipart") - return () + member (*inline*) this.parsePostData maxContentLength (contentLengthHeader : Choice) (contentTypeHeader:Choice) : Task>= + socket { + match contentLengthHeader with + | Choice1Of2 contentLengthString -> + let contentLength = Convert.ToInt32 contentLengthString + + if contentLength > maxContentLength then + return! SocketOp.abort(InputDataError (Some 413, "Payload too large")) + else + match contentTypeHeader with + | Choice1Of2 ce when String.startsWith "application/x-www-form-urlencoded" ce -> + let! rawForm = getRawPostData contentLength + _rawForm <- rawForm + | Choice1Of2 ce when String.startsWith "multipart/form-data" ce -> + let boundary = "--" + parseBoundary ce + do! parseMultipart boundary + | Choice1Of2 _ | Choice2Of2 _ -> + let! rawForm = getRawPostData contentLength + _rawForm <- rawForm - | Choice1Of2 _ | Choice2Of2 _ -> - let! rawForm = getRawPostData contentLength - _rawForm <- rawForm return () | Choice2Of2 _ -> return () } /// Process the request, reading as it goes from the incoming 'stream', yielding a HttpRequest /// when done - member (*inline*) this.processRequest () = socket { + member (*inline*) this.readRequest () = socket { - logger.verbose (eventX "reading first line of request") let! firstLine = reader.readLine() - let! rawMethod, path, rawQuery, httpVersion = + let! (rawMethod, path, rawQuery, httpVersion) = parseUrl firstLine - @|! (None, "Invalid ") + @|! (None, "Invalid first line") - logger.verbose (eventX "reading headers") let! headers = reader.readHeaders() // Respond with 400 Bad Request as @@ -266,9 +244,8 @@ type ConnectionFacade(connection: Connection, runtime: HttpRuntime, logger:Logge if headers %% "expect" = Choice1Of2 "100-continue" then let! _ = httpOutput.run HttpRequest.empty Intermediate.CONTINUE - logger.verbose (eventX "sent 100-continue response") + () - logger.verbose (eventX "parsing post data") do! this.parsePostData runtime.maxContentLength (headers %% "content-length") (headers %% "content-type") let request = @@ -284,66 +261,47 @@ type ConnectionFacade(connection: Connection, runtime: HttpRuntime, logger:Logge multiPartFields = Seq.toList multiPartFields trace = TraceHeader.parseTraceHeaders headers } - // clear form data before exit + // Clear form data before exit files.Clear() multiPartFields.Clear() _rawForm <- [||] - return Some request + return request } member this.exitHttpLoopWithError (err:Error) = task{ match err with | InputDataError (None, msg) -> - logger.verbose (eventX "Error parsing HTTP request with {message}" >> setFieldValue "message" msg) match! httpOutput.run HttpRequest.empty (RequestErrors.BAD_REQUEST msg) with - | _ -> - logger.verbose (eventX "Exiting http loop") + | _ -> () | InputDataError (Some status,msg) -> - logger.verbose (eventX "Error parsing HTTP request with {message}" >> setFieldValue "message" msg) match Http.HttpCode.tryParse status with | (Choice1Of2 statusCode) -> match! httpOutput.run HttpRequest.empty (Response.response statusCode (Encoding.UTF8.GetBytes msg)) with - | _ -> logger.verbose (eventX "Exiting http loop") + | _ -> () | (Choice2Of2 err) -> - logger.warn (eventX "Invalid HTTP status code {statusCode}" >> setFieldValue "statusCode" status) match! httpOutput.run HttpRequest.empty (RequestErrors.BAD_REQUEST msg) with - | _ -> - logger.verbose (eventX "Exiting http loop") - | err -> - logger.verbose (eventX "Socket error while processing request, exiting {error}" >> setFieldValue "error" err) + | _ -> () + | err -> () return Ok(false) } - member this.loop () = + member this.processRequest () = task { - logger.verbose (eventX "Processing request... -> processor") - let! result' = this.processRequest () - logger.verbose (eventX "Processed request. <- processor") - match result' with - | Ok result -> - match result with - | None -> - logger.verbose (eventX "'result = None', exiting") - return Ok (false) - | Some request -> - try - match! httpOutput.run request webpart with - | Result.Error err -> - return Result.Error err - | Ok keepAlive -> - if keepAlive then - logger.verbose (eventX "'Connection: keep-alive' recurse") - return Ok (keepAlive) - else - logger.verbose (eventX "Connection: close") - return Ok(false) - with ex -> - return Result.Error (Error.ConnectionError ex.Message) + match! this.readRequest () with | Result.Error err -> // Couldn't parse HTTP request; answering with BAD_REQUEST and closing the connection. return! this.exitHttpLoopWithError err + | Ok request -> + try + match! httpOutput.run request webpart with + | Result.Error err -> + return Result.Error err + | Ok keepAlive -> + return Ok (keepAlive) + with ex -> + return Result.Error (Error.ConnectionError ex.Message) } member this.shutdown() = @@ -360,7 +318,7 @@ type ConnectionFacade(connection: Connection, runtime: HttpRuntime, logger:Logge let flag = ref true let result = ref (Ok ()) while !flag && not (cancellationToken.IsCancellationRequested) do - let! b = this.loop () + let! b = this.processRequest () match b with | Ok b -> flag := b @@ -373,21 +331,21 @@ type ConnectionFacade(connection: Connection, runtime: HttpRuntime, logger:Logge member this.accept(binding) = task{ Interlocked.Increment Globals.numberOfClients |> ignore - logger.verbose (eventX "{client} connected. Now has {totalClients} connected" - >> setFieldValue "client" (binding.ip.ToString()) - >> setFieldValue "totalClients" (!Globals.numberOfClients)) + let clientIp = (binding.ip.ToString()) + if Globals.verbose then + Console.WriteLine("{0} connected. Now has {1} connected", clientIp,(!Globals.numberOfClients)) connection.socketBinding <- binding try let task = Task.Factory.StartNew(reader.readLoop,cancellationToken) let! a = this.requestLoop() () with - | :? System.IO.EndOfStreamException -> - logger.debug (eventX "Disconnected client (end of stream)") - logger.verbose (eventX "Shutting down transport") - this.shutdown() + | ex -> + if reader.isDirty then + reader.stop() + do Console.WriteLine("Error: " + ex.Message) Interlocked.Decrement(Globals.numberOfClients) |> ignore - logger.verbose (eventX "Disconnected {client}. {totalClients} connected." - >> setFieldValue "client" (binding.ip.ToString()) - >> setFieldValue "totalClients" (!Globals.numberOfClients)) + if Globals.verbose then + do Console.WriteLine("Disconnected {0}. {1} connected.", clientIp,(!Globals.numberOfClients)) + do this.shutdown() } diff --git a/src/Suave/Cookie.fs b/src/Suave/Cookie.fs index ee7c29fd..b80a45fe 100644 --- a/src/Suave/Cookie.fs +++ b/src/Suave/Cookie.fs @@ -3,11 +3,8 @@ namespace Suave module Cookie = open System - open System.Text open System.Globalization open Suave.Operators - open Suave.Logging - open Suave.Logging.Message open Suave.Utils type CookieLife = @@ -123,13 +120,6 @@ module Cookie = (Headers.Fields.Response.setCookie, header) :: headers) (ctx.response.headers |> List.filter notSetCookie) - if cookie.value.Length > 4096 then - ctx.runtime.logger.warn ( - eventX "Cookie {cookieName} has {cookieBytes} which is too large! Lengths over 4 096 bytes risk corruption in some browsers; consider alternate storage" - >> setFieldValue "cookieName" cookie.name - >> setFieldValue "cookieBytes" cookie.value.Length) - - succeed { ctx with response = { ctx.response with headers = headers' } } @@ -139,15 +129,7 @@ module Cookie = Writers.addHeader Headers.Fields.Response.setCookie stringValue let setPair (httpCookie : HttpCookie) (clientCookie : HttpCookie) : WebPart = - context (fun ctx -> - ctx.runtime.logger.debug ( - eventX "Setting {cookieName} to value of {cookieBytes}" - >> setFieldValue "cookieName" httpCookie.name - >> setFieldValue "cookieBytes" httpCookie.value.Length - >> setSingleName "Suave.Cookie.setPair") - - succeed) - >=> setCookie httpCookie + setCookie httpCookie >=> setCookie clientCookie let unsetPair httpCookieName : WebPart = @@ -209,19 +191,14 @@ module Cookie = let updateCookies (csctx : CookiesState) fPlainText : WebPart = context (fun ctx -> - let debug message = - ctx.runtime.logger.debug ( - eventX message - >> setSingleName "Suave.Cookie.updateCookies") - let plainText = match readCookies csctx.serverKey csctx.cookieName ctx.response.cookies with | Choice1Of2 (_, plainText) -> - debug "Existing cookie" + //debug "Existing cookie" fPlainText (Some plainText) | Choice2Of2 _ -> - debug "First time" + //debug "First time" fPlainText None // Since the contents will completely change every write, we simply re-generate the cookie @@ -238,9 +215,6 @@ module Cookie = (fSuccess : WebPart) : WebPart = context (fun ctx -> - let debug message = - ctx.runtime.logger.debug (eventX message >> setSingleName "Suave.Cookie.cookieState") - let setCookies plainText = let httpCookie, clientCookie = generateCookies csctx.serverKey csctx.cookieName @@ -252,7 +226,6 @@ module Cookie = match readCookies csctx.serverKey csctx.cookieName ctx.request.cookies with | Choice1Of2 (httpCookie, plainText) -> - debug "Existing cookie" refreshCookies csctx.relativeExpiry httpCookie >=> Writers.setUserData csctx.userStateKey plainText >=> fSuccess @@ -260,19 +233,14 @@ module Cookie = | Choice2Of2 (NoCookieFound _) -> match noCookie () with | Choice1Of2 plainText -> - debug "No existing cookie, setting text" setCookies plainText >=> fSuccess | Choice2Of2 wp_kont -> - debug "No existing cookie, calling WebPart continuation" wp_kont | Choice2Of2 (DecryptionError err) -> - debug ("decryption error: " + err.ToString()) match decryptionFailure err with | Choice1Of2 plainText -> - debug "Existing, broken cookie, setting cookie text anew" setCookies plainText >=> fSuccess | Choice2Of2 wpKont -> - debug "Existing, broken cookie, unsetting it, forwarding to given failure web part" wpKont >=> unsetPair csctx.cookieName) diff --git a/src/Suave/Globals.fs b/src/Suave/Globals.fs index 44d0a400..ffcd18a2 100644 --- a/src/Suave/Globals.fs +++ b/src/Suave/Globals.fs @@ -27,10 +27,26 @@ let SuaveVersion = Assembly.GetExecutingAssembly().GetName().Version.ToString() /// This is the server header let ServerHeader = "Server: Suave (https://suave.io)" +let mutable verbose = false + +open System.Diagnostics + +[] +type Logger = + + [] + static member debug (s:string) : unit = + Console.WriteLine s + + static member info (s:string) : unit = + Console.WriteLine s + + [] [] [] [] [] [] + do () \ No newline at end of file diff --git a/src/Suave/Http.fs b/src/Suave/Http.fs index 71dd51b7..09cb9899 100644 --- a/src/Suave/Http.fs +++ b/src/Suave/Http.fs @@ -369,7 +369,7 @@ module Http = type HttpContent = | NullContent | Bytes of byte [] - | SocketTask of (Connection * HttpResult -> SocketOp) + | SocketTask of (Connection * HttpResult -> Threading.Tasks.Task) and [] HttpResult = { status : HttpStatus @@ -383,7 +383,6 @@ module Http = mimeTypesMap : MimeTypesMap homeDirectory : string compressionFolder : string - logger : Logger matchedBinding : HttpBinding cookieSerialiser : CookieSerialiser hideHeader : bool @@ -476,20 +475,18 @@ module Http = mimeTypesMap = fun _ -> None homeDirectory = "." compressionFolder = "." - logger = Targets.create Debug [| "Suave" |] matchedBinding = HttpBinding.defaults cookieSerialiser = new BinaryFormatterSerialiser() hideHeader = false maxContentLength = 1024 } let create serverKey errorHandler mimeTypes homeDirectory compressionFolder - logger cookieSerialiser hideHeader maxContentLength binding = + (*logger*) cookieSerialiser hideHeader maxContentLength binding = { serverKey = serverKey errorHandler = errorHandler mimeTypesMap = mimeTypes homeDirectory = homeDirectory compressionFolder = compressionFolder - logger = logger matchedBinding = binding cookieSerialiser = cookieSerialiser hideHeader = hideHeader diff --git a/src/Suave/Http.fsi b/src/Suave/Http.fsi index 8bd0ad6b..ae60ca29 100644 --- a/src/Suave/Http.fsi +++ b/src/Suave/Http.fsi @@ -230,7 +230,7 @@ module Http = /// control the flow of bytes by using a `SocketOp`. Contrasting with `Bytes`, /// setting the `HttpContent` as this discriminated union type lets you stream /// data back to the client through Suave. - | SocketTask of (Connection * HttpResult -> SocketOp) + | SocketTask of (Connection * HttpResult -> Threading.Tasks.Task) /// The `HttpResult` is the structure that you work with to tell Suave how to @@ -257,7 +257,7 @@ module Http = mimeTypesMap : MimeTypesMap homeDirectory : string compressionFolder : string - logger : Logger + //logger : Logger matchedBinding : HttpBinding cookieSerialiser : CookieSerialiser hideHeader : bool @@ -337,7 +337,7 @@ module Http = /// make a new `HttpRuntime` from the given parameters val create : serverKey:ServerKey -> errorHandler:ErrorHandler -> mimeTypes:MimeTypesMap -> homeDirectory:string - -> compressionFolder:string -> logger:Logger + -> compressionFolder:string (*-> logger:Logger*) -> cookieSerialiser:CookieSerialiser -> hideHeader:bool -> maxContentLength:int -> binding:HttpBinding diff --git a/src/Suave/HttpOutput.fs b/src/Suave/HttpOutput.fs index 0710cb95..314d0b2f 100644 --- a/src/Suave/HttpOutput.fs +++ b/src/Suave/HttpOutput.fs @@ -3,9 +3,6 @@ namespace Suave open System.Collections.Generic open Suave.Utils open Suave.Sockets -open Suave.Sockets.Control -open Suave.Logging -open Suave.Logging.Message open System @@ -33,12 +30,12 @@ type HttpOutput(connection: Connection, runtime: HttpRuntime) = ; userState = new Dictionary() ; response = HttpResult.empty } - member (*inline*) this.writeContentType (headers : (string*string) list) = socket { + member (*inline*) this.writeContentType (headers : (string*string) list) = task { if not(List.exists(fun (x : string,_) -> x.ToLower().Equals("content-type")) headers )then return! connection.asyncWriteBufferedBytes ByteConstants.defaultContentTypeHeaderBytes } - member (*inline*) this.writeContentLengthHeader (content : byte[]) (context : HttpContext) = socket { + member (*inline*) this.writeContentLengthHeader (content : byte[]) (context : HttpContext) = task { match context.request.``method``, context.response.status.code with | (_, 100) | (_, 101) @@ -53,13 +50,15 @@ type HttpOutput(connection: Connection, runtime: HttpRuntime) = return! connection.asyncWriteBufferedArrayBytes [| ByteConstants.contentLengthBytes; ASCII.bytes (content.Length.ToString()); ByteConstants.EOLEOL |] } - member (*inline*) this.writeHeaders exclusions (headers : (string*string) seq) = socket { - for x,y in headers do + member (*inline*) this.writeHeaders exclusions (headers : (string*string) seq) = task { + use sourceEnumerator = headers.GetEnumerator() + while sourceEnumerator.MoveNext() do + let x,y = sourceEnumerator.Current if not (List.exists (fun y -> x.ToLower().Equals(y)) exclusions) then do! connection.asyncWriteLn (String.Concat [| x; ": "; y |]) } - member this.writePreamble (response:HttpResult) = socket { + member this.writePreamble (response:HttpResult) = task { let r = response let preamble = [| ByteConstants.httpVersionBytes; ASCII.bytes (r.status.code.ToString()); @@ -75,8 +74,8 @@ type HttpOutput(connection: Connection, runtime: HttpRuntime) = } member (*inline*) this.writeContent writePreamble context = function - | Bytes b -> socket { - let! (encoding, content : byte []) = Compression.transform b context + | Bytes b -> task { + let! (encoding, content : byte []) = Compression.transform b context match encoding with | Some n -> do! connection.asyncWriteLn (String.Concat [| "Content-Encoding: "; n.ToString() |]) @@ -98,10 +97,10 @@ type HttpOutput(connection: Connection, runtime: HttpRuntime) = else do! connection.flush() } - | SocketTask f -> socket{ + | SocketTask f -> task{ do! f (connection, context.response) } - | NullContent -> socket { + | NullContent -> task { if writePreamble then do! this.writeContentLengthHeader [||] context do! connection.flush() @@ -118,7 +117,7 @@ type HttpOutput(connection: Connection, runtime: HttpRuntime) = } member this.writeResponse (newCtx:HttpContext) = - socket{ + task{ if newCtx.response.writePreamble then do! this.writePreamble newCtx.response do! this.writeContent true newCtx newCtx.response.content @@ -133,30 +132,19 @@ type HttpOutput(connection: Connection, runtime: HttpRuntime) = try freshContext.request <- request freshContext.userState.Clear() - //Console.WriteLine "before calling webpart" let task = webPart freshContext - //Console.WriteLine "after calling webpart" - //Console.WriteLine "before Execute task" match! this.executeTask task with | Some ctx -> - //Console.WriteLine "before writeresponse" - match! this.writeResponse ctx with - | Ok () -> - //Console.WriteLine "after writeresponse" - let keepAlive = - match ctx.request.header "connection" with - | Choice1Of2 conn -> - String.equalsOrdinalCI conn "keep-alive" - | Choice2Of2 _ -> - ctx.request.httpVersion.Equals("HTTP/1.1") - //Console.WriteLine "exiting httpOutput.run" - return Ok (keepAlive) - | Result.Error err -> - ctx.runtime.logger.error (eventX "Socket error while writing response {error}" >> setFieldValue "error" err) - return Result.Error err + let! _ = this.writeResponse ctx + let keepAlive = + match ctx.request.header "connection" with + | Choice1Of2 conn -> + String.equalsOrdinalCI conn "keep-alive" + | Choice2Of2 _ -> + ctx.request.httpVersion.Equals("HTTP/1.1") + return Ok (keepAlive) | None -> return Ok (false) with ex -> - //Console.WriteLine "nother one bytes the doost" return Result.Error(Error.ConnectionError ex.Message) } diff --git a/src/Suave/Proxy.fs b/src/Suave/Proxy.fs index 4cb1b015..afb55dd3 100644 --- a/src/Suave/Proxy.fs +++ b/src/Suave/Proxy.fs @@ -7,7 +7,6 @@ open Suave.Utils open Suave.Operators open Suave.Successful open Suave.Sockets -open Suave.Sockets.Control let private (?) headers (name : string) = headers @@ -25,7 +24,7 @@ let private httpWebResponseToHttpContext (ctx : HttpContext) (response : HttpWeb |> Seq.map (fun k -> k, response.Headers.Get k) |> Seq.toList - let writeContentLengthHeader (conn:Connection) = socket { + let writeContentLengthHeader (conn:Connection) = task { match headers ? ("Content-Length") with | Some x -> do! conn.asyncWriteLn (sprintf "Content-Length: %s" x) @@ -36,7 +35,7 @@ let private httpWebResponseToHttpContext (ctx : HttpContext) (response : HttpWeb let content = SocketTask - (fun (conn, _) -> socket { + (fun (conn, _) -> task { do! writeContentLengthHeader conn do! conn.asyncWriteLn "" do! conn.flush() @@ -103,12 +102,6 @@ let proxy (newHost : Uri) : WebPart = return httpWebResponseToHttpContext ctx response |> Some | exn -> - ctx.runtime.logger.log - Logging.Error - (fun lvl -> - Logging.Message.event lvl (sprintf "Unable to proxy the request %A %A. " ctx.request.rawMethod remappedAddress) - |> Logging.Message.addExn exn) - return! ( OK "Unable to proxy the request. " diff --git a/src/Suave/Sockets/AsyncSocket.fs b/src/Suave/Sockets/AsyncSocket.fs index 2fd9794a..0ccfca52 100644 --- a/src/Suave/Sockets/AsyncSocket.fs +++ b/src/Suave/Sockets/AsyncSocket.fs @@ -10,63 +10,45 @@ open System open System.IO open System.Text -let transferStreamWithBuffer (buf: ArraySegment<_>) (toStream : Connection) (from : Stream) : SocketOp = +let transferStreamWithBuffer (buf: ArraySegment<_>) (toStream : Connection) (from : Stream) = task { - try let reading = ref true - let error = ref false - let errorResult = ref (Ok()) - while !reading && not !error do + while !reading do let! read = from.ReadAsync (buf.Array, 0, buf.Array.Length) if read <= 0 then reading := false else let! a = send toStream (new Memory<_>(buf.Array, 0, read)) - match a with - | Ok () -> () - | Result.Error e as a -> - error := true - errorResult := a - if !error then - return !errorResult - else - return Ok () - with ex -> - return Result.Error(Error.ConnectionError ex.Message) + () } /// Asynchronously write from the 'from' stream to the 'to' stream. -let transferStream (toStream : Connection) (from : Stream) : SocketOp = - socket { +let transferStream (toStream : Connection) (from : Stream) = + task { let buf = new ArraySegment<_>(Array.zeroCreate 8192) do! transferStreamWithBuffer buf toStream from } let internal zeroCharMemory = new Memory(Encoding.ASCII.GetBytes "0") -let transferStreamChunked (conn : Connection) (from : Stream) : SocketOp = - socket { - let buf = new ArraySegment<_>(Array.zeroCreate 1024) - - let rec doBlock conn = - socket { - let! read = SocketOp.ofAsync <| from.AsyncRead (buf.Array, buf.Offset, buf.Count) - +let transferStreamChunked (conn : Connection) (from : Stream) = + task { + let buf = new ArraySegment<_>(Array.zeroCreate 1024) + let reading = ref true + while !reading do + let! read = from.AsyncRead (buf.Array, buf.Offset, buf.Count) if read <= 0 then - do! send conn zeroCharMemory - do! send conn Bytes.eolMemory - do! send conn Bytes.eolMemory + let! _ = send conn zeroCharMemory + let! _ = send conn Bytes.eolMemory + let! _ = send conn Bytes.eolMemory + reading := false else let readHex = read.ToString("X") - do! send conn (Memory<_>(Encoding.ASCII.GetBytes readHex)) - do! send conn Bytes.eolMemory + let! _ = send conn (Memory<_>(Encoding.ASCII.GetBytes readHex)) + let! _ = send conn Bytes.eolMemory - do! send conn (Memory<_>(buf.Array, buf.Offset, read)) - do! send conn Bytes.eolMemory - - do! doBlock conn - } - - do! doBlock conn - } + let! _ = send conn (Memory<_>(buf.Array, buf.Offset, read)) + let! _ = send conn Bytes.eolMemory + () + } diff --git a/src/Suave/Sockets/Connection.fs b/src/Suave/Sockets/Connection.fs index 0c588e1e..7e90a891 100644 --- a/src/Suave/Sockets/Connection.fs +++ b/src/Suave/Sockets/Connection.fs @@ -25,29 +25,29 @@ type Connection = x.socketBinding.port /// Flush out whatever is in the lineBuffer - member inline this.flush () : SocketOp = - socket { + member inline this.flush () = + task { if this.lineBufferCount> 0 then - do! this.transport.write (new Memory<_>(this.lineBuffer,0,this.lineBufferCount)) + let! _ = this.transport.write (new Memory<_>(this.lineBuffer,0,this.lineBufferCount)) this.lineBufferCount <- 0 } - member inline this.asyncWrite (str: string) : SocketOp = - socket { + member inline this.asyncWrite (str: string) = + task { if str.Length = 0 then return () else let maxByteCount = Encoding.UTF8.GetMaxByteCount(str.Length) if maxByteCount > this.lineBuffer.Length then - do! this.transport.write (new Memory<_>(this.lineBuffer, 0, this.lineBufferCount)) + let! _ = this.transport.write (new Memory<_>(this.lineBuffer, 0, this.lineBufferCount)) let byteCount = Encoding.UTF8.GetBytes(str, 0, str.Length, this.lineBuffer, 0) // don't waste time buffering here - do! this.transport.write (new Memory<_>(this.lineBuffer, 0, byteCount)) + let! _ = this.transport.write (new Memory<_>(this.lineBuffer, 0, byteCount)) this.lineBufferCount <- 0 return () elif this.lineBufferCount + maxByteCount > this.lineBuffer.Length then - do! this.transport.write (new Memory<_>(this.lineBuffer, 0, this.lineBufferCount)) + let! _ = this.transport.write (new Memory<_>(this.lineBuffer, 0, this.lineBufferCount)) // the string, char index, char count, bytes, byte index let c = Encoding.UTF8.GetBytes(str, 0, str.Length, this.lineBuffer, 0) this.lineBufferCount <- 0 @@ -59,37 +59,34 @@ type Connection = return () } - member inline this.asyncWriteLn (s : string) : SocketOp = - socket { - return! this.asyncWrite (s + Bytes.eol) - } + member inline this.asyncWriteLn (s : string) = this.asyncWrite (s + Bytes.eol) /// Write the string s to the stream asynchronously from a byte array - member inline this.asyncWriteBytes (b : byte[]) : SocketOp = + member inline this.asyncWriteBytes (b : byte[]) = task { if b.Length > 0 then - return! this.transport.write (new Memory<_>(b, 0, b.Length)) - else - return Ok () + let! _ = this.transport.write (new Memory<_>(b, 0, b.Length)) + () } - member inline this.asyncWriteBufferedBytes (b : byte[]) : SocketOp = - socket { + member inline this.asyncWriteBufferedBytes (b : byte[]) = + task { if this.lineBufferCount + b.Length > this.lineBuffer.Length then // flush lineBuffer if this.lineBufferCount > 0 then - do! this.transport.write (new Memory<_>(this.lineBuffer, 0, this.lineBufferCount)) + let! _ = this.transport.write (new Memory<_>(this.lineBuffer, 0, this.lineBufferCount)) + () // don't waste time buffering here - do! this.transport.write (new Memory<_>(b, 0, b.Length)) + let! _ = this.transport.write (new Memory<_>(b, 0, b.Length)) this.lineBufferCount <- 0 else Buffer.BlockCopy(b, 0, this.lineBuffer,this.lineBufferCount, b.Length) this.lineBufferCount <- this.lineBufferCount + b.Length } - member inline this.asyncWriteBufferedArrayBytes (xxs:(byte[])[]) : SocketOp = + member inline this.asyncWriteBufferedArrayBytes (xxs:(byte[])[]) = let rec loop index = - socket{ + task{ if index >= xxs.Length then return () else @@ -98,7 +95,7 @@ type Connection = } loop 0 - member inline this.writeChunk (chunk : byte []) = socket { + member inline this.writeChunk (chunk : byte []) = task { let chunkLength = chunk.Length.ToString("X") do! this.asyncWriteLn chunkLength do! this.asyncWriteLn (System.Text.Encoding.UTF8.GetString(chunk)) diff --git a/src/Suave/Sockets/HttpReader.fs b/src/Suave/Sockets/HttpReader.fs index 7716c38e..7d58a72b 100644 --- a/src/Suave/Sockets/HttpReader.fs +++ b/src/Suave/Sockets/HttpReader.fs @@ -3,6 +3,7 @@ namespace Suave open System open System.Collections.Generic open System.Text +open System.Threading.Tasks open Suave.Sockets open Suave.Sockets.Control @@ -52,22 +53,24 @@ module Aux = type HttpReader(transport : TcpTransport, lineBuffer : byte array, pipe: Pipe, cancellationToken) = let mutable running : bool = true + let mutable dirty : bool = false member this.stop () = + pipe.Writer.Complete() + pipe.Reader.Complete() + pipe.Reset() running <- false + dirty <- false member (*inline*) x.readMoreData () = task { let buff = pipe.Writer.GetMemory() - match! transport.read buff with - | Ok x -> - if x > 0 then - pipe.Writer.Advance(x) - let! flushResult = pipe.Writer.FlushAsync(cancellationToken) - return Ok() - else - return Result.Error (Error.ConnectionError "no more data") - | Result.Error error -> - return Result.Error error + let! x = transport.read buff + if x > 0 then + pipe.Writer.Advance(x) + let! flushResult = pipe.Writer.FlushAsync(cancellationToken) + return Ok() + else + return Result.Error (Error.ConnectionError "no more data") } member (*inline*) x.getData () = task{ @@ -183,7 +186,7 @@ type HttpReader(transport : TcpTransport, lineBuffer : byte array, pipe: Pipe, c let flag = ref true let error = ref false let result = ref (Ok ([])) - while !flag && (not !error) && (not cancellationToken.IsCancellationRequested) do + while !flag && (not cancellationToken.IsCancellationRequested) do let! _line = x.readLine () match _line with | Ok line -> @@ -204,11 +207,11 @@ type HttpReader(transport : TcpTransport, lineBuffer : byte array, pipe: Pipe, c } /// Read the post data from the stream, given the number of bytes that makes up the post data. - member (*inline*) x.readPostData (bytes : int) (select:ReadOnlyMemory -> int -> unit) : SocketOp = - let rec loop (n:int) : SocketOp = + member (*inline*) x.readPostData (bytes : int) (select:ReadOnlyMemory -> int -> unit) : Task = + let rec loop (n:int) : Task = task { if n = 0 then - return Result.Ok() + return () else let! result = x.getData() let bufferSequence = result.Buffer @@ -217,18 +220,22 @@ type HttpReader(transport : TcpTransport, lineBuffer : byte array, pipe: Pipe, c if segment.Length > n then select segment n pipe.Reader.AdvanceTo(bufferSequence.GetPosition(int64(n))) - return Result.Ok() + return () else select segment segment.Length pipe.Reader.AdvanceTo(bufferSequence.GetPosition(int64(segment.Length))) return! loop (n - segment.Length) else - return Result.Ok() + return () } loop bytes + member this.isDirty = dirty + member this.readLoop() = task{ + dirty <- true let reading = ref true + running <- true let result = ref (Ok()) while running && !reading && not(cancellationToken.IsCancellationRequested) do let! a = this.readMoreData() @@ -237,6 +244,7 @@ type HttpReader(transport : TcpTransport, lineBuffer : byte array, pipe: Pipe, c | a -> reading := false result := a + this.stop() return result } diff --git a/src/Suave/Sockets/TcpTransport.fs b/src/Suave/Sockets/TcpTransport.fs index ada6d387..f889fb44 100644 --- a/src/Suave/Sockets/TcpTransport.fs +++ b/src/Suave/Sockets/TcpTransport.fs @@ -4,6 +4,7 @@ open System.Net open System.Net.Sockets open System.Threading open Suave +open System [] type TcpTransport(listenSocket : Socket, cancellationToken:CancellationToken) = @@ -12,13 +13,13 @@ type TcpTransport(listenSocket : Socket, cancellationToken:CancellationToken) = val mutable acceptSocket : Socket let shutdownSocket (acceptSocket:Socket) = - try - if acceptSocket <> null then - try - acceptSocket.Shutdown(SocketShutdown.Both) - with _ -> + // do not like this if here + if acceptSocket <> null then + try + acceptSocket.Shutdown(SocketShutdown.Both) + //Console.WriteLine("Socket shutdown smoothly") + finally acceptSocket.Dispose () - with _ -> () let remoteBinding (socket : Socket) : SocketBinding = let rep = socket.RemoteEndPoint :?> IPEndPoint @@ -32,16 +33,18 @@ type TcpTransport(listenSocket : Socket, cancellationToken:CancellationToken) = } member this.read (buf : ByteSegment) = - task{ - let! result = this.acceptSocket.ReceiveAsync(buf,cancellationToken) - return Ok(result) - } + this.acceptSocket.ReceiveAsync(buf,cancellationToken) + //task{ + //let! result = this.acceptSocket.ReceiveAsync(buf,cancellationToken) + //return Ok(result) + //} member this.write (buf : ByteSegment) = - task{ - let! result = this.acceptSocket.SendAsync(buf,cancellationToken) - return Ok() - } + this.acceptSocket.SendAsync(buf,cancellationToken) + //task{ + //let! result = this.acceptSocket.SendAsync(buf,cancellationToken) + //return Ok() + //} member this.shutdown() = shutdownSocket (this.acceptSocket) diff --git a/src/Suave/State.fs b/src/Suave/State.fs index 84be1f21..58287556 100644 --- a/src/Suave/State.fs +++ b/src/Suave/State.fs @@ -1,8 +1,6 @@ module Suave.State open Suave.Cookie -open Suave.Logging -open Suave.Logging.Message /// A session store is a reader and a writer function pair keyed on strings. type StateStore = @@ -25,13 +23,6 @@ module CookieStateStore = let write relativeExpiry (cookieName : string) (value : 'T) = context (fun ctx -> - let event message = - eventX message - >> setFieldValue "cookieName" cookieName - >> setSingleName "Suave.State.CookieStateStore.write" - - let debug eventFactory = - ctx.runtime.logger.debug eventFactory let cookieState = { serverKey = ctx.runtime.serverKey @@ -40,11 +31,8 @@ module CookieStateStore = relativeExpiry = relativeExpiry secure = false } - debug (event "Writing to {cookieName}") updateCookies cookieState (function | None -> - debug (event "In fPlainText, no existing cookie") - Map.empty |> Map.add cookieName (box value) |> ctx.runtime.cookieSerialiser.serialise @@ -52,13 +40,11 @@ module CookieStateStore = | Some data -> try let m = ctx.runtime.cookieSerialiser.deserialise data - debug (event "In fPlainText, has existing {cookie}" >> setFieldValue "cookie" m) m |> Map.add cookieName (box value) |> ctx.runtime.cookieSerialiser.serialise with ex -> - debug (event "In fPlainText, couldn't deserialize cookie data") Map.empty |> Map.add cookieName (box value) |> ctx.runtime.cookieSerialiser.serialise)) @@ -66,13 +52,6 @@ module CookieStateStore = let remove relativeExpiry (cookieName : string) = context (fun ctx -> - let event message = - eventX message - >> setFieldValue "cookieName" cookieName - >> setSingleName "Suave.State.CookieStateStore.remove" - - let debug eventFactory = - ctx.runtime.logger.debug eventFactory let cookieState = { serverKey = ctx.runtime.serverKey @@ -81,11 +60,8 @@ module CookieStateStore = relativeExpiry = relativeExpiry secure = false } - debug (event "Removing {cookieName}") updateCookies cookieState (function | None -> - debug (event "In fPlainText, no existing cookie") - Map.empty |> ctx.runtime.cookieSerialiser.serialise @@ -95,22 +71,16 @@ module CookieStateStore = ctx.runtime.cookieSerialiser.deserialise data with _ -> - debug (event "In fPlainText, couldn't deserialize cookie data") - Map.empty // Although not strictly needed, this allows us to avoid unnecessarily // re-serialising the same data if the key is not present. if m |> Map.containsKey cookieName then - debug (event "In fPlainText, has existing {cookie}" >> setFieldValue "cookie" m) - try m |> Map.remove cookieName // Remove the key if we have gotten this far. |> ctx.runtime.cookieSerialiser.serialise with _ -> - debug (event "In fPlainText, couldn't serialize cookie data") - // Return the original data on failure. data else @@ -119,9 +89,6 @@ module CookieStateStore = let stateful relativeExpiry secure : WebPart = context (fun ctx -> - ctx.runtime.logger.debug ( - eventX "Ensuring cookie state" - >> setSingleName "Suave.State.CookieStateStore.stateful") let cipherTextCorrupt = (fun s -> s.ToString()) >> RequestErrors.BAD_REQUEST >> Choice2Of2 diff --git a/src/Suave/Stream.fs b/src/Suave/Stream.fs index c35e309a..da12406a 100644 --- a/src/Suave/Stream.fs +++ b/src/Suave/Stream.fs @@ -3,7 +3,6 @@ module Suave.Stream open System.IO open Suave open Suave.Sockets -open Suave.Sockets.Control /// Send a stream back in the response with 200 status. /// A new stream will be created for every request and it will be disposed after the request completes. @@ -12,8 +11,8 @@ open Suave.Sockets.Control let okStream (makeStream : Async) : WebPart = fun ctx -> let write (conn: Connection, _) = - socket { - use! stream = SocketOp.ofAsync makeStream + task { + use! stream = makeStream do! conn.asyncWriteLn $"Content-Length: %i{stream.Length}\r\n" do! conn.flush() @@ -39,8 +38,8 @@ let okStream (makeStream : Async) : WebPart = let okStreamChunked (makeStream : Async) : WebPart = fun ctx -> let write (conn:Connection, _) = - socket { - use! stream = SocketOp.ofAsync makeStream + task { + use! stream = makeStream do! conn.asyncWriteLn "" do! conn.flush() diff --git a/src/Suave/Suave.fsproj b/src/Suave/Suave.fsproj index 81485d79..afcaecf2 100644 --- a/src/Suave/Suave.fsproj +++ b/src/Suave/Suave.fsproj @@ -8,7 +8,6 @@ - diff --git a/src/Suave/SuaveConfig.fs b/src/Suave/SuaveConfig.fs index a3df6595..7068d675 100644 --- a/src/Suave/SuaveConfig.fs +++ b/src/Suave/SuaveConfig.fs @@ -1,6 +1,5 @@ namespace Suave -open Suave.Logging open System /// The core configuration of suave. See also Suave.Web.default_config which @@ -41,14 +40,6 @@ type SuaveConfig = /// Folder for temporary compressed files compressedFilesFolder : string option - /// Suave's logger. You can override the default instance if you wish to - /// ship your logs, e.g. using https://www.nuget.org/packages/Logary.Adapters.Suave/ - /// Also, this logger will be configured by default for Suave unless you - /// explicitly use `Suave.Logging.Global.initialise` before starting the - /// web server (the first time – the second time, the static will already - /// have been initialised). - logger : Logger - /// The cookie serialiser to use for converting the data you save in cookies /// from your application into a byte array. cookieSerialiser : CookieSerialiser @@ -70,7 +61,6 @@ type SuaveConfig = member x.withMimeTypesMap(v) = { x with mimeTypesMap = v } member x.withHomeFolder(v) = { x with homeFolder = v } member x.withCompressedFilesFolder(v) = { x with compressedFilesFolder = v } - member x.withLogger(v) = { x with logger = v } member x.withHiddenHeader(v) = { x with hideHeader = v } member x.withMaxContentLength(v) = { x with maxContentLength = v } @@ -87,7 +77,6 @@ module SuaveConfig = config.mimeTypesMap contentFolder compressionFolder - config.logger config.cookieSerialiser config.hideHeader config.maxContentLength diff --git a/src/Suave/Tcp.fs b/src/Suave/Tcp.fs index c6320db0..84233c3b 100644 --- a/src/Suave/Tcp.fs +++ b/src/Suave/Tcp.fs @@ -4,14 +4,10 @@ open System open System.Threading open System.Net open System.Net.Sockets -open Suave.Logging -open Suave.Logging.Message open Suave.Sockets open Suave.Utils open System.Threading.Tasks -let private logger = Log.create "Suave.Tcp" - /// The max backlog of number of requests [] let MaxBacklog = Int32.MaxValue @@ -33,11 +29,9 @@ type StartedData = /// Stop the TCP listener server let stopTcp reason (socket : Socket) = try - logger.debug (eventX "Stopping TCP server {because}" >> setFieldValue "because" reason) socket.Dispose() - logger.debug (eventX "Stopped TCP server") with ex -> - logger.debug (eventX "Failure stopping TCP server" >> addExn ex) + () open System.IO.Pipelines open System.Runtime.InteropServices @@ -64,7 +58,7 @@ let createConnection listenSocket cancellationToken bufferSize = let createConnectionFacade connectionPool listenSocket (runtime: HttpRuntime) cancellationToken bufferSize webpart = let connection = createConnection listenSocket cancellationToken bufferSize - let facade = new ConnectionFacade(connection, runtime, logger, connectionPool,cancellationToken,webpart) + let facade = new ConnectionFacade(connection, runtime, connectionPool,cancellationToken,webpart) facade let createPools listenSocket maxOps runtime cancellationToken bufferSize (webpart:WebPart) = @@ -124,10 +118,6 @@ let enableRebinding (listenSocket: Socket) = else if RuntimeInformation.IsOSPlatform(OSPlatform.OSX) then setsockoptStatus <- setsockopt(listenSocket.Handle, SOL_SOCKET_OSX, SO_REUSEADDR_OSX, NativePtr.toNativeInt &&optionValue, uint32(sizeof)) - if setsockoptStatus <> 0 then - logger.warn(eventX "Setting SO_REUSEADDR failed with errno '{errno}'." >> setFieldValue "errno" (Marshal.GetLastWin32Error())) - - let runServer maxConcurrentOps bufferSize (binding: SocketBinding) (runtime:HttpRuntime) (cancellationToken: CancellationToken) (webpart: WebPart) startData (acceptingConnections: AsyncResultCell) = Task.Run(fun () -> @@ -142,7 +132,6 @@ let runServer maxConcurrentOps bufferSize (binding: SocketBinding) (runtime:Http aFewTimesDeterministic (fun () -> listenSocket.Bind binding.endpoint) listenSocket.Listen MaxBacklog - // Get the actual assigned port from listeSocket let _binding = { startData.binding with port = uint16((listenSocket.LocalEndPoint :?> IPEndPoint).Port) } let startData = @@ -150,26 +139,27 @@ let runServer maxConcurrentOps bufferSize (binding: SocketBinding) (runtime:Http acceptingConnections.complete startData |> ignore - logger.info ( - eventX "Smooth! Suave listener started in {startedListeningMilliseconds:#.###}ms with binding {ipAddress}:{port}" - >> setFieldValue "startedListeningMilliseconds" (startData.GetStartedListeningElapsedMilliseconds()) - // .Address can throw exceptions, just log its string representation - >> setFieldValue "ipAddress" (startData.binding.ip.ToString()) - >> setFieldValue "port" startData.binding.port - >> setSingleName "Suave.Tcp.runServer") + let startedListeningMilliseconds = (startData.GetStartedListeningElapsedMilliseconds()) + let ipAddress = (startData.binding.ip.ToString()) + let port = startData.binding.port + + Console.WriteLine($"Smooth! Suave listener started in {startedListeningMilliseconds} ms with binding {ipAddress}:{port}") - // we could have an old style imperative loop let remoteBinding (socket : Socket) = let rep = socket.RemoteEndPoint :?> IPEndPoint { ip = rep.Address; port = uint16 rep.Port } while not(cancellationToken.IsCancellationRequested) do let connection : ConnectionFacade = connectionPool.Pop() + let continuation (vt: Task) = connection.Connection.transport.acceptSocket <- vt.Result ignore(Task.Factory.StartNew(fun () -> connection.accept(remoteBinding vt.Result),cancellationToken)) - logger.verbose (eventX "Waiting for accept") + let task = listenSocket.AcceptAsync(cancellationToken) + + // kestrel uses here ThreadPool.UnsafeQueueUserWorkItem(kestrelConnection, preferLocal: false); + if task.IsCompleted then connection.Connection.transport.acceptSocket <- task.Result ignore(Task.Factory.StartNew(fun () -> connection.accept(remoteBinding task.Result),cancellationToken)) @@ -185,8 +175,6 @@ let runServer maxConcurrentOps bufferSize (binding: SocketBinding) (runtime:Http stopTcp "The operation was canceled" listenSocket | ex -> stopTcp "runtime exception" listenSocket - logger.fatal (eventX "TCP server failed" >> addExn ex) - //raise ex ) /// Start a new TCP server with a specific IP, Port and with a serve_client worker diff --git a/src/Suave/Utils/Logging.fs b/src/Suave/Utils/Logging.fs deleted file mode 100644 index 518c942a..00000000 --- a/src/Suave/Utils/Logging.fs +++ /dev/null @@ -1,963 +0,0 @@ -/// The logging namespace, which contains the logging abstraction for this -/// library. -namespace Suave.Logging - -open System -open System.Runtime.CompilerServices - -/// The log level denotes how 'important' the gauge or event message is. -[] -type LogLevel = - /// The log message is not that important; can be used for intricate debugging. - | Verbose - /// The log message is at a default level, debug level. Useful for shipping to - /// infrastructure that further processes it, but not so useful for human - /// inspection in its raw format, except during development. - | Debug - /// The log message is informational; e.g. the service started, stopped or - /// some important business event occurred. - | Info - /// The log message is a warning; e.g. there was an unhandled exception or - /// an even occurred which was unexpected. Sometimes human corrective action - /// is needed. - | Warn - /// The log message is at an error level, meaning an unhandled exception - /// occurred at a location where it is deemed important to keeping the service - /// running. A human should take corrective action. - | Error - /// The log message denotes a fatal error which cannot be recovered from. The - /// service should be shut down. Human corrective action is needed. - | Fatal - - /// Converts the LogLevel to a string - override x.ToString () = - match x with - | Verbose -> "verbose" - | Debug -> "debug" - | Info -> "info" - | Warn -> "warn" - | Error -> "error" - | Fatal -> "fatal" - - /// Converts the string passed to a Loglevel. - static member ofString (str : string) = - if str = null then invalidArg "str" "may not be null" - match str.ToLowerInvariant() with - | "verbose" -> Verbose - | "debug" -> Debug - | "info" -> Info - | "warn" -> Warn - | "error" -> Error - | "fatal" -> Fatal - | _ -> Info - - /// Turn the LogLevel into an integer - member x.toInt () = - (function - | Verbose -> 1 - | Debug -> 2 - | Info -> 3 - | Warn -> 4 - | Error -> 5 - | Fatal -> 6) x - - /// Turn an integer into a LogLevel - static member ofInt i = - (function - | 1 -> Verbose - | 2 -> Debug - | 3 -> Info - | 4 -> Warn - | 5 -> Error - | 6 -> Fatal - | _ as i -> failwithf "LogLevel matching integer %i is not available" i) i - - interface IComparable with - member x.CompareTo other = - compare (x.toInt()) (other.toInt()) - - static member op_LessThan (a, b) = - (a :> IComparable).CompareTo(b) < 0 - static member op_LessThanOrEqual (a, b) = - (a :> IComparable).CompareTo(b) <= 0 - static member op_GreaterThan (a, b) = - (a :> IComparable).CompareTo(b) > 0 - static member op_GreaterThanOrEqual (a, b) = - (a :> IComparable).CompareTo(b) >= 0 - - override x.GetHashCode () = - x.toInt () - - interface IComparable with - member x.CompareTo other = - match other with - | null -> - 1 - | :? LogLevel as tother -> - (x :> IComparable).CompareTo tother - | _ -> - failwithf "invalid comparison %A to %A" x other - - interface IEquatable with - member x.Equals other = - x.toInt() = other.toInt() - - override x.Equals other = - (x :> IComparable).CompareTo other = 0 - -/// Represents a logged value; either a Gauge or an Event. -type PointValue = - /// An event is what it sounds like; something occurred and needs to be - /// logged. Its field is named 'template' because it should not be interpolated - /// with values; instead these values should be put in the 'fields' field of - /// the Message. - | Event of template:string - /// This is as value for a metric, with a unit attached. The unit can be - /// something like Seconds or Hz. - | Gauge of value:int64 * units:string - -/// The # of nanoseconds after 1970-01-01 00:00:00. -type EpochNanoSeconds = int64 - -/// Helper functions for transforming DateTime to timestamps in unix epoch. -module DateTime = - - /// Get the Logary timestamp off the DateTime. - let timestamp (dt : DateTime) : EpochNanoSeconds = - (dt.Ticks - DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks) - * 100L - - /// Get the DateTimeOffset ticks off from the EpochNanoSeconds. - let ticksUTC (epoch : EpochNanoSeconds) : int64 = - epoch / 100L - + DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc).Ticks - -/// Helper functions for transforming DateTimeOffset to timestamps in unix epoch. -module DateTimeOffset = - - /// Get the Logary timestamp off the DateTimeOffset. - let timestamp (dt : DateTimeOffset) : EpochNanoSeconds = - (dt.Ticks - DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero).Ticks) - * 100L - - /// Get the DateTimeOffset ticks from EpochNanoSeconds - let ticksUTC (epoch : EpochNanoSeconds) : int64 = - epoch / 100L - + DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero).Ticks - -/// This is record that is logged. It's capable of representing both metrics -/// (gauges) and events. See https://github.com/logary/logary for details. -type Message = - { /// The 'path' or 'name' of this data point. Do not confuse template in - /// (Event template) = message.value - name : string[] - /// The main value for this metric or event. Either a Gauge or an Event. (A - /// discriminated union type) - value : PointValue - /// The structured-logging data. - fields : Map - /// When? nanoseconds since UNIX epoch. - timestamp : EpochNanoSeconds - /// How important? See the docs on the LogLevel type for details. - level : LogLevel } - - /// Gets the ticks for UTC since 0001-01-01 00:00:00 for this message. You - /// can pass this value into a DateTimeOffset c'tor - member x.utcTicks = - DateTimeOffset.ticksUTC x.timestamp - - /// If you're looking for how to transform the Message's fields, then use the - /// module methods rather than instance methods, since you'll be creating new - /// values rather than changing an existing value. - member x.README = - () - -/// The logger is the interface for calling code to use for logging. Its -/// different functions have different semantics - read the docs for each -/// method to choose the right one for your use-case. -type Logger = - /// Gets the name of the logger instance. - abstract member name : string[] - /// Logs with the specified log level with backpressure via the logging - /// library's buffers *and* ACK/flush to the underlying message targets. - /// - /// Calls to this function will block the caller only while executing the - /// callback (if the level is active). - /// - /// The returned async value will yield when the message has been flushed to - /// the underlying message targets. - /// - /// You need to start the (cold) async value for the logging to happen. - /// - /// You should not do blocking/heavy operations in the callback. - abstract member logWithAck : LogLevel -> (LogLevel -> Message) -> Async - - /// Logs with the specified log level with backpressure via the logging - /// library's buffers. - /// - /// Calls to this function will block the caller only while executing the - /// callback (if the level is active). - /// - /// The returned async value will yield when the message has been added to - /// the buffers of the logging library. - /// - /// You need to start the (cold) async value for the logging to happen. - /// - /// You should not do blocking/heavy operations in the callback. - abstract member log : LogLevel -> (LogLevel -> Message) -> unit - -/// Syntactic sugar on top of Logger for F# libraries. -[] -module LoggerEx = - open System.Diagnostics - - type Logger with - - [] - member x.verbose (messageFactory : LogLevel -> Message) : unit = - x.log Verbose messageFactory - - [] - member x.debug (messageFactory : LogLevel -> Message) : unit = - x.log LogLevel.Debug messageFactory - - member x.info (messageFactory : LogLevel -> Message) : unit = - x.log Info messageFactory - - member x.warn (messageFactory : LogLevel -> Message) : unit = - x.log Warn messageFactory - - member x.error (messageFactory : LogLevel -> Message) : unit = - x.log Error messageFactory - - member x.fatal (messageFactory : LogLevel -> Message) : unit = - x.log Fatal messageFactory - - member x.logSimple message : unit = - x.log message.level (fun _ -> message) - -type LoggingConfig = - { /// The `timestamp` function should preferably be monotonic and not 'jumpy' - /// or take much time to call. - timestamp : unit -> int64 - /// The `getLogger` function returns a logger that directly can be logged to. - getLogger : string[] -> Logger - /// When composing apps from the outside-in (rather than having a unified - /// framework with static/global config) with libraries (again, rather than - /// a unified framework) like is best-practice, there's not necessarily a - /// way to coordinate around the STDOUT and STDERR streams between - /// different libraries running things on different threads. Use Logary's - /// adapter to replace this semaphore with a global semaphore. - consoleSemaphore : obj } - -module Literate = - /// The output tokens, which can be potentially coloured. - type LiterateToken = - | Text | Subtext - | Punctuation - | LevelVerbose | LevelDebug | LevelInfo | LevelWarning | LevelError | LevelFatal - | KeywordSymbol | NumericSymbol | StringSymbol | OtherSymbol | NameSymbol - | MissingTemplateField - - type LiterateOptions = - { formatProvider : IFormatProvider - theme : LiterateToken -> ConsoleColor - getLogLevelText : LogLevel -> string - printTemplateFieldNames : bool } - - static member create ?formatProvider = - // note: literate is meant for human consumption, and so the default - // format provider of 'Current' is appropriate here. The reader expects - // to see the dates, numbers, currency, etc formatted in the local culture - { formatProvider = defaultArg formatProvider Globalization.CultureInfo.CurrentCulture - getLogLevelText = function - | Debug -> "DBG" - | Error -> "ERR" - | Fatal -> "FTL" - | Info -> "INF" - | Verbose -> "VRB" - | Warn -> "WRN" - theme = function - | Text -> ConsoleColor.White - | Subtext -> ConsoleColor.Gray - | Punctuation -> ConsoleColor.DarkGray - | LevelVerbose -> ConsoleColor.Gray - | LevelDebug -> ConsoleColor.Gray - | LevelInfo -> ConsoleColor.White - | LevelWarning -> ConsoleColor.Yellow - | LevelError -> ConsoleColor.Red - | LevelFatal -> ConsoleColor.Red - | KeywordSymbol -> ConsoleColor.Blue - | NumericSymbol -> ConsoleColor.Magenta - | StringSymbol -> ConsoleColor.Cyan - | OtherSymbol -> ConsoleColor.Green - | NameSymbol -> ConsoleColor.Gray - | MissingTemplateField -> ConsoleColor.Red - printTemplateFieldNames = false } - - static member createInvariant() = - LiterateOptions.create Globalization.CultureInfo.InvariantCulture - -/// Module that contains the 'known' keys of the Maps in the Message type's -/// fields/runtime data. -module Literals = - - /// What version of the Facade is this. This is a major version that allows the Facade - /// adapter to choose how it handles the API. - let FacadeVersion = 2u - - /// What language this Facade has. This controls things like naming standards. - let FacadeLanguage = "F#" - - [] - let FieldExnKey = "exn" - - [] - let FieldErrorsKey = "errors" - -module internal FsMtParser = - open System.Text - - type Property(name : string, format : string) = - static let emptyInstance = Property("", null) - static member empty = emptyInstance - member x.name = name - member x.format = format - member internal x.AppendPropertyString(sb : StringBuilder, ?replacementName) = - sb.Append("{") - .Append(defaultArg replacementName name) - .Append(match x.format with null | "" -> "" | _ -> ":" + x.format) - .Append("}") - override x.ToString() = x.AppendPropertyString(StringBuilder()).ToString() - - module internal ParserBits = - - let inline isNull o = - match o with - | null -> true - | _ -> false - - let inline isLetterOrDigit c = System.Char.IsLetterOrDigit c - let inline isValidInPropName c = c = '_' || System.Char.IsLetterOrDigit c - let inline isValidInFormat c = c <> '}' && (c = ' ' || isLetterOrDigit c || System.Char.IsPunctuation c) - let inline isValidCharInPropTag c = c = ':' || isValidInPropName c || isValidInFormat c - - [] - type Range(startIndex : int, endIndex : int) = - member inline x.start = startIndex - member inline x.``end`` = endIndex - member inline x.length = (endIndex - startIndex) + 1 - member inline x.getSubstring (s : string) = s.Substring(startIndex, x.length) - member inline x.isEmpty = startIndex = -1 && endIndex = -1 - static member inline substring (s : string, startIndex, endIndex) = s.Substring(startIndex, (endIndex - startIndex) + 1) - static member inline empty = Range(-1, -1) - - let inline tryGetFirstCharInRange predicate (s : string) (range : Range) = - let rec go i = - if i > range.``end`` then -1 - else if not (predicate s.[i]) then go (i+1) else i - go range.start - - let inline tryGetFirstChar predicate (s : string) first = - tryGetFirstCharInRange predicate s (Range(first, s.Length - 1)) - - let inline hasAnyInRange predicate (s : string) (range : Range) = - match tryGetFirstChar (predicate) s range.start with - | -1 -> - false - | i -> - i <= range.``end`` - - let inline hasAny predicate (s : string) = hasAnyInRange predicate s (Range(0, s.Length - 1)) - let inline indexOfInRange s range c = tryGetFirstCharInRange ((=) c) s range - - let inline tryGetPropInRange (template : string) (within : Range) : Property = - // Attempts to validate and parse a property token within the specified range inside - // the template string. If the property insides contains any invalid characters, - // then the `Property.Empty' instance is returned (hence the name 'try') - let nameRange, formatRange = - match indexOfInRange template within ':' with - | -1 -> - within, Range.empty // no format - | formatIndex -> - Range(within.start, formatIndex-1), Range(formatIndex+1, within.``end``) // has format part - let propertyName = nameRange.getSubstring template - if propertyName = "" || (hasAny (not<unit) : int = - // Finds the next text token (starting from the 'startAt' index) and returns the next character - // index within the template string. If the end of the template string is reached, or the start - // of a property token is found (i.e. a single { character), then the 'consumed' text is passed - // to the 'foundText' method, and index of the next character is returned. - let mutable escapedBuilder = Unchecked.defaultof // don't create one until it's needed - let inline append (ch : char) = if not (isNull escapedBuilder) then escapedBuilder.Append(ch) |> ignore - let inline createStringBuilderAndPopulate i = - if isNull escapedBuilder then - escapedBuilder <- StringBuilder() // found escaped open-brace, take the slow path - for chIndex = startAt to i-1 do append template.[chIndex] // append all existing chars - let rec go i = - if i >= template.Length then - template.Length // bail out at the end of the string - else - let ch = template.[i] - match ch with - | '{' -> - if (i+1) < template.Length && template.[i+1] = '{' then - createStringBuilderAndPopulate i; append ch; go (i+2) - else i // found an open brace (potentially a property), so bail out - | '}' when (i+1) < template.Length && template.[i+1] = '}' -> - createStringBuilderAndPopulate i; append ch; go (i+2) - | _ -> - append ch; go (i+1) - - let nextIndex = go startAt - if (nextIndex > startAt) then // if we 'consumed' any characters, signal that we 'foundText' - if isNull escapedBuilder then - foundText (Range.substring(template, startAt, nextIndex - 1)) - else - foundText (escapedBuilder.ToString()) - nextIndex - - let findPropOrText (start : int) (template : string) - (foundText : string -> unit) - (foundProp : Property -> unit) : int = - // Attempts to find the indices of the next property in the template - // string (starting from the 'start' index). Once the start and end of - // the property token is known, it will be further validated (by the - // tryGetPropInRange method). If the range turns out to be invalid, it's - // not a property token, and we return it as text instead. We also need - // to handle some special case here: if the end of the string is reached, - // without finding the close brace (we just signal 'foundText' in that case). - let nextInvalidCharIndex = - match tryGetFirstChar (not << isValidCharInPropTag) template (start+1) with - | -1 -> - template.Length - | idx -> - idx - - if nextInvalidCharIndex = template.Length || template.[nextInvalidCharIndex] <> '}' then - foundText (Range.substring(template, start, (nextInvalidCharIndex - 1))) - nextInvalidCharIndex - else - let nextIndex = nextInvalidCharIndex + 1 - let propInsidesRng = Range(start + 1, nextIndex - 2) - match tryGetPropInRange template propInsidesRng with - | prop when not (obj.ReferenceEquals(prop, Property.empty)) -> - foundProp prop - | _ -> - foundText (Range.substring(template, start, (nextIndex - 1))) - nextIndex - - /// Parses template strings such as "Hello, {PropertyWithFormat:##.##}" - /// and calls the 'foundTextF' or 'foundPropF' functions as the text or - /// property tokens are encountered. - let parseParts (template : string) foundTextF foundPropF = - let tlen = template.Length - let rec go start = - if start >= tlen then () else - match ParserBits.findNextNonPropText start template foundTextF with - | next when next <> start -> - go next - | _ -> - go (ParserBits.findPropOrText start template foundTextF foundPropF) - go 0 - -/// Internal module for formatting text for printing to the console. -module internal Formatting = - open System.Text - open Literals - open Literate - - let literateFormatValue (options : LiterateOptions) (fields : Map) = function - | Event template -> - let themedParts = ResizeArray() - let matchedFields = ResizeArray() - let foundText (text: string) = themedParts.Add (text, Text) - let foundProp (prop: FsMtParser.Property) = - match Map.tryFind prop.name fields with - | Some propValue -> - // render using string.Format, so the formatting is applied - let stringFormatTemplate = prop.AppendPropertyString(StringBuilder(), "0").ToString() - let fieldAsText = String.Format (options.formatProvider, stringFormatTemplate, [| propValue |]) - // find the right theme colour based on data type - let valueColour = - match propValue with - | :? bool -> - KeywordSymbol - | :? int16 | :? int32 | :? int64 | :? decimal | :? float | :? double -> - NumericSymbol - | :? string | :? char -> - StringSymbol - | _ -> - OtherSymbol - if options.printTemplateFieldNames then - themedParts.Add ("["+prop.name+"] ", Subtext) - matchedFields.Add prop.name - themedParts.Add (fieldAsText, valueColour) - - | None -> - themedParts.Add (prop.ToString(), MissingTemplateField) - - FsMtParser.parseParts template foundText foundProp - Set.ofSeq matchedFields, List.ofSeq themedParts - - | Gauge (value, units) -> - Set.empty, [ value.ToString(), NumericSymbol - units, KeywordSymbol ] - - let formatValue (fields : Map) (pv : PointValue) = - let matchedFields, themedParts = - literateFormatValue (LiterateOptions.createInvariant()) fields pv - matchedFields, System.String.Concat(themedParts |> List.map fst) - - let literateExceptionColouriser (options : LiterateOptions) (ex : exn) = - let stackFrameLinePrefix = " at" // 3 spaces - let monoStackFrameLinePrefix = " at" // 2 spaces - use exnLines = new System.IO.StringReader(ex.ToString()) - let rec go lines = - match exnLines.ReadLine() with - | null -> - List.rev lines // finished reading - | line -> - if line.StartsWith(stackFrameLinePrefix) || line.StartsWith(monoStackFrameLinePrefix) then - // subtext - go ((line, Subtext) :: (Environment.NewLine, Text) :: lines) - else - // regular text - go ((line, Text) :: (Environment.NewLine, Text) :: lines) - go [] - - let literateColouriseExceptions (context : LiterateOptions) message = - let exnExceptionParts = - match message.fields.TryFind FieldExnKey with - | Some (:? Exception as ex) -> - literateExceptionColouriser context ex - | _ -> - [] // there is no spoon - let errorsExceptionParts = - match message.fields.TryFind FieldErrorsKey with - | Some (:? List as exnListAsObjList) -> - exnListAsObjList |> List.collect (function - | :? exn as ex -> - literateExceptionColouriser context ex - | _ -> - []) - | _ -> - [] - - exnExceptionParts @ errorsExceptionParts - - let getLogLevelToken = function - | Verbose -> LevelVerbose - | Debug -> LevelDebug - | Info -> LevelInfo - | Warn -> LevelWarning - | Error -> LevelError - | Fatal -> LevelFatal - - /// Split a structured message up into theme-able parts (tokens), allowing the - /// final output to display to a user with colours to enhance readability. - let literateDefaultTokeniser (options : LiterateOptions) (message : Message) : (string * LiterateToken) list = - let formatLocalTime (utcTicks : int64) = - DateTimeOffset(utcTicks, TimeSpan.Zero).LocalDateTime.ToString("HH:mm:ss", options.formatProvider), - Subtext - - let themedMessageParts = - message.value |> literateFormatValue options message.fields |> snd - - let themedExceptionParts = literateColouriseExceptions options message - - [ "[", Punctuation - formatLocalTime message.utcTicks - " ", Subtext - options.getLogLevelText message.level, getLogLevelToken message.level - "] ", Punctuation ] - @ themedMessageParts - @ themedExceptionParts - - let literateDefaultColourWriter sem (parts : (string * ConsoleColor) list) = - lock sem <| fun _ -> - let originalColour = Console.ForegroundColor - let mutable currentColour = originalColour - parts |> List.iter (fun (text, colour) -> - if currentColour <> colour then - Console.ForegroundColor <- colour - currentColour <- colour - Console.Write(text) - ) - if currentColour <> originalColour then - Console.ForegroundColor <- originalColour - - /// let the ISO8601 love flow - let defaultFormatter (message : Message) = - let app (x : obj) (sb : StringBuilder) = - sb.Append x |> ignore - - let formatLevel (level : LogLevel) = - "[" + Char.ToUpperInvariant(level.ToString().[0]).ToString() + "] " - - let formatInstant (utcTicks : int64) = - (DateTimeOffset(utcTicks, TimeSpan.Zero).ToString("o")) + ": " - - let formatName (name : string[]) = - " [" + String.concat "." name + "]" - - let formatExn (fields : Map) = - match fields |> Map.tryFind FieldExnKey with - | None -> - String.Empty - | Some ex -> - " exn:\n" + ex.ToString() - - let formatFields (ignored : Set) (fields : Map) = - if not (Map.isEmpty fields) then - fields - |> Seq.filter (fun (KeyValue (k, _)) -> not (ignored |> Set.contains k)) - |> Seq.map (fun (KeyValue (k, v)) -> "\n - " + k + ": " + v.ToString()) - |> String.concat "" - else - "" - - let matchedFields, valueString = - formatValue message.fields message.value - - // [I] 2014-04-05T12:34:56Z: Hello World! [my.sample.app] - formatLevel message.level + - formatInstant message.utcTicks + - valueString + - formatName message.name + - formatExn message.fields + - formatFields matchedFields message.fields - -/// Assists with controlling the output of the `LiterateConsoleTarget`. -module internal LiterateFormatting = - open Literate - type TokenisedPart = string * LiterateToken - type LiterateTokeniser = LiterateOptions -> Message -> TokenisedPart list - - type internal TemplateToken = TextToken of text:string | PropToken of name : string * format : string - let internal parseTemplate template = - let tokens = ResizeArray() - let foundText (text: string) = tokens.Add (TextToken text) - let foundProp (prop: FsMtParser.Property) = tokens.Add (PropToken (prop.name, prop.format)) - FsMtParser.parseParts template foundText foundProp - tokens :> seq - - [] - module OutputTemplateTokenisers = - - let tokeniseTimestamp format (options : LiterateOptions) (message : Message) = - let localDateTimeOffset = DateTimeOffset(message.utcTicks, TimeSpan.Zero).ToLocalTime() - let formattedTimestamp = localDateTimeOffset.ToString(format, options.formatProvider) - seq { yield formattedTimestamp, Subtext } - - let tokeniseTimestampUtc format (options : LiterateOptions) (message : Message) = - let utcDateTimeOffset = DateTimeOffset(message.utcTicks, TimeSpan.Zero) - let formattedTimestamp = utcDateTimeOffset.ToString(format, options.formatProvider) - seq { yield formattedTimestamp, Subtext } - - let tokeniseMissingField name format = - seq { - yield "{", Punctuation - yield name, MissingTemplateField - if not (String.IsNullOrEmpty format) then - yield ":", Punctuation - yield format, Subtext - yield "}", Punctuation } - - let tokeniseLogLevel (options : LiterateOptions) (message : Message) = - seq { yield options.getLogLevelText message.level, Formatting.getLogLevelToken message.level } - - let tokeniseSource (options : LiterateOptions) (message : Message) = - seq { yield (String.concat "." message.name), Subtext } - - let tokeniseNewline (options : LiterateOptions) (message : Message) = - seq { yield Environment.NewLine, Text } - - let tokeniseTab (options : LiterateOptions) (message : Message) = - seq { yield "\t", Text } - - /// Creates a `LiterateTokeniser` function which can be passed to the `LiterateConsoleTarget` - /// constructor in order to customise how each log message is rendered. The default template - /// would be: `[{timestampLocal:HH:mm:ss} {level}] {message}{newline}{exceptions}`. - /// Available template fields are: `timestamp`, `timestampUtc`, `level`, `source`, - /// `newline`, `tab`, `message`, `exceptions`. Any misspelled or otheriwese invalid property - /// names will be treated as `LiterateToken.MissingTemplateField`. - let tokeniserForOutputTemplate template : LiterateTokeniser = - let tokens = parseTemplate template - fun options message -> - seq { - for token in tokens do - match token with - | TextToken text -> yield text, LiterateToken.Punctuation - | PropToken (name, format) -> - match name with - | "timestamp" -> yield! tokeniseTimestamp format options message - | "timestampUtc" -> yield! tokeniseTimestampUtc format options message - | "level" -> yield! tokeniseLogLevel options message - | "source" -> yield! tokeniseSource options message - | "newline" -> yield! tokeniseNewline options message - | "tab" -> yield! tokeniseTab options message - | "message" -> yield! Formatting.literateFormatValue options message.fields message.value |> snd - | "exceptions" -> yield! Formatting.literateColouriseExceptions options message - | _ -> yield! tokeniseMissingField name format - } - |> Seq.toList - -/// Logs a line in a format that is great for human consumption, -/// using console colours to enhance readability. -/// Sample: [10:30:49 INF] User "AdamC" began the "checkout" process with 100 cart items -type LiterateConsoleTarget(name, minLevel, ?options, ?literateTokeniser, ?outputWriter, ?consoleSemaphore) = - let sem = defaultArg consoleSemaphore (obj()) - let options = defaultArg options (Literate.LiterateOptions.create()) - let tokenise = defaultArg literateTokeniser Formatting.literateDefaultTokeniser - let colourWriter = defaultArg outputWriter Formatting.literateDefaultColourWriter sem - - let colouriseThenNewLine message = - (tokenise options message) @ [Environment.NewLine, Literate.Text] - |> List.map (fun (s, t) -> - s, options.theme(t)) - - /// Creates the target with a custom output template. The default `outputTemplate` - /// is `[{timestampLocal:HH:mm:ss} {level}] {message}{exceptions}`. - /// Available template fields are: `timestamp`, `timestampUtc`, `level`, `source`, - /// `newline`, `tab`, `message`, `exceptions`. Any misspelled or otheriwese invalid property - /// names will be treated as `LiterateToken.MissingTemplateField`. - new (name, minLevel, outputTemplate, ?options, ?outputWriter, ?consoleSemaphore) = - let tokeniser = LiterateFormatting.tokeniserForOutputTemplate outputTemplate - LiterateConsoleTarget(name, minLevel, ?options=options, literateTokeniser=tokeniser, ?outputWriter=outputWriter, ?consoleSemaphore=consoleSemaphore) - - interface Logger with - member x.name = name - member x.logWithAck level msgFactory = - if level >= minLevel then - colourWriter (colouriseThenNewLine (msgFactory level)) - async.Return () - member x.log level msgFactory = - if level >= minLevel then - colourWriter (colouriseThenNewLine (msgFactory level)) - -type TextWriterTarget(name, minLevel, writer : System.IO.TextWriter, ?formatter) = - let formatter = defaultArg formatter Formatting.defaultFormatter - let log msg = writer.WriteLine(formatter msg) - - interface Logger with - member x.name = name - member x.log level messageFactory = - if level >= minLevel then log (messageFactory level) - - member x.logWithAck level messageFactory = - if level >= minLevel then log (messageFactory level) - async.Return () - -type OutputWindowTarget(name, minLevel, ?formatter) = - let formatter = defaultArg formatter Formatting.defaultFormatter - let log msg = System.Diagnostics.Debug.WriteLine(formatter msg) - - interface Logger with - member x.name = name - member x.log level messageFactory = - if level >= minLevel then log (messageFactory level) - - member x.logWithAck level messageFactory = - if level >= minLevel then log (messageFactory level) - async.Return () - -/// A logger to use for combining a number of other loggers -type CombiningTarget(name, otherLoggers : Logger list) = - interface Logger with - member x.name = name - member x.logWithAck level messageFactory = - otherLoggers - |> List.map (fun l -> l.logWithAck level messageFactory) - |> Async.Parallel - |> Async.Ignore // Async - - member x.log level messageFactory = - otherLoggers - |> List.map (fun l -> l.log level messageFactory) - |> ignore - -module Global = - /// This is the global semaphore for colourising the console output. Ensure - /// that the same semaphore is used across libraries by using the Logary - /// Facade Adapter in the final composing app/service. - let private consoleSemaphore = obj () - - /// The global default configuration, which logs to Console at Info level. - let defaultConfig = - { timestamp = fun () -> DateTimeOffset.timestamp DateTimeOffset.UtcNow - getLogger = fun name -> LiterateConsoleTarget(name, Info) :> Logger - consoleSemaphore = consoleSemaphore } - - let private config = - ref (defaultConfig, (* logical clock *) 1u) - - /// The flyweight just references the current configuration. If you want - /// multiple per-process logging setups, then don't use the static methods, - /// but instead pass a Logger instance around, setting the name field of the - /// Message value you pass into the logger. - type internal Flyweight(name : string[]) = - let updating = obj() - let mutable fwClock : uint32 = snd !config - let mutable logger : Logger = (fst !config).getLogger name - let rec withLogger action = - let cfg, cfgClock = !config // copy to local - let fwCurr = fwClock // copy to local - if cfgClock <> fwCurr then - lock updating <| fun _ -> - logger <- cfg.getLogger name - fwClock <- fwCurr + 1u - action logger - - let ensureName (m : Message) = - if Array.isEmpty m.name then { m with name = name } else m - - interface Logger with - member x.name = name - member x.log level msgFactory = - withLogger (fun logger -> logger.log level (msgFactory >> ensureName)) - - member x.logWithAck level msgFactory = - withLogger (fun logger -> logger.logWithAck level (msgFactory >> ensureName)) - - let internal getStaticLogger (name : string []) = - Flyweight name - - let timestamp () : EpochNanoSeconds = - (fst !config).timestamp () - - /// Returns the synchronisation object to use when printing to the console. - let internal semaphore () = - (fst !config).consoleSemaphore - - /// Run the passed function under the console semaphore lock. - let internal lockSem fn = - lock (semaphore ()) fn - - /// Call from the initialisation of your library. Initialises the - /// Suave.Logging globally/per process. - let initialise cfg = - config := (cfg, snd !config + 1u) - - let initialiseIfDefault cfg = - if snd !config = 1u then initialise cfg - -/// "Shortcut" for creating targets; useful at the top-level configuration point of -/// your library. -module Targets = - /// Create a new target. Prefer `Log.create` in your own libraries, or let the - /// composing app replace your target instance through your configuration. - /// - /// Will log to console (colourised) by default, and also to the output window - /// in your IDE if you specify a level below Info. - let create level name = - if level >= LogLevel.Info then - LiterateConsoleTarget(name, level, consoleSemaphore = Global.semaphore()) :> Logger - else - CombiningTarget( - name, - [ LiterateConsoleTarget(name, level, consoleSemaphore = Global.semaphore()) - OutputWindowTarget(name, level) ]) - :> Logger - -/// Module for acquiring static loggers (when you don't want or can't) -/// pass loggers as values. -module Log = - - /// Create a named logger. Full stop (.) acts as segment delimiter in the - /// hierachy of namespaces and loggers. - let create (name : string) = - if name = null then invalidArg "name" "name is null" - Global.getStaticLogger (name.Split([|'.'|], StringSplitOptions.RemoveEmptyEntries)) - :> Logger - - /// Create an hierarchically named logger - let createHiera (name : string[]) = - if name = null then invalidArg "name" "name is null" - if name.Length = 0 then invalidArg "name" "must have >0 segments" - Global.getStaticLogger name - :> Logger - -/// The Message module contains functions that can help callers compose messages. This -/// module is especially helpful to open to make calls into Logary's facade small. -[] -module Message = - open Literals - - /// Create a new event log message. - let event level template = - { name = [||] - value = Event template - fields = Map.empty - timestamp = Global.timestamp () - level = level } - - /// Create a new event log message – like `event` but with parameters flipped. - /// Useful to use with `Logger.log` with point-free style, to reduce the - /// noise. E.g. `logger.logVerbose (eventX "Returned {code}" >> setField "code" 24)` - let eventX template level = - event level template - - /// Create a new instantaneous value in a log message. - let gauge value units = - { name = [||] - value = Gauge (value, units) - fields = Map.empty - timestamp = Global.timestamp () - level = Debug } - - /// Sets the name/path of the log message. - let setName (name : string[]) (x : Message) = - { x with name = name } - - /// Sets the final portion o fthe name of the Message. - let setNameEnding (ending : string) (x : Message) = - if String.IsNullOrWhiteSpace ending then x else - let segs = ResizeArray<_>(x.name) - segs.Add ending - { x with name = segs.ToArray() } - - /// Sets the name as a single string; if this string contains dots, the string - /// will be split on these dots. - let setSingleName (name : string) (x : Message) = - if name = null then invalidArg "name" "may not be null" - - let name' = - name.Split([|'.'|], StringSplitOptions.RemoveEmptyEntries) - - x |> setName name' - - /// Sets the value of the field on the log message. - let setField (key : string) (value : obj) (x : Message) = - { x with fields = x.fields |> Map.add key value } - - /// Alias to `setField` - let setFieldValue = setField - - /// Sets the timestamp on the log message. - let setTimestamp (ts : EpochNanoSeconds) (x : Message) = - { x with timestamp = ts } - - /// Sets the level on the log message. - let setLevel (level : LogLevel) (x : Message) = - { x with level = level } - - /// Adds an exception to the Message, to the 'errors' field, inside a list. - let addExn ex (x : Message) = - let fields' = - match Map.tryFind FieldErrorsKey x.fields with - | None -> - x.fields |> Map.add FieldErrorsKey (box [ box ex ]) - - | Some errors -> - let arr : obj list = unbox errors - x.fields |> Map.add FieldErrorsKey (box (box ex :: arr)) - - { x with fields = fields' } diff --git a/src/Suave/Web.fs b/src/Suave/Web.fs index adcb927f..62a14070 100644 --- a/src/Suave/Web.fs +++ b/src/Suave/Web.fs @@ -11,13 +11,10 @@ module Web = open System.IO open System.Text open Suave.Utils - open Suave.Logging - open Suave.Logging.Message /// The default error handler returns a 500 Internal Error in response to /// thrown exceptions. let defaultErrorHandler (ex : Exception) msg (ctx : HttpContext) = - ctx.runtime.logger.error (eventX msg >> setSingleName "Suave.Web.defaultErrorHandler" >> addExn ex) if ctx.isLocalTrustProxy then Response.response HTTP_500 (Encoding.UTF8.GetBytes("

" + ex.Message + "


" + ex.ToString())) ctx else @@ -49,10 +46,6 @@ module Web = // spawn tcp listeners/web workers let toRuntime = SuaveConfig.toRuntime config homeFolder compressionFolder - // If noone has already touched the logging configuration, initialise it to - // that of Suave's configuration. - Global.initialiseIfDefault { Global.defaultConfig with getLogger = fun _ -> config.logger } - let startWebWorker runtime = let tcpServer = (tcpServerFactory :> TcpServerFactory).create(config.maxOps, config.bufferSize, runtime.matchedBinding.socketBinding,runtime,config.cancellationToken,webpart) @@ -86,7 +79,6 @@ module Web = mimeTypesMap = Writers.defaultMimeTypesMap homeFolder = None compressedFilesFolder = None - logger = Targets.create Info [| "Suave" |] cookieSerialiser = new BinaryFormatterSerialiser() hideHeader = false maxContentLength = 10000000 // 10 megabytes diff --git a/src/Suave/WebSocket.fs b/src/Suave/WebSocket.fs index 4e18e5a0..ab25ccd3 100644 --- a/src/Suave/WebSocket.fs +++ b/src/Suave/WebSocket.fs @@ -7,8 +7,6 @@ module WebSocket = open Suave.Operators open Suave.Utils open Suave.Utils.AsyncExtensions - open Suave.Logging - open Suave.Logging.Message open System open System.Security.Cryptography @@ -111,10 +109,11 @@ module WebSocket = let internal bytesToNetworkOrder (bytes : byte[]) = if BitConverter.IsLittleEndian then bytes |> Array.rev else bytes - let writeFrame (connection: Connection) (f: Frame) = socket { - do! connection.transport.write (Memory([| f.OpcodeByte |], 0, 1)) - do! connection.transport.write (Memory(f.EncodedLength, 0, f.EncodedLength.Length)) - do! connection.transport.write f.Data + let writeFrame (connection: Connection) (f: Frame) = task { + let! _ = connection.transport.write (Memory([| f.OpcodeByte |], 0, 1)) + let! _ = connection.transport.write (Memory(f.EncodedLength, 0, f.EncodedLength.Length)) + let! _ = connection.transport.write f.Data + return Ok() } let internal frame (opcode : Opcode) (data : ByteSegment) (fin : bool) = @@ -136,7 +135,7 @@ module WebSocket = { Frame.OpcodeByte = firstByte; EncodedLength = encodedLength; Data = data } let readBytes (connection : Connection) (n : int) = - socket { + task { let arr = Array.zeroCreate n let offset = ref 0 let reader = connection.reader @@ -145,7 +144,7 @@ module WebSocket = let target = new Span(arr,!offset,count) source.CopyTo(target) offset := !offset + count) - return arr + return Ok(arr) } let readBytesIntoByteSegment retrieveByteSegment (connection : Connection) (n : int) = @@ -159,13 +158,13 @@ module WebSocket = let offset = ref 0 let reader = connection.reader - socket{ + task{ do! reader.readPostData n (fun a count -> let source = a.Span.Slice(0,count) let target = byteSegment.Span.Slice(!offset,count) source.CopyTo(target) offset := !offset + count) - return byteSegment + return Ok(byteSegment) } type internal Cont<'t> = 't -> unit @@ -208,29 +207,6 @@ module WebSocket = let frame = frame opcode bs fin runAsyncWithSemaphore writeSemaphore (writeFrame connection frame) - let readFrame () = socket { - //assert (Seq.length connection.segments = 0) - let! arr = readBytes connection 2 - let header = exctractHeader arr - let! extendedLength = readExtendedLength header - - assert(header.hasMask) - let! mask = readBytes connection 4 - - if extendedLength > uint64 Int32.MaxValue then - let reason = "Frame size of " + extendedLength.ToString() + " bytes exceeds maximum accepted frame size (2 GB)" - let data = - [| yield! BitConverter.GetBytes (CloseCode.CLOSE_TOO_LARGE.code) |> bytesToNetworkOrder - yield! Encoding.UTF8.GetBytes reason |] - do! sendFrame Close (Memory(data)) true - return! SocketOp.abort (InputDataError(None, reason)) - else - let! frame = readBytes connection (int extendedLength) - // Messages from the client MUST be masked - let data = if header.hasMask then frame |> Array.mapi (fun i x -> x ^^^ mask.[i % 4]) else frame - return (header.opcode, data, header.fin) - } - let readFrameIntoSegment (byteSegmentForLengthFunc: int -> ByteSegment) = socket { //assert (Seq.length connection.segments = 0) let! arr = readBytes connection 2 @@ -256,16 +232,12 @@ module WebSocket = for i = 0 to (int extendedLength) - 1 do frame.Span.[i] <- frame.Span.[i] ^^^ mask.[i % 4] - return (header.opcode, frame, header.fin) + return header.opcode, frame, header.fin } - member this.read () = task { - let! result = runAsyncWithSemaphore readSemaphore (readFrameIntoSegment (Array.zeroCreate >> Memory)) - return - match result with - | Ok(opcode, frame, header) -> Ok(opcode, frame, header) - | Result.Error(error) -> Result.Error(error) - } + member this.read () = socket { + return! runAsyncWithSemaphore readSemaphore (readFrameIntoSegment (Array.zeroCreate >> Memory)) + } /// Reads from the websocket and puts the data into a ByteSegment selected by the byteSegmentForLengthFunc parameter /// A function that takes in the message length in bytes required to hold the next websocket message and returns an appropriately sized ArraySegment of bytes @@ -336,8 +308,7 @@ module WebSocket = | Ok _ -> do () | Result.Error err -> - ctx.runtime.logger.warn (eventX "WebSocket disconnected {error}" >> setSingleName "Suave.Websocket.handShakeWithSubprotocol" - >> setFieldValue "error" err) + do () return! Control.CLOSE ctx | Choice2Of2 response -> return! response } @@ -351,8 +322,7 @@ module WebSocket = | Ok _ -> do () | Result.Error err -> - ctx.runtime.logger.warn (eventX "WebSocket disconnected {error}" >> setSingleName "Suave.Websocket.handShake" - >> setFieldValue "error" err) + Console.WriteLine($"WebSocket disconnected {err}",err) return! Control.CLOSE ctx | Choice2Of2 response -> return! response