From 00d17dea79284a7e3d43be07247e0ab04ed4b932 Mon Sep 17 00:00:00 2001 From: Aristarkh Zagorodnikov Date: Mon, 3 Jul 2017 21:11:23 +0300 Subject: [PATCH] Domain socket handles (#1922) * UvPipeHandle.Open(IntPtr) and underlying interop * LibuvConstants.ENOTSUP * IEndpointInformation.HandleType along with ListenOptions extra ctor and handle type re-specification * Exception-based auto-detection of socket type in Listener, accept socket creation support for detected handle types in ListenerContext * Added systemd Unix socket activation tests --- .../ListenOptions.cs | 43 ++++++ .../Internal/FileHandleType.cs | 15 ++ .../Internal/IEndPointInformation.cs | 7 + .../Internal/LibuvConstants.cs | 14 ++ .../Internal/Listener.cs | 132 ++++++++++++------ .../Internal/ListenerContext.cs | 62 ++++++-- .../Internal/Networking/LibuvFunctions.cs | 11 ++ .../Internal/Networking/UvPipeHandle.cs | 5 + .../SystemdActivation/Dockerfile | 2 +- .../SystemdActivation/docker-entrypoint.sh | 4 + .../SystemdActivation/docker.sh | 3 + 11 files changed, 248 insertions(+), 50 deletions(-) create mode 100644 src/Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions/Internal/FileHandleType.cs diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Core/ListenOptions.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Core/ListenOptions.cs index 5508b46afe..46bae7fba3 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Core/ListenOptions.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Core/ListenOptions.cs @@ -16,6 +16,8 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core /// public class ListenOptions : IEndPointInformation { + private FileHandleType _handleType; + internal ListenOptions(IPEndPoint endPoint) { Type = ListenType.IPEndPoint; @@ -29,9 +31,24 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core } internal ListenOptions(ulong fileHandle) + : this(fileHandle, FileHandleType.Auto) + { + } + + internal ListenOptions(ulong fileHandle, FileHandleType handleType) { Type = ListenType.FileHandle; FileHandle = fileHandle; + switch (handleType) + { + case FileHandleType.Auto: + case FileHandleType.Tcp: + case FileHandleType.Pipe: + _handleType = handleType; + break; + default: + throw new NotSupportedException(); + } } /// @@ -39,6 +56,32 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core /// public ListenType Type { get; } + public FileHandleType HandleType + { + get => _handleType; + set + { + if (value == _handleType) + { + return; + } + if (Type != ListenType.FileHandle || _handleType != FileHandleType.Auto) + { + throw new InvalidOperationException(); + } + + switch (value) + { + case FileHandleType.Tcp: + case FileHandleType.Pipe: + _handleType = value; + break; + default: + throw new ArgumentException(nameof(HandleType)); + } + } + } + // IPEndPoint is mutable so port 0 can be updated to the bound port. /// /// The to bind to. diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions/Internal/FileHandleType.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions/Internal/FileHandleType.cs new file mode 100644 index 0000000000..bb70e4ec34 --- /dev/null +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions/Internal/FileHandleType.cs @@ -0,0 +1,15 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal +{ + /// + /// Enumerates the types. + /// + public enum FileHandleType + { + Auto, + Tcp, + Pipe + } +} diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions/Internal/IEndPointInformation.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions/Internal/IEndPointInformation.cs index 7f025ffe23..1b7abfa497 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions/Internal/IEndPointInformation.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions/Internal/IEndPointInformation.cs @@ -31,6 +31,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal /// ulong FileHandle { get; } + // HandleType is mutable so it can be re-specified later. + /// + /// The type of file descriptor being used. + /// Only set if is . + /// + FileHandleType HandleType { get; set; } + /// /// Set to false to enable Nagle's algorithm for all connections. /// diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvConstants.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvConstants.cs index 70424f7b0c..1e7c8f319d 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvConstants.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/LibuvConstants.cs @@ -12,6 +12,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal public const int EOF = -4095; public static readonly int? ECONNRESET = GetECONNRESET(); public static readonly int? EADDRINUSE = GetEADDRINUSE(); + public static readonly int? ENOTSUP = GetENOTSUP(); private static int? GetECONNRESET() { @@ -46,5 +47,18 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal } return null; } + + private static int? GetENOTSUP() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) + { + return -95; + } + else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) + { + return -45; + } + return null; + } } } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/Listener.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/Listener.cs index a4b6a8d6bb..26c6fba06a 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/Listener.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/Listener.cs @@ -46,53 +46,103 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal switch (EndPointInformation.Type) { case ListenType.IPEndPoint: - case ListenType.FileHandle: - var socket = new UvTcpHandle(Log); - - try - { - socket.Init(Thread.Loop, Thread.QueueCloseHandle); - socket.NoDelay(EndPointInformation.NoDelay); - - if (EndPointInformation.Type == ListenType.IPEndPoint) - { - socket.Bind(EndPointInformation.IPEndPoint); - - // If requested port was "0", replace with assigned dynamic port. - EndPointInformation.IPEndPoint = socket.GetSockIPEndPoint(); - } - else - { - socket.Open((IntPtr)EndPointInformation.FileHandle); - } - } - catch - { - socket.Dispose(); - throw; - } - - return socket; + return ListenTcp(useFileHandle: false); case ListenType.SocketPath: - var pipe = new UvPipeHandle(Log); - - try - { - pipe.Init(Thread.Loop, Thread.QueueCloseHandle, false); - pipe.Bind(EndPointInformation.SocketPath); - } - catch - { - pipe.Dispose(); - throw; - } - - return pipe; + return ListenPipe(useFileHandle: false); + case ListenType.FileHandle: + return ListenHandle(); default: throw new NotSupportedException(); } } + private UvTcpHandle ListenTcp(bool useFileHandle) + { + var socket = new UvTcpHandle(Log); + + try + { + socket.Init(Thread.Loop, Thread.QueueCloseHandle); + socket.NoDelay(EndPointInformation.NoDelay); + + if (!useFileHandle) + { + socket.Bind(EndPointInformation.IPEndPoint); + + // If requested port was "0", replace with assigned dynamic port. + EndPointInformation.IPEndPoint = socket.GetSockIPEndPoint(); + } + else + { + socket.Open((IntPtr)EndPointInformation.FileHandle); + } + } + catch + { + socket.Dispose(); + throw; + } + + return socket; + } + + private UvPipeHandle ListenPipe(bool useFileHandle) + { + var pipe = new UvPipeHandle(Log); + + try + { + pipe.Init(Thread.Loop, Thread.QueueCloseHandle, false); + + if (!useFileHandle) + { + pipe.Bind(EndPointInformation.SocketPath); + } + else + { + pipe.Open((IntPtr)EndPointInformation.FileHandle); + } + } + catch + { + pipe.Dispose(); + throw; + } + + return pipe; + } + + private UvStreamHandle ListenHandle() + { + switch (EndPointInformation.HandleType) + { + case FileHandleType.Auto: + break; + case FileHandleType.Tcp: + return ListenTcp(useFileHandle: true); + case FileHandleType.Pipe: + return ListenPipe(useFileHandle: true); + default: + throw new NotSupportedException(); + } + + UvStreamHandle handle; + try + { + handle = ListenTcp(useFileHandle: true); + EndPointInformation.HandleType = FileHandleType.Tcp; + return handle; + } + catch (UvException exception) when (exception.StatusCode == LibuvConstants.ENOTSUP) + { + Log.LogDebug(0, exception, "Listener.ListenHandle"); + } + + handle = ListenPipe(useFileHandle: true); + EndPointInformation.HandleType = FileHandleType.Pipe; + return handle; + } + private static void ConnectionCallback(UvStreamHandle stream, int status, UvException error, object state) { var listener = (Listener)state; diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/ListenerContext.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/ListenerContext.cs index 217bac0d12..37ea4eed36 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/ListenerContext.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/ListenerContext.cs @@ -28,18 +28,64 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal switch (EndPointInformation.Type) { case ListenType.IPEndPoint: - case ListenType.FileHandle: - var tcpHandle = new UvTcpHandle(TransportContext.Log); - tcpHandle.Init(Thread.Loop, Thread.QueueCloseHandle); - tcpHandle.NoDelay(EndPointInformation.NoDelay); - return tcpHandle; + return AcceptTcp(); case ListenType.SocketPath: - var pipeHandle = new UvPipeHandle(TransportContext.Log); - pipeHandle.Init(Thread.Loop, Thread.QueueCloseHandle); - return pipeHandle; + return AcceptPipe(); + case ListenType.FileHandle: + return AcceptHandle(); default: throw new InvalidOperationException(); } } + + private UvTcpHandle AcceptTcp() + { + var socket = new UvTcpHandle(TransportContext.Log); + + try + { + socket.Init(Thread.Loop, Thread.QueueCloseHandle); + socket.NoDelay(EndPointInformation.NoDelay); + } + catch + { + socket.Dispose(); + throw; + } + + return socket; + } + + private UvPipeHandle AcceptPipe() + { + var pipe = new UvPipeHandle(TransportContext.Log); + + try + { + pipe.Init(Thread.Loop, Thread.QueueCloseHandle); + } + catch + { + pipe.Dispose(); + throw; + } + + return pipe; + } + + private UvStreamHandle AcceptHandle() + { + switch (EndPointInformation.HandleType) + { + case FileHandleType.Auto: + throw new InvalidOperationException("Cannot accept on a non-specific file handle, listen should be performed first."); + case FileHandleType.Tcp: + return AcceptTcp(); + case FileHandleType.Pipe: + return AcceptPipe(); + default: + throw new NotSupportedException(); + } + } } } diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/Networking/LibuvFunctions.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/Networking/LibuvFunctions.cs index 06e7030bef..d5452242e6 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/Networking/LibuvFunctions.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/Networking/LibuvFunctions.cs @@ -30,6 +30,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networkin _uv_tcp_nodelay = NativeMethods.uv_tcp_nodelay; _uv_pipe_init = NativeMethods.uv_pipe_init; _uv_pipe_bind = NativeMethods.uv_pipe_bind; + _uv_pipe_open = NativeMethods.uv_pipe_open; _uv_listen = NativeMethods.uv_listen; _uv_accept = NativeMethods.uv_accept; _uv_pipe_connect = NativeMethods.uv_pipe_connect; @@ -229,6 +230,13 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networkin ThrowIfErrored(_uv_pipe_bind(handle, name)); } + protected Func _uv_pipe_open; + public void pipe_open(UvPipeHandle handle, IntPtr hSocket) + { + handle.Validate(); + ThrowIfErrored(_uv_pipe_open(handle, hSocket)); + } + [UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void uv_connection_cb(IntPtr server, int status); protected Func _uv_listen; @@ -534,6 +542,9 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networkin [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] public static extern int uv_pipe_bind(UvPipeHandle loop, string name); + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] + public static extern int uv_pipe_open(UvPipeHandle handle, IntPtr hSocket); + [DllImport("libuv", CallingConvention = CallingConvention.Cdecl)] public static extern int uv_listen(UvStreamHandle handle, int backlog, uv_connection_cb cb); diff --git a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/Networking/UvPipeHandle.cs b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/Networking/UvPipeHandle.cs index 338cc1f643..9afdb67712 100644 --- a/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/Networking/UvPipeHandle.cs +++ b/src/Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv/Internal/Networking/UvPipeHandle.cs @@ -21,6 +21,11 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Transport.Libuv.Internal.Networkin _uv.pipe_init(loop, this, ipc); } + public void Open(IntPtr fileDescriptor) + { + _uv.pipe_open(this, fileDescriptor); + } + public void Bind(string name) { _uv.pipe_bind(this, name); diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/Dockerfile b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/Dockerfile index 8fd99946ee..5aee5312dd 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/Dockerfile +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/Dockerfile @@ -15,7 +15,7 @@ RUN ["ln", "-s", "/usr/share/dotnet/dotnet", "/usr/bin/dotnet"] ADD publish/ /publish/ # Expose target ports. -EXPOSE 8080 8081 8082 +EXPOSE 8080 8081 8082 8083 8084 8085 # Set entrypoint. COPY ./docker-entrypoint.sh / diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/docker-entrypoint.sh b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/docker-entrypoint.sh index db0de9d602..c85c5fe0bd 100644 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/docker-entrypoint.sh +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/docker-entrypoint.sh @@ -6,5 +6,9 @@ cd /publish systemd-socket-activate -l 8080 -E BASE_PORT=7000 dotnet SampleApp.dll & socat TCP-LISTEN:8081,fork TCP-CONNECT:127.0.0.1:7000 & socat TCP-LISTEN:8082,fork TCP-CONNECT:127.0.0.1:7001 & +systemd-socket-activate -l /tmp/activate-kestrel.sock -E BASE_PORT=7100 dotnet SampleApp.dll & +socat TCP-LISTEN:8083,fork UNIX-CLIENT:/tmp/activate-kestrel.sock & +socat TCP-LISTEN:8084,fork TCP-CONNECT:127.0.0.1:7100 & +socat TCP-LISTEN:8085,fork TCP-CONNECT:127.0.0.1:7101 & trap 'exit 0' SIGTERM wait diff --git a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/docker.sh b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/docker.sh index cce561474d..eec694cdd7 100755 --- a/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/docker.sh +++ b/test/Microsoft.AspNetCore.Server.Kestrel.FunctionalTests/SystemdActivation/docker.sh @@ -15,6 +15,9 @@ for i in {1..10}; do curl -f http://$(docker port $container 8080/tcp) \ && curl -f http://$(docker port $container 8081/tcp) \ && curl -fk https://$(docker port $container 8082/tcp) \ + && curl -f http://$(docker port $container 8083/tcp) \ + && curl -f http://$(docker port $container 8084/tcp) \ + && curl -fk https://$(docker port $container 8085/tcp) \ && exit 0 || sleep 1; done